JavaScript中的垃圾回收

JavaScrip中的垃圾回收。

本文参考如下的帖子

JavaScript 的内存管理

不同于CC++的手动释放内存,JavaScript由引擎来为基本类型,对象,函数等来分配内存,分配内存这一块操作对用户完全透明,用户不用在意内存如何分配,分配到哪里,如何回收,这些都由引擎给做了。

由垃圾回收器来进行垃圾回收,减少了程序员的心智负担,但同时也减弱了对程序的控制程度,但总体上利大于弊。

垃圾回收的目的

将不在被使用的内存释放掉,使得系统能够重新的使用这些内存来存放相关的变量

垃圾回收算法

引用计数

对象A能够通过自身去使用对象B,那么这个对象A就持有了对对象B的引用。

引用计数即通过判断对象是否被其他对象引用到来作为是否回首的依据。

如果一个对象没有被其他对象引用,那么这个对象应该被释放,因为已经没有其他的对象能够使用到该对象了,反之,就不应该释放该对象,避免未来的某个时刻被其他对象使用时造成错误。

缺点:存在循环引用的情况,当两个对象已经不能被其他任何对象引用但是这两个对象存在对对方的引用时,该垃圾回收算法无法对这两个对象进行回收,从而造成内存泄漏。

1
2
3
4
5
6
7
8
9
10
function f() {
var o1 = {};
var o2 = {};
o1.a = o2; // o1 引用 o2
o2.a = o1; // o2 引用 o1

return "Hello World!";
}

f();

当函数f执行完毕时,正常情况下应该释放对象o1o2,但是算法发现两者仍然被其他对象引用,于是无法进行回收,造成了内存泄漏。

MDN内存管理一章指出在IE67上使用引用计数来回收DOM对象,这种方式很容易造成内存泄漏。

1
2
3
4
5
6
7
function f() {
const div = document.createElement("div");
div.circleReference = div;
// 挂载一个较大的数组,这样能够方便从内存占用上分析出来是否内存泄露了
div.lotsOfData = new Array(10000).join("*");
return "Hello World!";
}

如果div对象成功释放,那么属性lotsOfData应该也被释放,因为只有div引用了它,而实际上由于属性circleReference引用了自身,使得div的引用计数不为0(循环引用了),无法释放,导致属性lotsOfData也无法释放,造成内存泄漏。

标记清除算法

通过一个“根”对象,去寻找能够访问到的全部的变量,然后把无法访问到的变量进行回收。

相比与引用计数,如果两个对象不再被其他对象引用,但是两者相互引用时,标记清除算法也能够鉴别出这两个对象应该被回收,因为从“根”对象已经无法达到这两个对象了。

它的有优点也是它的缺点,说成限制可能比较准确,即“那些无法从根对象查询到的对象都将被清除”。

标记清除算法已成为主流的JavaScript的垃圾回收算法,基于标记清除算法来改进垃圾回收。

v8 堆的构成

  • 新生代区 New Space。新生代分为两个区,一个是from区,一个是to区(相对而言),每次分配对象都会在from区分配,当内存将占满时,开始垃圾回收机制,把from区里面还被引用的对象拷贝到to区,然后新分配的对象开始放在to区,当to区也快占满时,就以同样的步骤转移到from,如此往复。新生代区的回收频率很快。新生代由副垃圾收集器(Scavenging)进行垃圾回收。

  • 老生代区 Old Space。当位于新生代区的对象长时间留在新生代区时,v8会把这个对象搬到老生代中(晋升机制),然后隔一段时间也对老生代进行扫描,把已经无法可达的对象给释放掉。

    晋升机制的条件:

    • 经历过一次副垃圾回收(Scavenging)算法,且并未被标记清除的,也就是过一次翻转置换操作的对象。
    • 在进行翻转置换时,被复制的对象大于to space空间的25%。(from spaceto space一定是一样大的)。

    晋升后的对象分配到老生代内存区,便由老生代内存区来管理。

  • 大对象区 Large Object Space,专门存储大的对象,由于大对象的拷贝耗时,所以对大对象基本不移动。

  • 代码区 Code Space 存放代码对象,最大限制为512MB,也是唯一拥有执行权限的内存。

  • 单元区、属性单元区、Map 区 Cell SpaceProperty Cell SpaceMap SpaceMap空间存放对象的Map信息也就是隐藏类Hiden Class最大限制为8MB;每个Map对象固定大小,为了快速定位,所以将该空间单独出来。

如何查看是否内存泄漏

Chrome浏览器,或者微软的Edge浏览器中,通过F12呼出开发者工具栏。

  • 打开开发者工具,选择Performance面板;

    在 Edge 中,对应的中文面板为性能面板:

  • 在顶部勾选Memory

  • 点击左上角的record按钮;

    Edge中,对应为左上角的记录按钮

  • 点击记录后,在页面上进行各种操作,模拟用户的使用情况;

  • 一段时间后,点击对话框的stop按钮,面板上就会显示这段时间的内存占用情况。

Node中,可以使用process.memoryUsage方法来查看内存使用情况。

process.memoryUsage() - Node.js 官方文档

这个方法返回一个以字节为单位的描述内存情况的对象,属性值类型都为整型。

返回对象的属性如下:

  • rss 整个进程在内存中占用的大小,包括所有的C++对象,JavaScript对象以及代码;
  • heapTotal 堆的大小;
  • heapUsed 已使用堆的大小;
  • external 存放绑定到由v8引擎管理的JavaScriptC++对象;
  • arrayBuffers 分配给ArrayBufferSharedArrayBuffer对象的内存,即包括所有的Buffer对象,已包含在external的大小中。

对于内存泄漏的判断,一般只需关注heapUsedheapTotal即可。

内存泄漏例子

全局变量

1
2
3
4
5
6
7
8
9
10
11
function fn() {
// 意外定义了一个全局变量,可以使用window.a访问到它,无法被回收。
a = 1;

// 正确的做法使用关键字var,let,const定义变量再使用。
// var a = 1;
// let a = 1;
// const a = 1;
}

fn();

定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
const array = [
/*一个很大的数组*/
];

setInterval(function () {
// 执行一些操作。
// ...
// 比如
const div = document.getElementById("div1");
if (div) {
// 使用array渲染
}
}, 2000);

当定时器中div获取不到时,整个定时器失去了意义,但此时由于定时器没有被回收,而定时器又引用了array,导致array也无法被回收。

闭包

1
2
3
4
5
6
7
8
9
function fn() {
const str = new Array(1000000).join("*");
return function () {
return {
str,
};
};
}
window.a = fn()();

DOM 对象的引用

1
2
3
4
5
6
7
8
9
10
var elements = {
image: document.getElementById("image"),
};
function doStuff() {
elements.image.src = "http://example.com/image_name.png";
}
function removeImage() {
document.body.removeChild(document.getElementById("image"));
// 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}