17.1 概述
下面这段话引自《JavaScript权威指南(第四版)》
- 由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。
像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
JS的垃圾回收机制是为了防止内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。
C#、Java、JavaScript有自动垃圾回收机制,但c++和c就没有垃圾回收机制,也许是因为垃圾回收机制必须由一种平台来实现。在JS中,JS的执行环境会负责管理代码执行过程中使用的内存。
17.2 什么是垃圾
一般来说没有被引用的对象就是垃圾,就是要被清除。有个例外如果几个对象引用形成一个环,互相引用,但根访问不到它们,这几个对象也是垃圾,也要被清除。
17.3 JavaScript的垃圾回收方式
现在各大浏览器通常用采用的垃圾回收有两种方法:
- 标记清除(mark and sweep)
- 引用计数(reference counting)
1.标记清除(mark and sweep)
JS中最常见的垃圾回收方式是标记清除。
工作原理:
是当变量进入环境时(例如,在函数中声明一个变量时),将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
工作流程:
-
垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记。
-
去掉环境中的变量以及被环境中的变量引用的变量的标记。
-
这种之后再被加上标记的会被视为准备删除的变量(环境中的这些变量已经无法访问)。
-
垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间。
2.引用计数(reference counting)
这种方式常常会引起内存泄漏,低版本的IE使用这种方式。
工作原理:
跟踪记录每个值被引用的次数。
工作流程:
-
声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是1。
-
同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1.
-
当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数-1.
-
当引用次数变成0时,说明没办法访问这个值了。
-
当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。
该方式会引起内存泄漏的原因是它不能解决循环引用的问题。
循环引用的时候就会释放不掉内存。
循环引用就是对象A中包含另一个指向对象B的指针,B中也包含一个指向A的引用。因为IE中的BOM、DOM的实现使用了COM,而COM对象使用的垃圾收集机制是引用计数策略。所以存在循环引用的问题。
解决:
手工断开js对象和DOM之间的链接。赋值为null。IE9把DOM和BOM转换成真正的JS对象了,所以避免了这个问题。
17.3 如何减少JavaScript中的垃圾回收
1. 对象尽量复用
尤其是在循环等地方出现创建新对象,能复用就复用。不用的对象,尽可能设置为
null
,尽快被垃圾回收掉。
var t = {} // 放在循环外只会创建一个新对象。
for (var i = 0; i < 10; i++) {
// var t = {};//这里会创建10次对象
t.age = 19
t.name = '123'
t.index = i
console.log(t)
}
t = null //对象如果已经不用了,那就立即设置为null;等待垃圾回收。
2. 数组array优化
将
[]
赋值给一个数组对象,是清空数组的捷径(例如:arr = []
😉,但是需要注意的是,这种方式又创建了一个新的空对象,并且将原来的数组对象变成了一小片内存垃圾!
实际上,将数组长度赋值为0(arr.length = 0
)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。
const arr = [1, 2, 3, 4];
console.log('xiuzhu');
arr.length = 0 // 可以直接让数字清空,而且数组类型不变。
// arr = []; 虽然让a变量成一个空数组,但是在堆上重新申请了一个空数组对象。
17.4 什么情况会引起内存泄漏
1. 意外的全局变量引起的内存泄漏
function foo(arg) {
bar = "this is a hidden global variable";
}
bar
没被声明,会变成一个全局变量,在页面关闭之前不会被释放。
另一种意外的全局变量可能由this
创建:
function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();
原因:全局变量,不会被回收。
解决:使用严格模式避免。
2. 闭包引起的内存泄漏
function bindEvent() {
var obj = document.createElement('xxx')
obj.onclick = function() {
// Even if it is a empty function
}
}
闭包可以维持函数内局部变量,使其得不到释放。上例定义事件回调时,由于是函数内定义函数,并且内部函数–事件回调引用外部函数,形成了闭包。
// 解决方法
// 将事件处理函数定义在外面
function bindEvent() {
var obj = document.createElement('xxx')
obj.onclick = onclickHandler
}
// 或者在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
var obj = document.createElement('xxx')
obj.onclick = function() {
// Even if it is a empty function
}
obj = null
}
原因:闭包可以维持函数内局部变量,使其得不到释放。
解决:将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom
的引用。
3. 没有清理的DOM元素引用
有时,保存
DOM
节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行DOM
存成字典(JSON
键值对)或者数组很有意义。
此时,同样的 DOM 元素存在两个引用:一个在DOM 树
中,另一个在字典
中。将来你决定删除这些行时,需要把两个引用都清除。
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
}
function removeButton() {
document.body.removeChild(document.getElementById('button'));
// 此时,仍旧存在一个全局的 #button 的引用
// elements 字典。button 元素仍旧在内存中,不能被GC回收。
}
原因:虽然我们用
removeChild
移除了button
,但是还在elements
对象里保存着#button
的引用,
解决:手动删除。
4. 被遗忘的定时器或者回调
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
原因:定时器中有dom的引用,即使dom删除了,但是定时器还在,所以内存中还是有这个dom。
解决:手动删除定时器和dom。
5. 子元素存在引用引起的内存泄漏
原因:div中的ul、li 得到这个div,会间接引用某个得到的li,那么此时因为div间接引用li,即使li被清空,也还是在内存中,并且只要li不被删除,他的父元素都不会被删除。
解决:手动删除清空。
6. Vue造成内存泄漏的情况
补充
7. 内存泄漏的识别方法
图中 Heap 对应的部分就可以看到内存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我们称为min),min在不断上涨,那么肯定是有较为严重的内存泄漏问题。
步骤:
- 打开开发者工具 Performance
- 勾选 Screenshots 和 memory
- 左上角小圆点开始录制(record)
- 停止录制