BooheeFE/weekly

2018/07/15 - JavaScript中的垃圾回收和内存泄漏

lance10030 opened this issue · 1 comments

javaScript 中的垃圾回收和内存泄漏

之前接触的js的内存管理方面的内容一直比较零散,最近在这一块做了一些系统的学习.学习过程中的一些总结在这里分享给大家.欢迎批评指正,共同学习,共同进步.

在一部分语言中是提供了内存管理的接口的,例如C语言中的 molloc()free(); 而在 JavaScript 中会自动进行内存的分配和回收的,因为自动这两个字,就让很多的开发者认为我们是不需要去关心内存方面的问题,当然,这是一种错误的看法.关注内存的管理,避免内存的泄漏也是性能优化重要的一项.

变量的生命周期

Javascript 变量的生命周期要分开来看,对于全局变量,他的生命周期会持续到页面关闭(这就涉及到了后面要总结的内存泄漏的一种方式).而对于局部变量,在所在的函数的代码执行之后,局部变量的生命周期结束,他所占用的内存会通过垃圾回收机制释放(即垃圾回收).

垃圾回收机制

垃圾回收通常有两种方式来实现:

引用计数

这种算法在MDN文档中被称为最"天真"的垃圾回收算法;核心原理是: 判断一个对象是否要被回收就是要看是否还有引用指向它,如果是"零引用",那么就回收.说这种算法天真,是因为它存在着较为严重的缺陷---循环引用:

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

首先要注意我们是在函数作用域中讨论的这个问题,而不是全局环境中.老版本的IE中的非JavaScript原生对象如 DOMBOM 对象就采用的这种策略.下面这种情况下就会出现内存泄漏:

var el =document.getElementById("some_element");
var Obj =new Object();
myObj.el = el;
el.someObject = Obj;

当然我们可以在不用的时候手动释放:

myObj.el = null;
el.someObject = null;

标记清除

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

这个算法假定有一个根(root)的对象;在 Javascript 里,根是全局对象,对应于浏览器环境的 window,node 环境的 global.垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象.

这个算法相对于引用计数的优势在于,“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”.

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

限制: 那些无法从根对象查询到的对象都将被清除
当然,在我们的开发实践中很少遇到这种情况,这也是我们忽略内存管理的原因之一.

常见的内存泄漏举例

1.忘记声明的局部变量

function a(){
    b=2
    console.log('b没有被声明!')
}

b 没被声明,会变成一个全局变量,在页面关闭之前不会被释放.使用严格模式可以避免.

2.闭包带来的内存泄漏

var leaks = (function(){
    var leak = 'xxxxxx';// 闭包中引用,不会被回收
    return function(){
        console.log(leak);
    }
})()

当然有时候我们是故意让这个变量保存在内存中的,但是要避免无意的时候造成的内存泄漏.

3.移除 DOM 节点时候忘记移除暂存的值

有时候出于优化性能的目的,我们会用一个变量暂存 节点,接下来使用的时候就不用再从 DOM 中去获取.但是在移除 DOM 节点的时候却忘记了解除暂存的变量对 DOM 节点的引用,也会造成内存泄漏

var element = {
  image: document.getElementById('image'),
  button: document.getElementById('button')
};

document.body.removeChild(document.getElementById('image'));
// 如果element没有被回收,这里移除了 image 节点也是没用的,image 节点依然留存在内存中.

与此类似情景还有: DOM 节点绑定了事件, 但是在移除的时候没有解除事件绑定,那么仅仅移除 DOM 节点也是没用的

4. 定时器中的内存泄漏

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果没有清除定时器,那么 someResource 就不会被释放,如果刚好它又占用了较大内存,就会引发性能问题. 再提一下 setTimeout ,它计时结束后它的回调里面引用的对象占用的内存是可以被回收的. 当然有些场景 setTimeout 的计时可能很长, 这样的情况下也是需要纳入考虑的.

chrome中查看

老版本的在 Timeline 中查看, 新版本的在 performance 中查看:

image

步骤:

  1. 打开开发者工具 Performance
  2. 勾选 Screenshotsmemory
  3. 左上角小圆点开始录制(record)
  4. 停止录制

图中 Heap 对应的部分就可以看到内存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我们称为min),min在不断上涨,那么肯定是有较为严重的内存泄漏问题.

关于工具的使用暂时在这里浅尝辄止了,后面再深入的学习了开发者工具方方面面的使用再来和大家分享.

参考文档:

  1. MDN文档
  2. 推荐给大家的一个ppt

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

谢谢大佬的文章!又学到新知识了。