一. 定义
js
如同其他高级语言一样都有垃圾回收机制,会周期性的检查之前分配的内存是否可达,帮助开发者管理内存。对不可达的内存通过算法确定、标记并适时回收。
而内存泄露则可以理解为当应用程序不再需要占用内存时,由于某些原因操作系统未回收其内存。
二. 堆?栈?队列?
2.1 任务队列
由于js
是单线程,所以所有的任务都需要排队。学过操作系统的都知道单线程最大的浪费就是输入输出时的等待,所以JavaScript
设计者采取了另一种策略:主线程忽略IO
设备,挂起处于等待中的任务,先运行它后面的任务。等到IO
设备返回结果后再讲挂起任务继续执行下去
2.1.1 同步任务(synchronous)
在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
2.1.2 异步任务(asynchronous)
不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
2.2 函数执行栈
函数的嵌套调用就是通过函数执行栈。每嵌套一层向栈中推入函数信息,得到返回值后出栈
2.3 堆
而主要的用户创建的对象就存放在堆中。内存泄漏定位的主要区域就在这里。
三. Mark-and-Sweep(标记扫描)算法
大部分垃圾回收机制的算法均称为mark-and-sweep
,由以下几部分组成
- 垃圾回收器创建了一个“
roots
”列表。Roots
通常是代码中全局变量的引用。JavaScript
中,“window
”对象是一个全局变量,被当作root
。window
对象总是存在,因此垃圾回收器可以检查它和它的所有子对象是否存在(即不是垃圾); - 所有的
roots
被检查和标记为激活(即不是垃圾)。所有的子对象也被递归地检查。从root
开始的所有对象如果是可达的,它就不被当作垃圾。 - 所有未被标记的内存会被当做垃圾,收集器现在可以释放内存,归还给操作系统了。
不需要的引用是指开发者明知内存引用不再需要,却由于某些原因,它仍被留在激活的
root
树中。在JavaScript
中,不需要的引用是保留在代码中的变量,它不再需要,却指向一块本该被释放的内存。
四. 举几个栗子
4.1 意外的全局变量
4.1.1 粗心
如果定义时忘记了var
那么引擎会自动帮你创建一个全局变量。虽然无伤大雅还是尽量避免的好
4.1.2 this创建的意外
function foo() {
this.a = "accidental global";
}
4.2 闭包&&定时器
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) {
console.log("hi");
}
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
代码片段做了一件事情:每次调用replaceThing
,theThing
得到一个包含一个大数组和一个新闭包(someMethod
)的新对象。同时,变量unused
是一个引用originalThing
的闭包(先前的replaceThing
又调用了theThing
)。思绪混乱了吗?
一旦一个作用域被创建为闭包,那么它的父作用域将被共享
最重要的事情是,闭包的作用域一旦创建,它们有同样的父级作用域,作用域是共享的。someMethod()
可以通过theThing
使用,someMethod()
与unused
分享闭包作用域,尽管unused
从未使用。它引用的originalThing
迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC
)并无法降低内存占用。本质上,闭包的链表已经创建,每一个闭包作用域携带一个指向大数组的间接的引用,造成严重的内存泄露。
当然,我们可以通过在replaceThing
的最后添加originalThing = null
来修复此问题。
4.3 DOM引用
当需要删除DOM的引用时候注意一点:同一份DOM一般拥有两份引用:DOM树和字典
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
// The image is a direct child of the body element.
document.body.removeChild(document.getElementById('image'));
// At this point, we still have a reference to #button in the
//global elements object. In other words, the button element is
//still in memory and cannot be collected by the GC.
elements.img = null // tell the engine to collect the memory
}
还有一个额外的考虑,当涉及 DOM 树内部或叶子节点的引用时,必须考虑这一点。假设你在 JavaScript 代码中保留了对 table 特定单元格(<td>
)的引用。有一天,你决定从 DOM 中删除该 table,但扔保留着对该单元格的引用。直观地来看,可以假设 GC 将收集除了该单元格之外所有的内容。实际上,这不会发生的:该单元格是该 table 的子节点,并且 children 保持着对它们 parents 的引用。也就是说,在 JavaScript 代码中对单元格的引用会导致整个表都保留在内存中的。保留 DOM 元素的引用时,需要仔细考虑。
好啦今天的话题就讲到这里