JavaScript是如何工作的:內(nèi)存管理 + 如何處理4個常見的內(nèi)存泄漏(譯)

前言:這篇文章的主要內(nèi)容由翻譯而來佑菩,原文鏈接。但是大體內(nèi)容與原文不盡相同忍疾,刪除了一些內(nèi)容闯传,同時新增部分內(nèi)容。由于本文大部分內(nèi)容是翻譯而來膝昆,若有理解不當(dāng)之處還望諒解并指出丸边,我會盡快進(jìn)行修改。(內(nèi)心:如果有什么不對的地方還希望大家指出荚孵,反正我也不會改 妹窖。玩笑話玩笑話 別當(dāng)真!)

概述

在一些語言中收叶,開發(fā)人員需要手動的使用原生語句來顯示的分配和釋放內(nèi)存骄呼。但是在許多高級語言中,這些過程都會被自動的執(zhí)行。在JavaScript中蜓萄,變量(對象隅茎,字符串,等等)創(chuàng)建的時候為其分配內(nèi)存嫉沽,當(dāng)不再被使用的時候會“自動地”釋放這些內(nèi)存辟犀,這個過程被稱為垃圾回收。這個看似“自動的”釋放資源的本質(zhì)是一個混亂的來源绸硕,給JavaScript(和其他高等級語言)開發(fā)者可以不去關(guān)心內(nèi)存管理的錯誤印象堂竟。這是一個很大的錯誤

內(nèi)存泄漏

內(nèi)存泄漏(Memory Leak)是指程序中己動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放玻佩,造成系統(tǒng)內(nèi)存的浪費出嘹,導(dǎo)致程序運行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。

內(nèi)存泄漏缺陷具有隱蔽性咬崔、積累性的特征税稼,比其他內(nèi)存非法訪問錯誤更難檢測。因為內(nèi)存泄漏的產(chǎn)生原因是內(nèi)存塊未被釋放垮斯,屬于遺漏型缺陷而不是過錯型缺陷郎仆。此外,內(nèi)存泄漏通常不會直接產(chǎn)生可觀察的錯誤癥狀兜蠕,而是逐漸積累丸升,降低系統(tǒng)整體性能,極端的情況下可能使系統(tǒng)崩潰牺氨。

內(nèi)存生命周期

無論使用哪一種編程語言,內(nèi)存的生命周期幾乎總是一模一樣的
分配內(nèi)存、使用內(nèi)存墩剖、釋放內(nèi)存猴凹。
在這里我們主要討論內(nèi)存的回收。

引用計數(shù)垃圾回收

這是最簡單的垃圾回收算法岭皂。一個對象在沒有被其他的引用指向的時候就被認(rèn)為“可回收的”郊霎。


對JS引用類型不熟悉的請先百度引用類型,理解了值類型(基本類型)和引用類型之后才能理解下面的代碼

var obj1 = {
  obj2: {
    x: 1
  }
};
//2個對象被創(chuàng)建爷绘。 obj2被obj1引用书劝,并且作為obj1的屬性存在。這里并沒有可以被回收的土至。
//obj1和obj2都指向了{(lán)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在引用片部,現(xiàn)在obj3也沒用在引用了。
// obj1原來的對象就淪為了一只單身狗霜定,于是乎抓狗大隊就來帶走了它档悠。(好吧、其實內(nèi)存就可以被回收了)然爆。
// 然而  obj2對象依然有人愛(被obj4引用)站粟。所以obj2的內(nèi)存就不會被垃圾回收。

obj4 = null;
// obj2內(nèi)心在吶喊:小姐姐不要離開我 QOQ≡瘢現(xiàn)在obj2也沒有被引用了奴烙,引用計數(shù)就是0
也就是可以被回收了。

簡而言之~剖张,如果內(nèi)存有人愛切诀,那就不會被回收服爷。如果是單身狗的話矾端,[手動滑稽]。

循環(huán)引用會造成麻煩

引用計數(shù)在涉及循環(huán)引用的時候有一個缺陷盒刚。在下面的例子中顾犹,創(chuàng)建了2個對象倒庵,并且相互引用,這樣創(chuàng)建了一個循環(huán)炫刷。因此他們實際上是無用的擎宝,可以被釋放。然而引用計數(shù)算法考慮到2個對象中的每一個至少被引用了一次浑玛,因此都不可以被回收绍申。


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

f();

單身狗心里千萬頭草泥馬在奔騰(我特么也會自己牽自己手啊,我也會假裝情侶拍照肮苏谩)
標(biāo)記清除算法

別以為你假裝不是單身狗就拿你沒辦法了极阅,這個算法確定了對象是否可以被達(dá)到。
這個算法包含了以下步驟:

  1. 從‘根’上生成一個列表(通常是以全局變量為根)涨享。在JS中window對象可以作為一個'根'
  2. 所有的'根'都被標(biāo)記為活躍的筋搏,所有的子變量也被遞歸檢查。能夠從'根'上到達(dá)的都不會被認(rèn)為成垃圾厕隧。
  3. 沒有被標(biāo)記為活躍的就被認(rèn)為成垃圾拆又。這些內(nèi)存就會被釋放儒旬。

上圖就是標(biāo)記清除的動作。

在之前的例子中帖族,雖然兩個變量相互引用栈源,但在函數(shù)執(zhí)行完之后,這個兩個變量都沒有被window對象上的任何對象所引用竖般。因此甚垦,他們會被認(rèn)為不可到達(dá)的。

4種常見的JS內(nèi)存泄漏

1:全局變量
JavaScript用一個有趣的方式管理未被聲明的變量:對未聲明的變量的引用在全局對象里創(chuàng)建一個新的變量涣雕。在瀏覽器的情況下艰亮,這個全局對象是window。換句話說:

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

如果bar被假定只在foo函數(shù)的作用域里引用挣郭,但是你忘記了使用var去聲明它迄埃,一個意外的全局變量就被聲明了。
在這個例子里兑障,泄漏一個簡單的字符并不會造成很大的傷害侄非,但是它確實有可能變得更糟。
有時有會通過this來創(chuàng)建意外的全局變量流译。

為了防止這些問題發(fā)生逞怨,可以在你的JaveScript文件開頭使用'use strict';福澡。這個可以使用一種嚴(yán)格的模式解析JavaScript來阻止意外的全局變量叠赦。

如果有時全局變量被用于暫時儲存大量的數(shù)據(jù)或者涉及到的信息,那么在使用完之后應(yīng)該指定為null或者重新分配革砸。

2:被遺忘的定時器或者回調(diào)
還是來個栗子吧除秀,定時器可能會產(chǎn)生對不再需要的DOM節(jié)點或者數(shù)據(jù)的引用。

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

renderer對象在將來有可能被移除算利,讓interval沒有存在的意義鳞仙。然而當(dāng)處理器interval仍然起作用時,renderer并不能被回收(interval在對象被移除時需要被停止),如果interval不能被回收笔时,它的依賴也不可能被回收。這就意味著serverData仗岸,大概保存了大量的數(shù)據(jù)允耿,也不可能被回收。
如今扒怖,大部分的瀏覽器都能而且會在對象變得不可到達(dá)的時候回收觀察處理器较锡,甚至監(jiān)聽器沒有被明確的移除掉。在對象被處理之前盗痒,最好也要顯式地刪除這些觀察者蚂蕴。

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);

如今低散,現(xiàn)在的瀏覽器(包括IE和Edge)使用現(xiàn)代的垃圾回收算法,可以立即發(fā)現(xiàn)并處理這些循環(huán)引用骡楼。換句話說熔号,在一個節(jié)點刪除之前也不是必須要調(diào)用removeEventListener。
框架和插件例如jQuqery在處理節(jié)點(當(dāng)使用具體的api的時候)之前會移除監(jiān)聽器鸟整。這個是插件內(nèi)部的處理可以確保不會產(chǎn)生內(nèi)存泄漏引镊,甚至運行在有問題的瀏覽器上(哈哈哈 說的就是IE6)。

3: 閉包
閉包是javascript開發(fā)的一個關(guān)鍵方面篮条,一個內(nèi)部函數(shù)使用了外部(封閉)函數(shù)的變量弟头。由于JavaScript運行的細(xì)節(jié),它可能以下面的方式造成內(nèi)存泄漏:

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被調(diào)用赴恨,theThing獲得一個包含大量數(shù)據(jù)和新的閉包(someMethod)的對象。同時伴栓,變量unused引用了originalThingtheThing是上一次函數(shù)被調(diào)用時產(chǎn)生的)伦连。已經(jīng)有點困惑了吧?最重要的事情是一旦為同一父域中的作用域產(chǎn)生閉包挣饥,則該作用域是共享的除师。

在這個案例中,someMethodunused共享閉包作用域扔枫,unused引用了originalThing,這阻止了originalThing的回收汛聚,盡管unused不會被使用,但是someMethod依然可以通過theThing來訪問replaceThing作用域外的變量(例如某些全局的)。

4:來自DOM的引用
在你要重復(fù)的操作DOM節(jié)點的時候短荐,存儲DOM節(jié)點是十分有用的倚舀。但是在你需要移除DOM節(jié)點的時候,需要確保移除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的節(jié)點痕貌,但是還保留了一個對該節(jié)點的引用。于是image仍然不能被回收糠排。

當(dāng)涉及到DOM樹內(nèi)部或子節(jié)點時舵稠,需要考慮額外的考慮因素。例如入宦,你在JavaScript中保持對某個表格的特定單元格的引用哺徊。有一天你決定從DOM中移除表格但是保留了對單元格的引用。你也許會認(rèn)為除了單元格其他的都會被回收乾闰。實際并不是這樣的:單元格是表格的一個子節(jié)點落追,子節(jié)點保持了對父節(jié)點的引用。確切的說涯肩,JS代碼中對單元格的引用造成了整個表格被留在內(nèi)存中了轿钠,所以在移除有被引用的節(jié)點時候要移除其子節(jié)點巢钓。

總結(jié)
  1. 小心使用全局變量,盡量不要使用全局變量來存儲大量數(shù)據(jù)疗垛,如果是暫時使用症汹,要在使用完成之后手動指定為null或者重新分配
  2. 如果使用了定時器,在無用的時候要記得清除继谚。如果為DOM節(jié)點綁定了事件監(jiān)聽器烈菌,在移除節(jié)點時要先注銷事件監(jiān)聽器。
  3. 小心閉包的使用花履。如果掌握不好芽世,至少在使用大量數(shù)據(jù)的時候仔細(xì)考量。在使用遞歸的時候也要非常小心(例如用canvas做小游戲)诡壁。
  4. 在移除DOM節(jié)點的時候要確保在代碼中沒有對節(jié)點的引用济瓢,這樣才能完全的移除節(jié)點。在移除父節(jié)點之前要先移除子節(jié)點妹卿。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旺矾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子夺克,更是在濱河造成了極大的恐慌箕宙,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铺纽,死亡現(xiàn)場離奇詭異柬帕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)狡门,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進(jìn)店門陷寝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人其馏,你說我怎么就攤上這事凤跑。” “怎么了叛复?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵仔引,是天一觀的道長。 經(jīng)常有香客問我褐奥,道長咖耘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任抖僵,我火速辦了婚禮,結(jié)果婚禮上缘揪,老公的妹妹穿的比我還像新娘耍群。我一直安慰自己义桂,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布蹈垢。 她就那樣靜靜地躺著慷吊,像睡著了一般。 火紅的嫁衣襯著肌膚如雪曹抬。 梳的紋絲不亂的頭發(fā)上溉瓶,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天,我揣著相機(jī)與錄音谤民,去河邊找鬼堰酿。 笑死,一個胖子當(dāng)著我的面吹牛张足,可吹牛的內(nèi)容都是我干的触创。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼为牍,長吁一口氣:“原來是場噩夢啊……” “哼哼绑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起碉咆,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤抖韩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后疫铜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體茂浮,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年块攒,在試婚紗的時候發(fā)現(xiàn)自己被綠了励稳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡囱井,死狀恐怖驹尼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情庞呕,我是刑警寧澤新翎,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站住练,受9級特大地震影響地啰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜讲逛,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一亏吝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盏混,春花似錦蔚鸥、人聲如沸惜论。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽馆类。三九已至,卻和暖如春弹谁,著一層夾襖步出監(jiān)牢的瞬間乾巧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工预愤, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留沟于,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓鳖粟,卻偏偏與公主長得像社裆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子向图,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容