JavaScript是如何工作的:内存管理 + 如何处理4个常见的内存泄漏(译)

前言:这篇文章的主要内容由翻译而来,原文链接。但是大体内容与原文不尽相同,删除了一些内容,同时新增部分内容。由于本文大部分内容是翻译而来,若有理解不当之处还望谅解并指出,我会尽快进行修改。(内心:如果有什么不对的地方还希望大家指出,反正我也不会改 。玩笑话玩笑话 别当真!)

概述

在一些语言中,开发人员需要手动的使用原生语句来显示的分配和释放内存。但是在许多高级语言中,这些过程都会被自动的执行。在JavaScript中,变量(对象,字符串,等等)创建的时候为其分配内存,当不再被使用的时候会“自动地”释放这些内存,这个过程被称为垃圾回收。这个看似“自动的”释放资源的本质是一个混乱的来源,给JavaScript(和其他高等级语言)开发者可以不去关心内存管理的错误印象。这是一个很大的错误

内存泄漏

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

内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。

内存生命周期

无论使用哪一种编程语言,内存的生命周期几乎总是一模一样的
分配内存、使用内存、释放内存。
在这里我们主要讨论内存的回收。

引用计数垃圾回收

这是最简单的垃圾回收算法。一个对象在没有被其他的引用指向的时候就被认为“可回收的”。


对JS引用类型不熟悉的请先百度引用类型,理解了值类型(基本类型)和引用类型之后才能理解下面的代码

var obj1 = {
  obj2: {
    x: 1
  }
};
//2个对象被创建。 obj2被obj1引用,并且作为obj1的属性存在。这里并没有可以被回收的。
//obj1和obj2都指向了{obj2: {x: 1}}这个对象,这个示例中用`原来的对象`来表示这个对象。

var obj3 = obj1;  //obj3也引用了obj1指向的对象。
obj1 = 1; // obj1不引用原来的对象了。此时原来的对象只有obj3在引用。

var obj4 = obj3.obj2; //obj4引用了obj3对象的obj2属性,
//此时obj2对象有2个引用,一个是作为obj3的一个属性,一个是作为obj4变量。

obj3 = 1;
// 咦,obj1原来对象只有obj3在引用,现在obj3也没用在引用了。
// obj1原来的对象就沦为了一只单身狗,于是乎抓狗大队就来带走了它。(好吧、其实内存就可以被回收了)。
// 然而  obj2对象依然有人爱(被obj4引用)。所以obj2的内存就不会被垃圾回收。

obj4 = null;
// obj2内心在呐喊:小姐姐不要离开我 QOQ。现在obj2也没有被引用了,引用计数就是0
也就是可以被回收了。

简而言之~,如果内存有人爱,那就不会被回收。如果是单身狗的话,[手动滑稽]。

循环引用会造成麻烦

引用计数在涉及循环引用的时候有一个缺陷。在下面的例子中,创建了2个对象,并且相互引用,这样创建了一个循环。因此他们实际上是无用的,可以被释放。然而引用计数算法考虑到2个对象中的每一个至少被引用了一次,因此都不可以被回收。


function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2;
  o2.p = o1;
}

f();

单身狗心里千万头草泥马在奔腾(我特么也会自己牵自己手啊,我也会假装情侣拍照啊)
标记清除算法

别以为你假装不是单身狗就拿你没办法了,这个算法确定了对象是否可以被达到。
这个算法包含了以下步骤:

  1. 从‘根’上生成一个列表(通常是以全局变量为根)。在JS中window对象可以作为一个'根'
  2. 所有的'根'都被标记为活跃的,所有的子变量也被递归检查。能够从'根'上到达的都不会被认为成垃圾。
  3. 没有被标记为活跃的就被认为成垃圾。这些内存就会被释放。

上图就是标记清除的动作。

在之前的例子中,虽然两个变量相互引用,但在函数执行完之后,这个两个变量都没有被window对象上的任何对象所引用。因此,他们会被认为不可到达的。

4种常见的JS内存泄漏

1:全局变量
JavaScript用一个有趣的方式管理未被声明的变量:对未声明的变量的引用在全局对象里创建一个新的变量。在浏览器的情况下,这个全局对象是window。换句话说:

function foo(arg) {
  bar = 'some text';
}
//等同于
function foo(arg) {
  window.bar = 'some text';
}

如果bar被假定只在foo函数的作用域里引用,但是你忘记了使用var去声明它,一个意外的全局变量就被声明了。
在这个例子里,泄漏一个简单的字符并不会造成很大的伤害,但是它确实有可能变得更糟。
有时有会通过this来创建意外的全局变量。

为了防止这些问题发生,可以在你的JaveScript文件开头使用'use strict';。这个可以使用一种严格的模式解析JavaScript来阻止意外的全局变量。

如果有时全局变量被用于暂时储存大量的数据或者涉及到的信息,那么在使用完之后应该指定为null或者重新分配

2:被遗忘的定时器或者回调
还是来个栗子吧,定时器可能会产生对不再需要的DOM节点或者数据的引用。

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每五秒会执行一次

renderer对象在将来有可能被移除,让interval没有存在的意义。然而当处理器interval仍然起作用时,renderer并不能被回收(interval在对象被移除时需要被停止),如果interval不能被回收,它的依赖也不可能被回收。这就意味着serverData,大概保存了大量的数据,也不可能被回收。
如今,大部分的浏览器都能而且会在对象变得不可到达的时候回收观察处理器,甚至监听器没有被明确的移除掉。在对象被处理之前,最好也要显式地删除这些观察者。

var element = document.getElementById('launch-button');
var counter = 0;

function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}

element.addEventListener('click', onClick);
// 做一些其他的事情

element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

如今,现在的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,在一个节点删除之前也不是必须要调用removeEventListener。
框架和插件例如jQuqery在处理节点(当使用具体的api的时候)之前会移除监听器。这个是插件内部的处理可以确保不会产生内存泄漏,甚至运行在有问题的浏览器上(哈哈哈 说的就是IE6)。

3: 闭包
闭包是javascript开发的一个关键方面,一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行的细节,它可能以下面的方式造成内存泄漏:

var theThing = null;

var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) console.log('hi')  //引用了originalThing
  };
  
  theThing = {
    longStr: new Array(1000000).jojin('*'),
    someMethod: function (){
      console.log('message');  
    }
  };
};

setInterval(replaceThing,1000);

这些代码做了一件事情,每次relaceThing被调用,theThing获得一个包含大量数据和新的闭包(someMethod)的对象。同时,变量unused引用了originalThingtheThing是上一次函数被调用时产生的)。已经有点困惑了吧?最重要的事情是一旦为同一父域中的作用域产生闭包,则该作用域是共享的。

在这个案例中,someMethodunused共享闭包作用域,unused引用了originalThing,这阻止了originalThing的回收,尽管unused不会被使用,但是someMethod依然可以通过theThing来访问replaceThing作用域外的变量(例如某些全局的)。

4:来自DOM的引用
在你要重复的操作DOM节点的时候,存储DOM节点是十分有用的。但是在你需要移除DOM节点的时候,需要确保移除DOM tree和代码中储存的引用。

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

//Do some stuff

document.body.removeChild(document.getElementById('image'));
//这个时候  虽然从dom tree中移除了id为image的节点,但是还保留了一个对该节点的引用。于是image仍然不能被回收。

当涉及到DOM树内部或子节点时,需要考虑额外的考虑因素。例如,你在JavaScript中保持对某个表格的特定单元格的引用。有一天你决定从DOM中移除表格但是保留了对单元格的引用。你也许会认为除了单元格其他的都会被回收。实际并不是这样的:单元格是表格的一个子节点,子节点保持了对父节点的引用。确切的说,JS代码中对单元格的引用造成了整个表格被留在内存中了,所以在移除有被引用的节点时候要移除其子节点。

总结
  1. 小心使用全局变量,尽量不要使用全局变量来存储大量数据,如果是暂时使用,要在使用完成之后手动指定为null或者重新分配
  2. 如果使用了定时器,在无用的时候要记得清除。如果为DOM节点绑定了事件监听器,在移除节点时要先注销事件监听器。
  3. 小心闭包的使用。如果掌握不好,至少在使用大量数据的时候仔细考量。在使用递归的时候也要非常小心(例如用canvas做小游戏)。
  4. 在移除DOM节点的时候要确保在代码中没有对节点的引用,这样才能完全的移除节点。在移除父节点之前要先移除子节点。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容