机制
标记清除
JS
中最常见的垃圾回收方式是标记清除\
标记清除的概念也好理解,从根部出发看是否能达到某个对象,如果能达到则认定这个对象还被需要,如果无法达到,则释放它,这个过程大致分为三步:
- 垃圾回收器创建
roots
列表,roots
通常是代码中保留引用的全局变量(因此引起内存泄漏原因之一会有全局变量),在js
中,我们一般认定全局对象window
作为root
,也就是所谓的根部。 - 从根部出发检查所有的
roots
,所有的children
也会被递归检查,能从root
到达的都会被标记为active
。 - 未被标记为
active
的数据被认定为不再需要,垃圾回收器开始释放它们。
怎么理解?想象一下你自己去实现,假设你有一个包含所有对象的数组,假设为 a
,需要清除和不需要清除的对象,找到 roots
列表,然后做一个 dfs
遍历标记找到的对象,然后在对 a
数组进行遍历,没有被标记的就清楚
再来一个垃圾回收实例
根对象
- 全局对象,
window
- 浏览器对象,如
DOM
元素
变量回收原则
- 全局变量不会被回收
- 局部变量会被回收,也就是函数一旦运行完以后,函数内部的东西都会被销毁
- 只要被另外一个作用域所引用就不会被回收
内存泄漏
即无法释放已经不使用的内存,有如下情况
全局变量
意外的全局变量引起的内存泄漏,如
// 没有使用 let/const/var 声明的变量会自动变成全局变量
function foo(arg) {
bar = "this is a hidden global variable";
}
// foo() 执行环境为全局,this 指向 window 对象
function foo() {
this.bar = "this is a hidden global variable";
}
foo();
\
原因:全局变量,不会被回收。\
解决:使用严格模式避免。
闭包
闭包引起的内存泄漏,如
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);
\
原因:闭包可以维持函数内局部变量,使其得不到释放。\
解决:将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对 dom
的引用。
被遗忘的定时器或者回调
如下
<script>
export default {
mounted() {
this.resizeEventCallback = () => {
// 这里做一些操作
}
window.addEventListener('resize', this.resizeEventCallback)
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeEventCallback)
},
}
</script>
原因:定时器中有 dom
的引用,即使 dom
删除了,但是定时器还在,所以内存中还是有这个dom。\
解决:手动删除定时器和 dom
。
- 没有清理的DOM元素引用\
原因:虽然别的地方删除了,但是对象中还存在对dom的引用\
解决:手动删除。 - 子元素存在引用引起的内存泄漏\
原因:div中的ul li 得到这个div,会间接引用某个得到的li,那么此时因为div间接引用li,即使li被清空,也还是在内存中,并且只要li不被删除,他的父元素都不会被删除。\
解决:手动删除清空。
避免内存泄漏的方法
- 少用全局变量,避免意外产生全局变量
- 使用闭包要及时注意,有Dom元素的引用要及时清理。
- 计时器里的回调没用的时候要记得销毁。
- 为了避免疏忽导致的遗忘,我们可以使用
WeakSet
和WeakMap
结构,它们对于值的引用都是不计入垃圾回收机制的,表示这是弱引用。
示例
var i = 1;
var i = 2;
var add = (function () {
var i = 0;
return function () {
i++;
console.log(i);
};
})();
add();
answer: 3
,全局变量有两个,即 i
和 add
,局部变量有一个 i
,因为局部变量被另一个作用域引用,所以局部变量 i
也不回收
var i = 1; // 全局变量不会被回收
var i = 2; // 这里重复声明变量i,因此var声明被忽略,只是把i赋值为2
var add = (function () {
// 全局变量不会被回收
var i = 0; // 局部变量
return function () {
i++;
console.log(i); // 被另一个作用域引用导致不会被回收
};
})();
add();