JavaScript | 内存泄漏

概念

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果

垃圾回收

现如今大部发的编程语言也都提供了垃圾回收的机制,JavaScirpt作为一种高级编程语言,也提供了垃圾回收的机制,这使得不需要像C语言那样需要手动申请并释放内存。

原理:JavaScript在变量声明的时候就会自动分配内存,比如Number类型的数据在声明后就会自动放入到栈内存中,然后JavaScript让程序可以定期找出没有在使用的变量,并释放其占用的内存,关于内存管理可以参考官方文档

垃圾回收机制在JavaScript中主要有两种方式分别为:

  • 引用计数
  • 标记清除

引用计数

此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。

如果此时没有其他引用指向该对象(零引用)则此对象将被回收。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let a = {
  b:{
    c: '114514'
  }
}
// 新建了两个对象,其中一个对象(内对象)作为另一个对象的引用(外对象),另一个对象分配给a
// 此时两个对象都被引用了不可以进行垃圾回收

let d = a;
// d对(外对象)有了引用
a = 114514;
// a解除了对(外对象)的引用,此时d还对(外对象)有引用,所以此时不可以进行回收

let e = d.a; //引用了(外对象)的a属性
// 外对象此时有两个引用分别为d与e[对外对象a的引用]

d = 1919810; // 解除了d对(外对象)的引用了,但是现在e对(外对象)的a属性还有引用所以还不能进行垃圾回收

e = null; // (外对象)的a属性此时为零引用了,所以此时可以进行垃圾回收了

标记清除

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

了解:从 2012 年起,所有现代浏览器都使用了标记 - 清除垃圾回收算法。所有对 JavaScript 垃圾回收算法的改进都是基于标记 - 清除算法的改进,并没有改进标记 - 清除算法本身和它对“对象是否不再需要”的简化定义。

垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象打上对应的标记,然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉,剩下的那些就是待删除的变量了,原因是任何在上下文中的变量都访问不到它们了,随后垃圾回收机制会做内存清理,销毁所有此时带标记的值并释放对应的内存。

如果对上面的这段话不理解,可以参考下面的这篇文章了解下最基础的标记回收算法。

截图

首先我们从global也就是window出发,开始去找它每一个箭头,然后找到所有能根据箭头找到的变量,我们给这样的变量一个:√。其他找不到的标记为:×。最后清除所有为×的变量

截图


作者:听你听我 链接:https://zhuanlan.zhihu.com/p/353346756

案例:

1
2
3
4
5
6
7
8
var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

小结

有了垃圾回收机制,不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用。如果是的话,就必须手动解除引用

常见的内存泄漏

意外的全局变量

1
2
3
4
function test(aaa){
  beastSenbai = '114514';
}
test();

浏览器控制台输入beastSenbai输出'114514',因为此时这个变量变成了全局变量类似的还有下面这种情况

1
2
3
4
function test(aaa){
  this.beastSenbai = '114514';
}
test();

上面调用test方法相当于window.test(),此时函数内部的this指向window,所以这相当于给window对象添加了beastSenbai这个属性。

解决办法:

  • 使用严格模式,在js代码执行前加上'use strict'关键字即可

❕注:在严格模式下,普通函数的this指针会指向undefined

闭包

闭包:在一个作用域中可以访问另一个函数内部的局部变量的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function makeFunc() {
  var count = 0;
  function displayName() {
    count++;
    console.log(count,'闭包内部引用了count count不会被释放');
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc(); // 1 闭包内部引用了count count不会被释放
myFunc(); // 2 闭包内部引用了count count不会被释放
myFunc(); // 3 闭包内部引用了count count不会被释放
myFunc(); // 4 闭包内部引用了count count不会被释放

解决办法:

  • 解除myFuncmakeFunc()的引用:

    myFunc = null;

没有清理对DOM元素引用

对于下面这个案例,虽然画面上的button标签被删除了,但是在控制台里打btn依旧会显示<button>aaa</button>,但是还在btn变量里保存着button元素节点的引用,所以DOM还在内存里。

1
2
let btn = document.querySelector('button');
document.body.removeChild(btn);

解决办法:

  • 解除变量对于DOM元素的引用

    1
    2
    3
    
    let btn = document.querySelector('button');
    document.body.removeChild(btn);
    btn = null;
    

此时在控制台打btn就会显示null

定时器造成的内存泄漏

1
2
3
4
5
6
7
8
var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放

子元素存在引用引起的内存泄漏

截图

黄色是指直接被 js变量所引用,在内存里,红色是指间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的子元素 refB 由于 parentNode 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除。

参考文献