从Node.js中的GC到闭包中的内存泄漏

最近在系统的看Node,补了一些以前没有深入的东西,复习整理一下~

Node.js中的垃圾回收

V8将堆分为新生代和老生代,新生代空间中的对象较小,垃圾回收快,在新生代存活的对象会进入老生代空间。

新生代将空间一分为二,将存活的对象复制到另一个空间,然后替换替换回去,速度快但是空间花销大。

老生代空间垃圾回收分位标记清除和标记整理两个阶段。
标记清除:老生代空间活对象存在标记,没有被标记的会被清除。
标记整理:为了解决清除后的内存碎片问题,将存活的对象进行整理。


内存泄漏

内存泄漏可能存在的原因:

  1. 未声明的变量或挂在全局 global 下的变量不会自动回收
  2. 闭包
  3. 做缓存时,缓存中存储的键越多,长期存活的对象也就越多,垃圾回收时将会对这些对对象做无用功
  4. setInterval 执行完之后会返回一个值且不会自动释放
  5. 匿名函数

内存监测的方法:

process.memoryUsage

Node中使用process.memoryUsage可以查看进程内存使用情况

rss(resident set size):RAM 中保存的进程占用的内存部分,包括代码本身、栈、堆。
heapTotal:堆中总共申请到的内存量。
heapUsed:堆中目前用到的内存量,判断内存泄漏我们主要以这个字段为准。
external: V8 引擎内部的 C++ 对象占用的内存。

堆一般用来存储数组和对象,栈用来存储方法和简单变量。

memory

浏览器中可使用chrome的开放工具中的performance & memory监测。
performance绘制内存使用图,
memory显示具体内存占用情况。


闭包如何导致了内存泄漏

一个简单的例子:

1
2
3
function foo(element, a, b) {
element.onclick = function() { /* uses a and b */ };
}

上述代码存在闭包,因此会保留element、a、b的引用,直到引用闭包处被回收。

浏览器使用引用计数标记是否为活对象,如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。

稍复杂的例子:

1
2
3
4
5
6
function fa() {
var e = document.getElementById("id");
e.event = function () {
alert("hello word");
};
}

存储e的堆空间引用计数为2,存储匿名函数的堆空间引用为2,当e自动回收引用-1变为1,在外若执行e.event = null , 则匿名函数的堆空间引用为1,e和匿名函数的引用此时都不为0,都无法回收。


内存泄漏的一些优化方案

  1. weakMap
    1
    2
    3
    4
    let a = new map();
    b = new Object();
    a[b] = new Array(20 * 1024 * 1024)
    b = null

    这种情况下无法回收a[b]的堆内存,但是通过使用weakMap在b=null时可以自动回收map中b作为key值对应的存储空间。

  1. 栈溢出与尾调用优化
    当递归调用层次过多的话,会导致栈溢出错误,此时可通过尾调用优化的方式解决。

引用:

1. 也议 js闭包和ie内存泄露原理
2. JavaScript 内存泄漏教程
3. Nodejs中的内存管理和V8垃圾回收机制