原文出處: Chrome DevTools 譯文出處:DestinyXie javascript
內存泄漏是指計算機可用內存的逐漸減小。當程序持續沒法釋放其使用的臨時內存時就會發生。JavaScript的web應用也會常常遇到在原生應用程序中出現的內存相關的問題,如泄漏和溢出,web應用也須要應對垃圾回收停頓。html
整體來講,當你以爲你遇到了內存泄漏問題時,你須要思考三個問題:html5
本小節介紹在內存分析時使用的經常使用術語,這些術語在爲其它語言作內存分析的工具中也適用。這裏的術語和概念用在了堆分析儀(Heap Profiler)UI工具和相關的文檔中。java
這些可以幫助咱們熟悉如何有效的使用內存分析工具。若是你曾用過像Java、.NET等語言的內存分析工具的話,那麼這將是一個複習。node
把內存想象成一個包含基本類型(像數字和字符串)和對象(關聯數組)的圖表。它可能看起來像下面這幅一系列相關聯的點組成的圖。git
一個對象有兩種使用內存的方法:github
當你使用DevTools中的堆分析儀(Heap Profiler,用來分析內存問題的工具,在DevTools的」Profile」標籤下)時,你可能會驚喜的發現一些顯示各類信息的欄目。其中有兩項是:直接佔用內存(Shallow Size)和佔用總內存(Retained Size),那它們是什麼意思呢?web
這個是對象自己佔用的內存。正則表達式
典型的JavaScript對象都會有保留內存用來描述這個對象和存儲它的直接值。通常,只有數組和字符串會有明顯的直接佔用內存(Shallow Size)。但字符串和數組經常會在渲染器內存中存儲主要數據部分,僅僅在JavaScript對象棧中暴露一個很小的包裝對象。chrome
渲染器內存指你分析的頁面在渲染的過程當中所用到的全部內存:頁面自己的內存 + 頁面中的JS堆用到的內存 + 頁面觸發的相關工做進程(workers)中的JS堆用到的內存。然而,經過阻止垃圾自動回收別的對象,一個小對象都有可能間接佔用大量的內存。
一個對象一但刪除後它引用的依賴對象就不能被GC根(GC root)引用到,它們所佔用的內存就會被釋放,一個對象佔用總內存包括這些依賴對象所佔用的內存。
GC根是由控制器(handles)組成的,這些控制器(不管是局部仍是全局)是在創建由build-in函數(native code)到V8引擎以外的JavaScript對象的引用時建立的。全部這些控制器都可以在堆快照的GC roots(GC根) > Handle scope 和 GC roots >Global handlers中找到。若是不深刻了解瀏覽器的實現原理,在這篇文章中介紹這些控制器可能會讓人不能理解。GC根和控制器你都不須要過多關心。
有不少內部的GC根對用戶來講都是不重要的。從應用的角度來講有下面幾種狀況:
注意:咱們推薦用戶在建立堆快照時,不要在console中執行代碼,也不要啓用調試斷點。
內存圖由一個根部開始,多是瀏覽器的window
對象或Node.js模塊Global
對象。這些對象如何被內存回收不受用戶的控制。
不能被GC根遍歷到的對象都將被內存回收。
注意:直接佔用內存和佔用總內存字段中的數據是用字節表示的。
以前咱們已經瞭解到,堆是由各類互相關聯的對象組成的網狀結構。在數字領域,這種結構被稱爲圖或內存圖。圖是由邊緣(edges)鏈接着的節點(nodes)組成的,他們都被貼了標籤。
本文檔的後面你將瞭解到如何使用堆分析儀生成快照。從下圖的堆分析儀生成的快照中,咱們能看到距離(distance)這個字段:是指對象到GC根的距離。若是同一個類型的全部對象的距離都同樣,而有一小部分的距離卻比較大,那麼就可能出了些你須要進行調查的問題了。
支配對象就像一個樹結構,由於每一個對象都有一個支配者。一個對象的支配者可能不會直接引用它支配的對象,就是說,支配對象樹結構不是圖中的生成樹。
在上圖中:
在下圖的例子中,節點#3
是#10
的支配者,但#7
也在每一個從GC到#10
的路經中都出現了。像這樣,若是B對象在每一個從根節點到A對象的路經中都出現,那麼B對象就是A對象的支配對象。
在本節,咱們將描述一些內存相關的概念,這些概念是和V8 JavaScript虛擬機(V8 VM 或VM)有關的。當分析內存時,瞭解這些概念對理解堆快照是有幫助的。
有三個原始類型:
它們不會引用別的值,它們只會是葉子節點或終止節點。
數字(Numbers)如下面兩種方式之一被存儲:
字符型數據會如下面兩種方式存儲:
新建立的JavaScript對象會被在JavaScript堆上(或VM堆)分配內存。這些對象由V8的垃圾回收器管理,只要還有一個強引用他們就會在內存中保留。
本地對象是全部不在JavaScript堆中的對象,與堆對象不一樣的是,在它們的生命週期中,不會被V8垃圾加收器處理,只能經過JavaScript包裝對象引用。
鏈接字符串是由一對字符串合併成的對象,是合併後的結果。鏈接字符串只在有須要時合併。像一鏈接字符串的子字符串須要被構建時。
好比:若是你鏈接a和b,你獲得字符串(a, b)這用來表示鏈接的結果。若是你以後要再把這個結果與d鏈接,你就獲得了另外一個鏈接字符串((a, b), d)。
數組(Arrays) – 數組是數字類型鍵的對象。它們在V8引擎中存儲大數據量的數據時被普遍的使用。像字典這種有鍵-值對的對象就是用數組實現的。
一個典型的JavaScript對象能夠經過兩種數組類型之一的方式來存儲:
若是隻有少許的屬性,它們會被直接存儲在JavaScript對象自己中。
Map – 一種用來描述對象類型和它的結構的對象。好比,maps會被用來描述對象的結構以實現對對象屬性的快速訪問
每一個本地對象組都是由一組之間相互關聯的對象組成的。好比一個DOM子樹,每一個節點都能訪問到它的父元素,下一個子元素和下一個兄弟元素,它們構成了一個關聯圖。須要注意的是本地元素沒有在JavaScript堆中表現-這就是它們的大小是零的緣由,而它的包裝對象被建立了。
每一個包裝對象都會有一個到本地對象的引用,用來傳遞對這些本地對象的操做。這些本地對象也有到包裝對象的引用。但這並不會創造沒法收回的循環,GC是足夠智能的,可以分辨出那些已經沒有引用包裝對象的本地對象並釋放它們的。但若是有一個包裝對象沒有被釋放那它將會保留全部對象組和相關的包裝對象。
注意: 當使用Chrome作內存分析時,最好設置一個潔淨的測試環境
打開Chrome的內存管理器,觀察內存字段,在一個頁面上作相關的操做,你能夠很快定位這個操做是否會致使頁面佔用不少內存。你能夠從Chrome菜單 > 工具或按Shift + Esc,找到內存管理器。
打開後,在標頭右擊選用 JavasScript使用的內存 這項。
解決問題的第一步就是要可以證實問題存在。這就須要建立一個可重現的測試來作爲問題的基準度量。沒有可再現的程序,就不能可靠的度量問題。換句話說若是沒有基準來作爲對比,就沒法知道是哪些改變使問題出現的。
時間軸面版(Timeline panel)對於發現程序何時出了問題很用幫助。它展現了你的web應用或網站加載和交互的時刻。全部的事件:從加載資源到解JavaScript,樣式計算,垃圾回收停頓和頁面重繪。都在時間軸上表示出來了。
當分析內存問題時,時間軸面版上的內存視圖(Memory view)能用來觀察:
更多的關於在內存分析時,定位內存泄漏的方法,請閱Zack Grossbart的Memory profiling with the Chrome DevTools
首先要作的事情是找出你認爲可能致使內存泄漏的一些動做。能夠是發生在頁面上的任何事件,鼠標移入,點擊,或其它可能會致使頁面性能降低的交互。
在時間軸面版上開始記錄(Ctrl+E 或 Cmd+E)而後作你想要測試的動做。想要強制進行垃圾回收點面版上的垃圾筒圖標()。
下面是一個內存泄漏的例子,有些點沒有被垃圾回收:
若是通過一些反覆測試後,你看到的是鋸齒狀的圖形(在內存面版的上方),說明你的程序中有不少短時存在的對象。而若是一系列的動做沒有讓內存保持在必定的範圍,而且DOM節點數沒有返回到開始時的數目,你就能夠懷疑有內存泄漏了。
一旦肯定了存在內存上的問題,你就可使用分析面板(Profiles panel)上的堆分析儀(heap profiler)來定位問題的來源。
例子: 嘗試一下memory growth的例子,能幫助你有效的練習經過時間軸分析內存問題。
內存回收器(像V8中的)須要可以定位哪些對象是活的(live),而那些被認爲是死的(垃圾)的對象是沒法引用到的(unreachable)。
若是垃圾回收 (GC)由於JavaScript執行時有邏輯錯誤而沒有可以回收到垃圾對象,這些垃圾對象就沒法再被從新回收了。像這樣的狀況最終會讓你的應用愈來愈慢。
好比你在寫代碼時,有的變量和事件監聽器已經用不到了,可是卻仍然被有些代碼引用。只要引用還存在,那被引用的對象就沒法被GC正確的回收。
當你的應用程序在運行中,有些DOM對象可能已經更新/移除了,要記住檢查引用了DOM對象的變量並將其設null。檢查可能會引用到其它對象(或其它DOM元素)的對象屬性。雙眼要盯着可能會愈來愈增加的變量緩存。
在Profiles面板中,選擇Take Heap Snapshot,而後點擊Start或者按Cmd + E或者Ctrl + E:
快照最初是保存在渲染器進程內存中的。它們被按需導入到了DevTools中,當你點擊快照按鈕後就能夠看到它們了。當快照被載入DevTools中顯示後,快照標題下面的數字顯示了可以被引用到的(reachable)JavaScript對象佔有內存總數。
例子:嘗試一下garbage collection in action的例子,在時間軸(Timeline)面板中監控內存的使用。
注意:關閉DevTools窗口並不能從渲染內存中刪除掉收集的快照。當從新打開DevTools後,以前的快照列表還在。
記住咱們以前提到的,當你生成快照時你能夠強制執行在DevTools中GC。當咱們拍快照時,GC是自動執行的。在時間軸(Timeline)中點擊垃圾桶(垃圾回收)按鈕()就能夠輕鬆的執行垃圾回收了。
例子:嘗試一下scattered objects並用堆分析儀(Heap Profiler)分析它。你能夠看到(對象)項目的集合。
一個快照能夠根據不一樣的任務切換視圖。能夠經過如圖的選擇框切換:
下面是三個默認視圖:
Dominators(支配者)視圖能夠在Settings面板中開啓 – 顯示dominators tree. 能夠用來找到內存增加點。
對象的屬性和屬性值有不一樣的類型並自動的經過顏麼進行了區分。每一個屬性都是如下四種之一:
命名爲System
的對象沒有對應的JavaScript類型。它們是JavaScript VM對象系統內置的。V8將大多數內置對象和用戶JS對象放在同一個堆中。但它們只是V8的內部對象。
打開一個快照,默認是以概要視圖顯示的,顯示了對象總數,能夠展開顯示具體內容: Initially, a snapshot opens in the Summary view, displaying object totals, which can be expanded to show instances:
第一層級是」整體」行,它們顯示了:
展開一個整體行後,會顯示全部的對象實例。沒一個實例的直接佔用內存和佔用總內存都被相應顯示。@符號後的數字不對象的惟一ID,有了它你就能夠逐個對象的在不一樣快照間做對比。
例子:嘗試這個例子(在新tab標籤中打開)來了解如何使用概要視圖。
記住黃色的對象被JavaScript引用,而紅色的對象是由黃色背景色引用被分離了的節點。
該視圖用來對照不一樣的快照來找到快照之間的差別,來發現有內存泄漏的對象。來證實對應用的某個操做沒有形成泄漏(好比:通常一對操做和撤消的動做,像找開一個document,而後關閉,這樣是不會形成泄漏的),你能夠按如下的步驟嘗試:
在對照視圖下,兩個快照之間的不一樣就會展示出來了。當展開一個總類目後,增長和刪除了的對象就顯示出來了:
例子:嘗試例子(在新tab標籤中打開)來了解如何使用對照視圖來定位內存泄漏。
控制視圖能夠稱做對你的應用的對象結構的」鳥瞰視圖(bird’s eys view)」。它能讓你查看function內部,跟你的JavaScript對象同樣的觀察VM內部對象,能讓你在你的應用的很是低層的內存使用狀況。
該視圖提供了幾個進入點:
下圖是一個典型的控制視圖:
例子:嘗試例子(在新tab標籤中打開)來了解如何使用控制視圖來查看閉包內部和事件處理。
關於閉包的建議
給函數命名對你在快照中的閉包函數間做出區分會很用幫助。如:下面的例子中沒有給函數命名:
1
2
3
4
5
6
7
8
9
|
function createLargeClosure() {
var largeStr = new Array(1000000).join('x');
var lC = function() { // this is NOT a named function
return largeStr;
};
return lC;
}
|
而下面這個有給函數命名:
1
2
3
4
5
6
7
8
9
|
function createLargeClosure() {
var largeStr = new Array(1000000).join('x');
var lC = function lC() { // this IS a named function
return largeStr;
};
return lC;
}
|
例子:嘗試這個例子why eval is evil來分析內存中閉包的影響。你可能也對嘗試下面這個例子,記錄heap allocations(堆分配)有興趣。
這個工具獨一無二的一點是展現了瀏覽器原生對象(DOM節點,CSS規則)和JavaScript對象之間的雙向引用。這能幫助你發現由於忘記解除引用遊離的DOM子節點而致使的難以發覺的內存泄漏。
DOM內存泄漏可能會超出你的想象。看下下面的例子 – #tree對象何時被GC呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var select = document.querySelector;
var treeRef = select("#tree");
var leafRef = select("#leaf");
var body = select("body");
body.removeChild(treeRef);
//#tree can't be GC yet due to treeRef
treeRef = null;
//#tree can't be GC yet due to indirect
//reference from leafRef
leafRef = null;
//#NOW can be #tree GC
|
#leaf
表明了對它的父節點的引用(parentNode)它遞歸引用到了#tree
,因此,只有當leafRef被nullified後#tree
表明的整個樹結構纔會被GC回收。
例子:嘗試leaking DOM nodes來了解哪裏DOM節點會內存泄漏並如何定位。你也能夠看一下這個例子:DOM leaks being bigger than expected。
查看Gonzalo Ruiz de Villa的文章Finding and debugging memory leaks with the Chrome DevTools來閱讀更多關於DOM內存泄漏和內存分析的基礎。
原生對象在Summary和Containment視呼中更容易找到 – 有它們專門的類目:
例子:嘗試下這個例子(在新tab標籤中打開)來了解如何將DOM樹分離。
支配者視圖顯示了堆圖的支配者樹。支配者視圖跟控制(Containment)視圖很像,可是沒有屬性名。這是由於支配者可能會是一個沒有直接引用的對象,就是說這個支配者樹不是堆圖的生成樹。但這是個有用的視圖能幫助咱們很快的定位內存增加點。
注意:在Chrome Canary中,支配者視圖可以在DevTools中的Settings > Show advanced heap snapshot properties 開啓,重啓DevTools生效。
例子:嘗試這個例子(在新tab標籤中打開)來練習如何找到內存增加點。能夠進一步嘗試下一個例子retaining paths and dominators。
對象跟蹤器整合了heap profiler的快照增量更新分析和Timeline面板的記錄。跟其它工具同樣,記錄對象的堆配置須要啓動記錄,執行一系列操做,而後中止記錄而後進行分析。
對象跟蹤器不間斷的記錄堆快照(頻率達到了每50毫秒!),結束時記錄最後一個快照。該堆分配分析器顯示對象在哪被建立並定位它的保留路徑。
開啓並使用對象分析器
開始使用對象分析器: 1. 確認你使用的是最新版的Chrome Canary。
上面的柱條表示在堆中生成的新對象。高度就對應了相應對象的大小,它的顏色表示了這個對象是否在最後拍的那個快照中還在:藍色柱表示在timeline最後這個對象還在,灰色柱表示這個對象在timeline中生成,但結束前已經被內存回收了。
上面的例子中,一個動做執行了10次。同一個程序保留了5個對象,因此最後5個藍色柱條被保留了。但這最後留下的柱存在潛在的問題。你能夠用timeline上的滑動條縮小到那個特定的快照並找到這個分配的對象。
點擊一個堆中的對象就能在堆快照的下面部分顯示它的保留總內存樹。檢查這個對象的保留總內存樹可以給你足夠的信息來了解爲何這個對象沒有被回收,而後你就能對代碼作相應的修改來去掉沒必要要的引用。
問:我不能看到對象的全部屬性,我也看到它們的非字符串值!爲何?
並不是全部屬性都完整的保存在JavaScript堆中。其中有些是經過執行原生代碼的getters方法來獲取的。這些屬性沒有在堆快照中捕獲,是爲了防止對getters方法的調用和避免程序狀態的改變,若是這些getters方法不是」純(pure)」的functions。一樣,非字符串的值,如數字,沒有被捕獲是爲了減小快照的大小。
問:@符號後面的數字是什麼意思 – 是地址仍是ID呢?這個ID值真的是惟一的麼?
這是對象ID。顯示對象的地址沒有意義,由於一個對象會在垃圾回收的時候被移除。這些對象IDs是真正的IDs – 就是說,它們在不一樣的快照間是惟一表示的。這樣就能夠的堆狀態間進行精確的對比。維持這些IDs會給GC流程增長額外的開支,但這僅在記錄第一次堆快照時分配 – 若是堆分析儀沒有用到,就不會有額外的開支。
問:」死」(沒法引用到的)對象被包含在快照中了麼?
沒有,只有能夠引用到的對象纔會顯示在快照中。並且,拍快照前都會先自動執行GC操做。
注意:在寫這篇文章的時候,咱們計劃在拍快照的時候再也不GC,防止堆尺寸的減小。如今已是這樣了,但垃圾對象依然顯示在快照以外。
問:GC根是由什麼組成的?
由不少部分組成:
問:我得知可使用Heap Profiler和Timeline Memory view來檢測內存泄漏。但我應該先用哪一個工具呢?
Timeline面版,是在你第一次使用你的頁面發現速度變慢了時用來論斷過多的內存使用。網站變慢是比較典型的內存泄漏的信號,但也多是其它的緣由 – 多是有渲染或網絡傳輸方面的瓶頸,因此要確保解決你網頁的真正問題。
論斷是不是內存問題,就打開Timeline面板和Memory標籤。點擊record按鈕,而後在你的應用上重複幾回你認爲可能致使內存泄漏的操做。中止記錄。你應用的內存使用圖就生成出來了。若是內存的使用一直在增加(而沒有相應的降低),這就代表你的應用可能有內存泄漏了。
通常一個正常的應用的內存使用圖形是鋸齒狀的,由於內存使用後又會被垃圾回收器回收。不用擔憂這種鋸齒形 – 由於老是會由於JavaScript而有內存的消耗,甚至一個空的requestAnimationFrame
也會形成這種鋸齒形,這是沒法避免的。只要不是那種分配了持續不少內存的形狀,那就代表生成了不少內存垃圾。
上圖的增加線是須要你警戒的。在診斷分析的時候Memory標籤中的DOM node counter,Document counter和Event listener count也是頗有用的。DOM節點數是使用的原生內存不會影響JavaScript內存圖。
一旦你確認你的應用有內存泄漏,堆分析儀就能夠用來找到內存泄漏的地方。
問:我發現堆快照中有的DOM節點的數字是用紅色標記爲」Detached DOM tree」,而其它的是黃色的,這是什麼意思呢?
你會發現有不一樣的顏色。紅色的節點(有着深色的背景)沒有從JavaScript到它們的直接的引用,但它們是分離出來的DOM結構的一部分,因此他們仍是在內存中保留了。有可能有一個節點被JavaScript引用到了(多是在閉包中或者一個變量),這個引用會阻止整個DOM樹被內存回收。
黃色節點(黃色背景)有JavaScript的直接引用。在同一個分離的DOM樹中查看一個黃色的節點來定位你的JavaScript的引用。就可能看到從DOM window到那個節點的屬性引用鏈(如:window.foo.bar[2].baz
)。
下面的動態圖顯示了分離節點的處理過程:
例子:嘗試這個例子detached nodes你能夠查看節點在Timeline中的生命週期,而後拍堆快照來找到分離的節點。
問:直接佔用內存(Shallow Size)和佔用總內存(Retained Size)分別表明什麼,它們的區別是什麼?
是這樣的,對象能夠在內存中以兩種方式存在(be alive) – 直接的被別一個可訪問的(alive)對象保留(window和document對象老是可訪問的)或被原生對象(象DOM對象)隱含的包留引用。後一種方式會由於阻止對象被GC自動回收,而有導制內存泄泥漏的可能。對象自身佔用的內存被稱爲直接佔用內存(一般來講,數組和字符串會保留更多的直接佔用內存(shallow size))。
一個任意大小的對象能夠經過阻止其它對象內存被回收在保留很大的內存使用。當一個對象被刪除後(它形成的一些依賴就沒法被引用了)可以釋放的內存的大小被稱有佔用總內存(retained size)。
問:constructor和retained字段下有不少的數據。我應該從哪開始調查我是的否遇到了內存泄漏呢?
通常來講最好是從經過retainers排序的第一個對象開始,retainers之間是經過距離排序的(是指到window對象的距離)。
距離最短的對象有多是首選的可能致使內存泄漏的對象。
問:Summary, Comparison, Dominators 和 Containment這些視圖之間的不一樣是什麼?
你能夠經過切換視圖來體驗它們的區別。
問:堆分析儀中的constructor(一組)內容表明什麼?
在你的程序的生命週期中生成的不少其它的對象,包括事件監聽器或自定義對象,能夠在下面的controllers中找到:
問:我在作內存分析時須要關閉Chrome裏可能會產生影響的什麼功能麼?
咱們建議在用Chrome DevTools作內存分析時,你可使用關閉全部擴展功能的隱身模式,或設置用戶文件夾爲(--user-data-dir=""
)後再打開Chrome。
應用,擴展甚至console中的記錄都會對你的分析有潛在的影響,若是你想讓你的分析可靠的話,禁用這些吧。
寫在最後的話
今天的JavaScript引擎已經具備很強的能力,可以自動回收代碼產生的內存垃圾。就是說,它們只能作到這樣了,但咱們的應用仍然被證實會由於邏輯錯誤而產生內存泄漏。使用相應的工具來找到應用的瓶頸,記住,不要靠猜 – 測試它。
儘管不少內容在本文章中已經提到了,但一系列測試內存相關的問題的例子仍是頗有用的,下面是一組DOM節點內存泄漏的例子。你可能但願在測試你的更復雜的頁面或應用前先用這些例子作試驗。
更多例子: