介紹
內存泄露是每個開發(fā)者最終都不得不面對的問題俗冻。即便使用自動內存管理的語言排宰,你還是會碰到一些內存泄漏的情況佣盒。內存泄露會導致一系列問題阿浓,比如:運行緩慢蜂嗽,崩潰择示,高延遲阳准,甚至一些與其他應用相關的問題源请。
什么是內存泄漏
本質上來講榛丢,內存泄露是當一塊內存不再被應用程序使用的時候铲球,由于某種原因,這塊內存沒有返還給操作系統(tǒng)或者空閑內存池的現(xiàn)象晰赞。編程語言使用不同的方式來管理內存稼病。這些方式可能會減少內存泄露的機會。然而掖鱼,某一塊具體的內存是否被使用實際上是一個不可判定問題(undecidable problem)然走。換句話說,只有開發(fā)者可以搞清楚一塊內存是否應該被操作系統(tǒng)回收戏挡。某些編程語言提供了幫助開發(fā)者來處理這件事情的特性芍瑞。而其它的編程語言需要開發(fā)者明確知道內存的使用情況。維基百科上有幾篇寫的不錯的講述手動?和自動內存管理的文章褐墅。
Javascript 的內存管理
Javascript 是那些被稱作垃圾回收語言當中的一員拆檬。垃圾回收語言通過周期性地檢查那些之前被分配出去的內存是否可以從應用的其他部分訪問來幫助開發(fā)者管理內存。換句話說妥凳,垃圾回收語言將內存管理的問題從“什么樣的內存是仍然被使用的竟贯?”簡化成為“什么樣的內存仍然可以從應用程序的其他部分訪問?”猾封。兩者的區(qū)別是細微的澄耍,但是很重要:開發(fā)者只需要知道一塊已分配的內存是否會在將來被使用噪珊,而不可訪問的內存可以通過算法確定并標記以便返還給操作系統(tǒng)晌缘。
非垃圾回收語言通常使用其他的技術來管理內存,包括:顯式內存管理痢站,程序員顯式地告訴編譯器在何時不再需要某塊內存磷箕;引用計數(shù),一個計數(shù)器關聯(lián)著每個內存塊(當計數(shù)器的計數(shù)變?yōu)?的時候阵难,這塊內存就被操作系統(tǒng)回收)岳枷。這些技術都有它們的折中考慮(也就是說都有潛在的內存泄漏風險)。
Javascript 中的內存泄露
引起垃圾收集語言內存泄露的主要原因是不必要的引用。想要理解什么是不必要的引用空繁,首先我們需要理解垃圾收集器是怎樣確定一塊內存能否被訪問的殿衰。
Mark-and-sweep
大多數(shù)的垃圾收集器(簡稱 GC)使用一個叫做 mark-and-sweep 的算法。這個算法由以下的幾個步驟組成:
垃圾收集器建立了一個“根節(jié)點”列表盛泡。根節(jié)點通常是那些引用被保留在代碼中的全局變量闷祥。對于 Javascript 而言,“Window” 對象就是一個能作為根節(jié)點的全局變量例子傲诵。window 對象是一直都存在的(即:不是垃圾)凯砍。所有根節(jié)點都是檢查過的并且被標記為活動的(即:不是垃圾)。所有的子節(jié)點也都被遞歸地檢查過拴竹。每塊可以從根節(jié)點訪問的內存都不會被視為垃圾悟衩。 所有沒有被標記為垃圾的內存現(xiàn)在可以被當做垃圾,而垃圾收集器也可以釋放這些內存并將它們返還給操作系統(tǒng)∷ò荩現(xiàn)代垃圾收集器使用不同的方式來改進這些算法座泳,但是它們都有相同的本質:可以訪問的內存塊被標記為非垃圾而其余的就被視為垃圾。
不必要的引用就是那些程序員知道這塊內存已經沒用了幕与,但是出于某種原因這塊內存依然存在于活躍的根節(jié)點發(fā)出的節(jié)點樹中钳榨。在 Javascript 的環(huán)境中,不必要的引用是某些不再被使用的代碼中的變量纽门。這些變量指向了一塊本來可以被釋放的內存薛耻。一些人認為這是程序員的失誤。
所以想要理解什么是 Javascript 中最常見的內存泄露赏陵,我們需要知道在什么情況下會出現(xiàn)不必要的引用饼齿。
3 種常見的 Javascript 內存泄露
1: 意外的全局變量
Javascript 語言的設計目標之一是開發(fā)一種類似于 Java 但是對初學者十分友好的語言。體現(xiàn) JavaScript 寬容性的一點表現(xiàn)在它處理未聲明變量的方式上:一個未聲明變量的引用會在全局對象中創(chuàng)建一個新的變量蝙搔。在瀏覽器的環(huán)境下缕溉,全局對象就是 window,也就是說:
functionfoo(arg){
bar="this is a hidden global variable";
}
實際上是:
functionfoo(arg){
window.bar="this is an explicit global variable";
}
如果 bar 是一個應該指向 foo 函數(shù)作用域內變量的引用吃型,但是你忘記使用 var 來聲明這個變量证鸥,這時一個全局變量就會被創(chuàng)建出來。在這個例子中勤晚,一個簡單的字符串泄露并不會造成很大的危害枉层,但這無疑是錯誤的。
另外一種偶然創(chuàng)建全局變量的方式如下:
functionfoo(){
this.variable="potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
// 函數(shù)自身發(fā)生了調用赐写,this 指向全局對象(window)鸟蜡,(譯者注:這時候會為全局對象 window 添加一個 variable 屬性)而不是 undefined。
foo();
為了防止這種錯誤的發(fā)生挺邀,可以在你的 JavaScript 文件開頭添加'use strict';語句揉忘。這個語句實際上開啟了解釋 JavaScript 代碼的嚴格模式跳座,這種模式可以避免創(chuàng)建意外的全局變量。
全局變量的注意事項
盡管我們在討論那些隱蔽的全局變量泣矛,但是也有很多代碼被明確的全局變量污染的情況疲眷。按照定義來講,這些都是不會被回收的變量(除非設置 null 或者被重新賦值)您朽。特別需要注意的是那些被用來臨時存儲和處理一些大量的信息的全局變量咪橙。如果你必須使用全局變量來存儲很多的數(shù)據(jù),請確保在使用過后將它設置為 null 或者將它重新賦值虚倒。常見的和全局變量相關的引發(fā)內存消耗增長的原因就是緩存美侦。緩存存儲著可復用的數(shù)據(jù)。為了讓這種做法更高效魂奥,必須為緩存的容量規(guī)定一個上界菠剩。由于緩存不能被及時回收的緣故,緩存無限制地增長會導致很高的內存消耗耻煤。
2: 被遺漏的定時器和回調函數(shù)
在 JavaScript 中 setInterval 的使用十分常見具壮。其他的庫也經常會提供觀察者和其他需要回調的功能。這些庫中的絕大部分都會關注一點哈蝇,就是當它們本身的實例被銷毀之前銷毀所有指向回調的引用棺妓。在 setInterval 這種情況下,一般情況下的代碼是這樣的:
varsomeResource=getData();
setInterval(function(){
varnode=document.getElementById('Node');
if(node){
// Do stuff with node and someResource.
node.innerHTML=JSON.stringify(someResource));
}
},1000);
這個例子說明了搖晃的定時器會發(fā)生什么:引用節(jié)點或者數(shù)據(jù)的定時器已經沒用了炮赦。那些表示節(jié)點的對象在將來可能會被移除掉怜跑,所以將整個代碼塊放在周期處理函數(shù)中并不是必要的。然而吠勘,由于周期函數(shù)一直在運行性芬,處理函數(shù)并不會被回收(只有周期函數(shù)停止運行之后才開始回收內存)。如果周期處理函數(shù)不能被回收剧防,它的依賴程序也同樣無法被回收植锉。這意味著一些資源,也許是一些相當大的數(shù)據(jù)都也無法被回收峭拘。
下面舉一個觀察者的例子俊庇,當它們不再被需要的時候(或者關聯(lián)對象將要失效的時候)顯式地將他們移除是十分重要的。在以前鸡挠,尤其是對于某些瀏覽器(IE6)是一個至關重要的步驟辉饱,因為它們不能很好地管理循環(huán)引用(下面的代碼描述了更多的細節(jié))。現(xiàn)在宵凌,當觀察者對象失效的時候便會被回收鞋囊,即便 listener 沒有被明確地移除,絕大多數(shù)的瀏覽器可以或者將會支持這個特性瞎惫。盡管如此溜腐,在對象被銷毀之前移除觀察者依然是一個好的實踐。示例如下:
varelement=document.getElementById('button');
functiononClick(event){
element.innerHtml='text';
}
element.addEventListener('click',onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
對象觀察者和循環(huán)引用中一些需要注意的點
觀察者和循環(huán)引用常常會讓 JavaScript 開發(fā)者踩坑瓜喇。以前在 IE 瀏覽器的垃圾回收器上會導致一個 bug(或者說是瀏覽器設計上的問題)挺益。舊版本的 IE 瀏覽器不會發(fā)現(xiàn) DOM 節(jié)點和 JavaScript 代碼之間的循環(huán)引用。這是一種觀察者的典型情況乘寒,觀察者通常保留著一個被觀察者的引用(正如上述例子中描述的那樣)望众。換句話說,在 IE 瀏覽器中伞辛,每當一個觀察者被添加到一個節(jié)點上時烂翰,就會發(fā)生一次內存泄漏。這也就是開發(fā)者在節(jié)點或者空的引用被添加到觀察者中之前顯式移除處理方法的原因蚤氏。目前甘耿,現(xiàn)代的瀏覽器(包括 IE 和 Microsoft Edge)都使用了可以發(fā)現(xiàn)這些循環(huán)引用并正確的處理它們的現(xiàn)代化垃圾回收算法。換言之竿滨,嚴格地講佳恬,在廢棄一個節(jié)點之前調用 removeEventListener 不再是必要的操作。
像是 jQuery 這樣的框架和庫(當使用一些特定的 API 時候)都在廢棄一個結點之前移除了 listener 于游。它們在內部就已經處理了這些事情毁葱,并且保證不會產生內存泄露,即便程序運行在那些問題很多的瀏覽器中贰剥,比如老版本的 IE倾剿。
3: DOM 之外的引用
有些情況下將 DOM 結點存儲到數(shù)據(jù)結構中會十分有用。假設你想要快速地更新一個表格中的幾行蚌成,如果你把每一行的引用都存儲在一個字典或者數(shù)組里面會起到很大作用柱告。如果你這么做了,程序中將會保留同一個結點的兩個引用:一個引用存在于 DOM 樹中笑陈,另一個被保留在字典中际度。如果在未來的某個時刻你決定要將這些行移除,則需要將所有的引用清除涵妥。
varelements={
button:document.getElementById('button'),
image:document.getElementById('image'),
text:document.getElementById('text')
};
functiondoStuff(){
image.src='http://some.url/image';
button.click();
console.log(text.innerHTML);
// Much more logic
}
functionremoveButton(){
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
}
標簽)的引用乖菱。在將來你決定將這個表格從 DOM 中移除,但是仍舊保留這個單元格的引用蓬网。憑直覺窒所,你可能會認為 GC 會回收除了這個單元格之外所有的東西,但是實際上這并不會發(fā)生:單元格是表格的一個子節(jié)點且所有子節(jié)點都保留著它們父節(jié)點的引用帆锋。換句話說吵取,JavaScript 代碼中對單元格的引用導致整個表格被保留在內存中。所以當你想要保留 DOM 元素的引用時锯厢,要仔細的考慮清除這一點皮官。
4: 閉包
JavaScript 開發(fā)中一個重要的內容就是閉包脯倒,它是可以獲取父級作用域的匿名函數(shù)。Meteor 的開發(fā)者發(fā)現(xiàn)在一種特殊情況下有可能會以一種很微妙的方式產生內存泄漏捺氢,這取決于 JavaScript 運行時的實現(xiàn)細節(jié)藻丢。
vartheThing=null;
varreplaceThing=function(){
varoriginalThing=theThing;
varunused=function(){
if(originalThing)
console.log("hi");
};
theThing={
longStr:newArray(1000000).join('*'),
someMethod:function(){
console.log(someMessage);
}
};
};
setInterval(replaceThing,1000);
這段代碼做了一件事:每次調用replaceThing時,theThing都會得到新的包含一個大數(shù)組和新的閉包(someMethod)的對象摄乒。同時悠反,沒有用到的那個變量持有一個引用了originalThing(replaceThing調用之前的theThing)閉包。哈馍佑,是不是已經有點暈了斋否?關鍵的問題是每當在同一個父作用域下創(chuàng)建閉包作用域的時候,這個作用域是被共享的拭荤。在這種情況下茵臭,someMethod的閉包作用域和unused的作用域是共享的。unused持有一個originalThing的引用穷劈。盡管unused從來沒有被使用過笼恰,someMethod可以在theThing之外被訪問。而且someMethod和unused共享了閉包作用域歇终,即便unused從來都沒有被使用過社证,它對originalThing的引用還是強制它保持活躍狀態(tài)(阻止它被回收)。當這段代碼重復運行時评凝,將可以觀察到內存消耗穩(wěn)定地上漲追葡,并且不會因為 GC 的存在而下降。本質上來講奕短,創(chuàng)建了一個閉包鏈表(根節(jié)點是theThing形式的變量)宜肉,而且每個閉包作用域都持有一個對大數(shù)組的間接引用,這導致了一個巨大的內存泄露翎碑。
這是一種人為的實現(xiàn)方式谬返。可以想到一個能夠解決這個問題的不同的閉包實現(xiàn)日杈,就像 Metero 的博客里面說的那樣遣铝。
垃圾收集器的直觀行為
盡管垃圾收集器是便利的,但是使用它們也需要有一些利弊權衡莉擒。其中之一就是不確定性酿炸。也就是說,GC 的行為是不可預測的涨冀。通常情況下都不能確定什么時候會發(fā)生垃圾回收填硕。這意味著在一些情形下,程序會使用比實際需要更多的內存鹿鳖。有些的情況下扁眯,在很敏感的應用中可以觀察到明顯的卡頓壮莹。盡管不確定性意味著你無法確定什么時候垃圾回收會發(fā)生,不過絕大多數(shù)的 GC 實現(xiàn)都會在內存分配時遵從通用的垃圾回收過程模式恋拍。如果沒有內存分配發(fā)生垛孔,大部分的 GC 都會保持靜默藕甩∈└遥考慮以下的情形:
大量內存分配發(fā)生時。
大部分(或者全部)的元素都被標記為不可達(假設我們講一個指向無用緩存的引用置 null 的時候)狭莱。
沒有進一步的內存分配發(fā)生僵娃。
這個情形下,GC 將不會運行任何進一步的回收過程腋妙。也就是說默怨,盡管有不可達的引用可以觸發(fā)回收,但是收集器并不要求回收它們骤素。嚴格的說這些不是內存泄露匙睹,但仍然導致高于正常情況的內存空間使用。
Google 在它們的 JavaScript 內存分析文檔中提供一個關于這個行為的優(yōu)秀例子济竹,見示例#2.
Chrome 內存分析工具簡介
Chrome 提供了一套很好的工具用來分析 JavaScript 的內存適用痕檬。這里有兩個與內存相關的重要視圖:timeline 視圖和 profiles 視圖。
Timeline view
timeline 視圖是我們用于發(fā)現(xiàn)不正常內存模式的必要工具送浊。當我們尋找嚴重的內存泄漏時梦谜,內存回收發(fā)生后產生的周期性的不會消減的內存跳躍式增長會被一面紅旗標記。在這個截圖里面我們可以看到袭景,這很像是一個穩(wěn)定的對象內存泄露唁桩。即便最后經歷了一個很大的內存回收,它占用的內存依舊比開始時多得多耸棒。節(jié)點數(shù)也比開始要高荒澡。這些都是代碼中某處 DOM 節(jié)點內存泄露的標志。
Profiles 視圖
你將會花費大部分的時間在觀察這個視圖上与殃。profiles 視圖讓你可以對 JavaScript 代碼運行時的內存進行快照单山,并且可以比較這些內存快照。它還讓你可以記錄一段時間內的內存分配情況奈籽。在每一個結果視圖中都可以展示不同類型的列表饥侵,但是對我們的任務最有用的是 summary 列表和 comparison 列表。
summary 視圖提供了不同類型的分配對象以及它們的合計大幸缕痢:shallow size (一個特定類型的所有對象的總和)和 retained size (shallow size 加上保留此對象的其它對象的大凶婧堋)。distance 顯示了對象到達 GC 根(校者注:最初引用的那塊內存膳殷,具體內容可自行搜索該術語)的最短距離。
comparison 視圖提供了同樣的信息但是允許對比不同的快照一睁。這對于找到泄露很有幫助。
舉例: 使用 Chrome 來發(fā)現(xiàn)內存泄露
有兩個重要類型的內存泄露:引起內存周期性增長的泄露和只發(fā)生一次且不引起更進一步內存增長的泄露佃却。顯而易見的是者吁,尋找周期性的內存泄漏是更簡單的。這些也是最麻煩的事情:如果內存會按時增長饲帅,泄露最終將導致瀏覽器變慢或者停止執(zhí)行腳本复凳。很明顯的非周期性大量內存泄露可以很容易的在其他內存分配中被發(fā)現(xiàn)。但是實際情況并不如此灶泵,往往這些泄露都是不足以引起注意的育八。這種情況下,小的非周期性內存泄露可以被當做一個優(yōu)化點赦邻。然而那些周期性的內存泄露應該被視為 bug 并且必須被修復髓棋。
為了舉例,我們將會使用 Chrome 的文檔中提供的一個例子惶洲。完整的代碼在下面可以找到:
varx=[];
functioncreateSomeNodes(){
vardiv,
i=100,
frag=document.createDocumentFragment();
for(;i>0;i--){
div=document.createElement("div");
div.appendChild(document.createTextNode(i+" - "+newDate().toTimeString()));
frag.appendChild(div);
}
document.getElementById("nodes").appendChild(frag);
}
functiongrow(){
x.push(newArray(1000000).join('x'));
createSomeNodes();
setTimeout(grow,1000);
}
當調用 grow 的時候按声,它會開始創(chuàng)建 div 節(jié)點并且把他們追加到 DOM 上。它將會分配一個大數(shù)組并將它追加到一個全局數(shù)組中恬吕。這將會導致內存的穩(wěn)定增長签则,使用上面提到的工具可以觀察到這一點。
垃圾收集語言通常表現(xiàn)出內存用量的抖動币呵。如果代碼在一個發(fā)生分配的循環(huán)中運行時怀愧,這是很常見的。我們將要尋找那些在內存分配之后周期性且不會回落的內存增長余赢。
查看內存是否周期性增長
對于這個問題芯义,timeline 視圖最合適不過了。在 Chrome 中運行這個例子妻柒,打開開發(fā)者工具扛拨,定位到 timeline,選擇內存并且點擊記錄按鈕举塔。然后去到那個頁面點擊按鈕開始內存泄露绑警。一段時間后停止記錄,然后觀察結果:
這個例子中每秒都會發(fā)生一次內存泄露央渣。記錄停止后计盒,在 grow 函數(shù)中設置一個斷點來防止 Chrome 強制關閉這個頁面。
在圖中有兩個明顯的標志表明我們正在泄漏內存芽丹。節(jié)點的圖表(綠色的線)和 JS 堆內存(藍色的線)北启。節(jié)點數(shù)穩(wěn)定地增長并且從不減少。這是一個明顯的警告標志。
JS 堆內存表現(xiàn)出穩(wěn)定的內存用量增長咕村。由于垃圾回收器的作用场钉,這很難被發(fā)現(xiàn)。你能看到一個初始內存的增長的圖線懈涛,緊接著有一個很大的回落逛万,接著又有一段增長然后出現(xiàn)了一個峰值,接著又是一個回落批钠。這個情況的關鍵是在于一個事實宇植,即每次內存用量回落時候,堆內存總是比上一次回落后的內存占用量更多价匠。也就是說当纱,盡管垃圾收集器成功地回收了很多的內存呛每,還是有一部分內存周期性的泄露了踩窖。
我們現(xiàn)在確定程序中有一個泄露,讓我們一起找到它晨横。
拍兩張快照
為了找到這個內存泄漏洋腮,我們將使用 Chrome 開發(fā)者工具紅的 profiles 選項卡。為了保證內存的使用在一個可控制的范圍內手形,在做這一步之前刷新一下頁面啥供。我們將使用 Take Heap Snapshot 功能。
刷新頁面库糠,在頁面加載結束后為堆內存捕獲一個快照伙狐。我們將要使用這個快照作為我們的基準。然后再次點擊按鈕瞬欧,等幾秒贷屎,然后再拍一個快照。拍完照后艘虎,推薦的做法是在腳本中設置一個斷點來停止它的運行唉侄,防止更多的內存泄露。
有兩個方法來查看兩個快照之間的內存分配情況野建,其中一種方法需要選擇 Summary 然后在右面選取在快照1和快照2之間分配的對象属划,另一種方法,選擇 Comparison 而不是 Summary候生。兩種方法下同眯,我們都將會看到一個列表,列表中展示了在兩個快照之間分配的對象唯鸭。
本例中须蜗,我們很容易就可以找到內存泄露:它們很明顯。看一下(string)構造函數(shù)的 Size Delta唠粥。58個對象占用了8 MB 內存疏魏。這看起來很可疑:新的對象被創(chuàng)建,但是沒有被釋放導致了8 MB 的內存消耗晤愧。
如果我們打開(string)構造函數(shù)分配列表大莫,我們會注意到在很多小內存分配中摻雜著的幾個大量的內存分配。這些情況立即引起了我們的注意官份。如果我們選擇它們當中的任意一個只厘,我們將會在下面的 retainer 選項卡中得到一些有趣的結果。
我們發(fā)現(xiàn)我們選中的內存分配信息是一個數(shù)組的一部分舅巷。相應地羔味,數(shù)組被變量 x 在全局 window 對象內部引用。這給我們指引了一條從我們的大對象到不會被回收的根節(jié)點(window)的完整的路徑钠右。我們也就找到了潛在的泄漏點以及它在哪里被引用赋元。
到現(xiàn)在為止,一切都很不錯飒房。但是我們的例子太簡單了:像例子中這樣大的內存分配并不是很常見搁凸。幸運的是我們的例子中還存在著細小的 DOM 節(jié)點內存泄漏。使用上面的內存快照可以很容易地找到這些節(jié)點狠毯,但是在更大的站點中护糖,事情變得復雜起來。最近嚼松,新的 Chrome 的版本中提供了一個附加的工具嫡良,這個工具十分適合我們的工作,這就是堆內存分配記錄(Record Heap Allocations)功能
通過記錄堆內存分配來發(fā)現(xiàn)內存泄露
取消掉你之前設置的斷點讓腳本繼續(xù)運行献酗,然后回到開發(fā)者工具的 Profiles 選項卡∏奘埽現(xiàn)在點擊 Record Heap Allocations。當工具運行時候你將注意到圖表頂部的藍色細線凌摄。這些代表著內存分配羡蛾。我們的代碼導致每秒鐘都有一個大的內存分配發(fā)生。讓它運行幾秒然后讓程序停止(不要忘記在此設置斷點來防止 Chrome 吃掉過多的內存)锨亏。
在這張圖中你能看到這個工具的殺手锏:選擇時間線中的一片來觀察在這段時間片中內存分配發(fā)生在什么地方痴怨。我們將時間片設置的盡量與藍色線接近。只有三個構造函數(shù)在這個列表中顯示出來:一個是與我們的大泄露有關的(string)器予,一個是和 DOM 節(jié)點的內存分配相關的浪藻,另一個是 Text 構造函數(shù)(DOM 節(jié)點中的文本構造函數(shù))。
從列表中選擇一個 HTMLDivElement 構造函數(shù)然后選擇一個內存分配堆棧乾翔。
啊哈爱葵!我們現(xiàn)在知道那些元素在什么地方被分配了(grow -> createSomeNodes)施戴。如果我們集中精神觀察圖像中的每個藍色線,還會注意到 HTMLDivElement 的構造函數(shù)被調用了很多次萌丈。如果我們回到快照 comparison 視圖就不難發(fā)現(xiàn)這個構造函數(shù)分配了很多次內存但是沒有從未釋放它們赞哗。也就是說,它不斷地分配內存空間辆雾,但卻沒有允許 GC 回收它們肪笋。種種跡象表明這是一個泄露,加上我們確切地知道這些對象被分配到了什么地方(createSomeNodes 函數(shù))《扔兀現(xiàn)在應該去研究代碼藤乙,并修復這個泄漏。
其他有用的特性
在堆內存分配結果視圖中我們可以使用比 Summary 更好的 Allocation 視圖惭墓。
這個視圖為我們呈現(xiàn)了一個函數(shù)的列表坛梁,同時也顯示了與它們相關的內存分配情況。我們能立即看到 grow 和 createSomeNodes 凸顯了出來腊凶。當選擇 grow 我們看到了與它相關的對象構造函數(shù)被調用的情況划咐。我們注意到了(string),HTMLDivElement 和 Text 而現(xiàn)在我們已經知道是對象的構造函數(shù)被泄露了吭狡。
這些工具的組合對找到泄漏有很大幫助尖殃。和它們一起工作。為你的生產環(huán)境站點做不同的分析(最好用沒有最小化或混淆的代碼)划煮。看看你能不能找到那些比正常情況消耗更多內存的對象吧(提示:這些很難被找到)缔俄。
如果要使用 Allocation 視圖弛秋,需要進入 Dev Tools -> Settings,選中“record heap allocation stack traces”俐载。獲取記錄之前必須要這么做蟹略。
結論
在垃圾回收語言中,如 JavaScript遏佣,確實會發(fā)生內存泄露挖炬。一些情況下我們都不會意識到這些泄露,最終它們將會帶來毀滅性的災難状婶。正是由于這個原因意敛,使用內存分析工具來發(fā)現(xiàn)內存泄露是十分重要的。運行分析工具應該成為開發(fā)周期中的一部分膛虫,特別是對于中型或大型應用來講〔菀觯現(xiàn)在就開始這么做,盡可能地為你的用戶提供最好的體驗稍刀。動手吧撩独!
(非原創(chuàng))