[聚合文章] 常见的 JavaScript 内存泄露

JavaScript 2017-12-26 18 阅读

这是关于JavaScript内存泄露相关的序列文章中一篇。由于时间有限更新进度会有点慢,但会持续更新的。自己也在学习中,难免对某些知识点的理解不是很正确,所以才将文章放置github上,一是想与大家分享,二是方便持续更新,三是便于实时修正错误点。也希望看本文的各位同学能多提issues,我会根据提的意见不断完善文章。最后希望各位能从文章中有所收获

最新内容请以github上的为准:exclamation:️ https://github.com/zhansingsong/js-leakage-patterns

什么是内存泄露

内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。内存泄漏通常情况下只能由获得程序源代码的程序员才能分析出来。然而,有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏,即使严格意义上来说这是不准确的。————wikipedia

注:下文中标注的CG是Chrome浏览器中Devtools的【Collect garbage】按钮缩写,表示回收垃圾操作。

意外的全局变量

JavaScript对未声明变量的处理方式:在全局对象上创建该变量的引用(即全局对象上的属性,不是变量,因为它能通过delete删除)。如果在浏览器中,全局对象就是window对象。

如果未声明的变量缓存大量的数据,会导致这些数据只有在窗口关闭或重新刷新页面时才能被释放。这样会造成意外的内存泄漏。

function foo(arg) {
    bar = "this is a hidden global variable with a large of data";
}

等同于:

function foo(arg) {
    window.bar = "this is an explicit global variable with a large of data";
}

另外,通过this创建意外的全局变量:

function foo(){
    this.variable = "potential accidental global";
}

// 当在全局作用域中调用foo函数,此时this指向的是全局对象(window),而不是'undefined'
foo();

解决方法:

在JavaScript文件中添加’use strict’,开启严格模式,可以有效地避免上述问题。

function foo(arg){
"use strict" // 在foo函数作用域内开启严格模式
    bar = "this is an explicit global variable with a large of data";// 报错:因为bar还没有被声明
}

如果需要在一个函数中使用全局变量,可以像如下代码所示,在window上明确声明:

function foo(arg) {
    window.bar = "this is a explicit global variable with a large of data";
}

这样不仅可读性高,而且后期维护也方便

谈到全局变量,需要注意那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为null或重新赋值。全局变量也常用来做cache,一般cache都是为了性能优化才用到的,为了性能,最好对cache的大小做个上限限制。因为cache是不能被回收的,越高cache会导致越高的内存消耗。

console.log

console.log:向web开发控制台打印一条消息,常用来在开发时调试分析。有时在开发时,需要打印一些对象信息,但发布时却忘记去掉console.log语句,这可能造成内存泄露。

在传递给console.log的对象是不能被垃圾回收 :recycle:,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log任何对象。

实例——>demos/log.html

<!DOCTYPE html>
<htmllang="en">

<head>
  <metacharset="UTF-8">
  <metaname="viewport"content="width=device-width, initial-scale=1.0">
  <metahttp-equiv="X-UA-Compatible"content="ie=edge">
  <title>Leaker</title>
</head>

<body>
  <inputtype="button"value="click">
  <script>
!function (){
function Leaker(){
this.init();
};
Leaker.prototype = {
init:function (){
this.name = (Array(100000)).join('*');
console.log("Leaking an object %o: %o", (new Date()),this);// this对象不能被回收
},

destroy:function (){
// do something....
}
};
document.querySelector('input').addEventListener('click',function (){
new Leaker();
},false);
}()
</script>
</body>

</html>

这里结合Chrome的Devtools–>Performance做一些分析,操作步骤如下:

:warning:注:最好在隐藏窗口中进行分析工作,避免浏览器插件影响分析结果

  1. 开启【Performance】项的记录
  2. 执行一次CG,创建基准参考线
  3. 连续单击【click】按钮三次,新建三个Leaker对象
  4. 执行一次CG
  5. 停止记录

可以看出【JS Heap】线最后没有降回到基准参考线的位置,显然存在没有被回收的内存。如果将代码修改为:

!function (){
      function Leaker(){
        this.init();
      };
      Leaker.prototype = {
        init: function (){
          this.name = (Array(100000)).join('*');
        },

        destroy: function (){
          // do something....
        }
      };
      document.querySelector('input').addEventListener('click', function (){
        new Leaker();
      }, false);
    }()

去掉console.log(“Leaking an object %o: %o”, (new Date()), this);语句。重复上述的操作步骤,分析结果如下:

<img src=” https://raw.githubusercontent.com/zhansingsong/js-leakage-patterns/master/images/console_2.png “ /》

从对比分析结果可知,console.log打印的对象是不会被垃圾回收器回收的。因此最好不要在页面中console.log任何大对象,这样可能会影响页面的整体性能,特别在生产环境中。除了console.log外,另外还有console.dir、console.error、console.warn等都存在类似的问题,这些细节需要特别的关注。

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。