你需要思考的問題
總體來說,當你覺得你遇到了內存泄漏問題時,你需要思考三個問題:
- 我的頁面是否占用了過多的內存? - Timeline內存查看工具(Timeline memory view) 和 Chrome任務管理(Chrome task manager) 能幫助你確認你是否使用了過多的內存。Memory view 能跟蹤頁面渲染過程中DOM節點計數,documents文檔計數和JS事件監聽計數。作為一個經驗法則:避免對不再需要用到的DOM元素的引用,移除不需要的事件監聽並且在存儲你可能不會用到的大塊數據時要留意。
- 我的頁面有沒有內存泄漏? - 對象分配跟蹤(Object allocation tracker)通過實時查看JS對象的分配來幫助你定位泄漏。你也可以使用堆分析儀(Heap Profiler)生成JS堆快照,通過分析內存圖和比較快照之間的差異,來找出沒有被垃圾回收清理掉的對象。
- 我的頁面垃圾強制回收有多頻繁? - 如果你的頁面垃圾回收很頻繁,那說明你的頁面可能內存使用分配太頻繁了。Timeline內存查看工具(Timeline memory view) 能夠幫助你發現感興趣的停頓。
術語和基本概念
本小節介紹在內存分析時使用的常用術語,這些術語在為其它語言做內存分析的工具中也適用。這里的術語和概念用在了堆分析儀(Heap Profiler)UI工具和相關的文檔中。
這些能夠幫助我們熟悉如何有效的使用內存分析工具。如果你曾用過像Java、.NET等語言的內存分析工具的話,那么這將是一個復習。
對象大小(Object sizes)
把內存想象成一個包含基本類型(像數字和字符串)和對象(關聯數組)的圖表。它可能看起來像下面這幅一系列相關聯的點組成的圖。
一個對象有兩種使用內存的方法:
- 對象自身直接使用
- 隱含的保持對其它對象的引用,這種方式會阻止垃圾回收(簡稱GC)對那些對象的自動回收處理。
當你使用DevTools中的堆分析儀(Heap Profiler,用來分析內存問題的工具,在DevTools的”Profile”標簽下)時,你可能會驚喜的發現一些顯示各種信息的欄目。其中有兩項是:直接占用內存(Shallow Size)和占用總內存(Retained Size),那它們是什么意思呢?
直接占用內存(Shallow Size,不包括引用的對象占用的內存)
這個是對象本身占用的內存。
典型的JavaScript對象都會有保留內存用來描述這個對象和存儲它的直接值。一般,只有數組和字符串會有明顯的直接占用內存(Shallow Size)。但字符串和數組常常會在渲染器內存中存儲主要數據部分,僅僅在JavaScript對象棧中暴露一個很小的包裝對象。
渲染器內存指你分析的頁面在渲染的過程中所用到的所有內存:頁面本身的內存 + 頁面中的JS堆用到的內存 + 頁面觸發的相關工作進程(workers)中的JS堆用到的內存。然而,通過阻止垃圾自動回收別的對象,一個小對象都有可能間接占用大量的內存。
占用總內存(Retained Size,包括引用的對象所占用的內存)
一個對象一但刪除后它引用的依賴對象就不能被GC根(GC root)引用到,它們所占用的內存就會被釋放,一個對象占用總內存包括這些依賴對象所占用的內存。
GC根是由控制器(handles)組成的,這些控制器(不論是局部還是全局)是在建立由build-in函數(native code)到V8引擎之外的JavaScript對象的引用時創建的。所有這些控制器都能夠在堆快照的GC roots(GC根) > Handle scope 和 GC roots >Global handlers中找到。如果不深入了解瀏覽器的實現原理,在這篇文章中介紹這些控制器可能會讓人不能理解。GC根和控制器你都不需要過多關心。
有很多內部的GC根對用戶來說都是不重要的。從應用的角度來說有下面幾種情況:
- Window 全局對象 (所有iframe中的)。在堆快照中有一個distance字段,它是從window對象到達對應對象的最短路徑長度。
- 由所有document能夠遍歷到的DOM節點組成的文檔DOM樹。不是所有節點都會被對應的JS引用,但有JS引用的節點在document存在的情況下都會被保留。
- 有很多對象可能是在調試代碼時或者DevTools console中(比如:console中的一些代碼執行結束后)創建出來的。
注意:我們推薦用戶在創建堆快照時,不要在console中執行代碼,也不要啟用調試斷點。
內存圖由一個根部開始,可能是瀏覽器的window對象或Node.js模塊Global對象。這些對象如何被內存回收不受用戶的控制。
不能被GC根遍歷到的對象都將被內存回收。
注意:直接占用內存和占用總內存字段中的數據是用字節表示的。
對象的占用總內存樹
之前我們已經了解到,堆是由各種互相關聯的對象組成的網狀結構。在數字領域,這種結構被稱為圖或內存圖。圖是由邊緣(edges)連接着的節點(nodes)組成的,他們都被貼了標簽。
- 節點(Nodes) (或對象) 節點的標簽名是由創建他們的構造(constructor)函數的名稱確定
- 邊緣(Edges) 標簽名就是屬性名
本文檔的后面你將了解到如何使用堆分析儀生成快照。從下圖的堆分析儀生成的快照中,我們能看到距離(distance)這個字段:是指對象到GC根的距離。如果同一個類型的所有對象的距離都一樣,而有一小部分的距離卻比較大,那么就可能出了些你需要進行調查的問題了。
支配對象(Dominators)
支配對象就像一個樹結構,因為每個對象都有一個支配者。一個對象的支配者可能不會直接引用它支配的對象,就是說,支配對象樹結構不是圖中的生成樹。
在上圖中:
- 節點1支配節點2
- 節點2支配節點3,4和6
- 節點3支配節點5
- 節點5支配節點8
- 節點6支配節點7
在下圖的例子中,節點#3是#10的支配者,但#7也在每個從GC到#10的路經中都出現了。像這樣,如果B對象在每個從根節點到A對象的路經中都出現,那么B對象就是A對象的支配對象。
V8介紹
在本節,我們將描述一些內存相關的概念,這些概念是和V8 JavaScript虛擬機(V8 VM 或VM)有關的。當分析內存時,了解這些概念對理解堆快照是有幫助的。
JavaScript對象描述
有三個原始類型:
- 數字(Numbers) (如 3.14159..)
- 布爾值(Booleans) (true或false)
- 字符型(Strings) (如 ‘Werner Heisenberg’)
它們不會引用別的值,它們只會是葉子節點或終止節點。
數字(Numbers)以下面兩種方式之一被存儲:
- 31位整數直接值,稱做:小整數(small integers)(SMIs),或
- 堆對象,引用為堆值。堆值是用來存儲不適合用SMI形式存儲的數據,像雙精度數(doubles),或者當一個值需要被打包(boxed)時,如給這個值再設置屬性值。
字符型數據會以下面兩種方式存儲:
- VM堆,或
- 外部的渲染器內存中。這時會創建一個包裝對象用來訪問存儲的位置,比如,Web頁面包存的腳本資源和其它內容,而不是直接復制至VM堆中。
新創建的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的內存管理器,觀察內存字段,在一個頁面上做相關的操作,你可以很快定位這個操作是否會導致頁面占用很多內存。你可以從Chrome菜單 > 工具或按Shift + Esc,找到內存管理器。
打開后,在標頭右擊選用 JavasScript使用的內存 這項。
通過DevTools Timeline來定位內存問題
解決問題的第一步就是要能夠證明問題存在。這就需要創建一個可重現的測試來做為問題的基准度量。沒有可再現的程序,就不能可靠的度量問題。換句話說如果沒有基准來做為對比,就無法知道是哪些改變使問題出現的。
時間軸面版(Timeline panel)對於發現程序什么時候出了問題很用幫助。它展示了你的web應用或網站加載和交互的時刻。所有的事件:從加載資源到解JavaScript,樣式計算,垃圾回收停頓和頁面重繪。都在時間軸上表示出來了。
當分析內存問題時,時間軸面版上的內存視圖(Memory view)能用來觀察:
- 使用的總內存 – 內存使用增長了么?
- DOM節點數
- 文檔(documents)數
- 注冊的事件監聽器(event listeners)數
更多的關於在內存分析時,定位內存泄漏的方法,請閱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)面板中監控內存的使用。
清除快照
點擊Clear all按鈕圖標(),就能清除掉所有快照:
注意:關閉DevTools窗口並不能從渲染內存中刪除掉收集的快照。當重新打開DevTools后,之前的快照列表還在。
記住我們之前提到的,當你生成快照時你可以強制執行在DevTools中GC。當我們拍快照時,GC是自動執行的。在時間軸(Timeline)中點擊垃圾桶(垃圾回收)按鈕()就可以輕松的執行垃圾回收了。
例子:嘗試一下scattered objects並用堆分析儀(Heap Profiler)分析它。你可以看到(對象)項目的集合。
切換快照視圖
一個快照可以根據不同的任務切換視圖。可以通過如圖的選擇框切換:
下面是三個默認視圖:
- Summary(概要) - 通過構造函數名分類顯示對象;
- Comparison(對照) - 顯示兩個快照間對象的差異;
- Containment(控制) - 可用來探測堆內容;
Dominators(支配者)視圖可以在Settings面板中開啟 – 顯示dominators tree. 可以用來找到內存增長點。
通過不同顏色區分對象
對象的屬性和屬性值有不同的類型並自動的通過顏么進行了區分。每個屬性都是以下四種之一:
- a:property - 通過名稱索引的普通屬性,由.(點)操作符,或 [](中括號)引用,如["foo bar"];
- 0:element - 通過數字索引的普通屬性,由 [](中括號)引用;
- a:context var - 函數內的屬性,在函數上下文內,通過名稱引用;
- a:system prop - 由JavaScript VM 添加的屬性,JavaScript代碼不能訪問。
命名為System的對象沒有對應的JavaScript類型。它們是JavaScript VM對象系統內置的。V8將大多數內置對象和用戶JS對象放在同一個堆中。但它們只是V8的內部對象。
視圖詳解
Summary view(概要視圖)
打開一個快照,默認是以概要視圖顯示的,顯示了對象總數,可以展開顯示具體內容: Initially, a snapshot opens in the Summary view, displaying object totals, which can be expanded to show instances:
第一層級是”總體”行,它們顯示了:
- Constructor(構造函數)表示所有通過該構造函數生成的對象
- 對象的實例數在Objects Count列上顯示
- Shallow size列顯示了由對應構造函數生成的對象的shallow sizes(直接占用內存)總數
- Retained size列展示了對應對象所占用的最大內存
- Distance列顯示的是對象到達GC根的最短距離
展開一個總體行后,會顯示所有的對象實例。沒一個實例的直接占用內存和占用總內存都被相應顯示。@符號后的數字不對象的唯一ID,有了它你就可以逐個對象的在不同快照間作對比。
例子:嘗試這個例子(在新tab標簽中打開)來了解如何使用概要視圖。
記住黃色的對象被JavaScript引用,而紅色的對象是由黃色背景色引用被分離了的節點。
Comparison view(對照視圖)
該視圖用來對照不同的快照來找到快照之間的差異,來發現有內存泄漏的對象。來證明對應用的某個操作沒有造成泄漏(比如:一般一對操作和撤消的動作,像找開一個document,然后關閉,這樣是不會造成泄漏的),你可以按以下的步驟嘗試:
- 在操作前拍一個堆快照;
- 執行一個操作(做你認為會造成泄漏的動作);
- 撤消之前的操作(上一個操作相反的操作,多重復幾次);
- 拍第二個快照,將視圖切換成對照視圖,並同快照1進行對比。
在對照視圖下,兩個快照之間的不同就會展現出來了。當展開一個總類目后,增加和刪除了的對象就顯示出來了:
例子:嘗試例子(在新tab標簽中打開)來了解如何使用對照視圖來定位內存泄漏。
Containment view(控制視圖)
控制視圖可以稱作對你的應用的對象結構的”鳥瞰視圖(bird’s eys view)”。它能讓你查看function內部,跟你的JavaScript對象一樣的觀察VM內部對象,能讓你在你的應用的非常低層的內存使用情況。
該視圖提供了幾個進入點:
- DOMWindow 對象 - 這些對象是JavaScript代碼的”全局”對象;
- GC根 - VM的垃圾回收器真正的GC根;
- Native對象 - 瀏覽器對象對”推入”JavaScript虛擬機中來進行自動操作,如:DOM節點,CSS規則(下一節會有詳細介紹。)
下圖是一個典型的控制視圖:
例子:嘗試例子(在新tab標簽中打開)來了解如何使用控制視圖來查看閉包內部和事件處理。
關於閉包的建議
給函數命名對你在快照中的閉包函數間作出區分會很用幫助。如:下面的例子中沒有給函數命名:
function createLargeClosure() { var largeStr = new Array(1000000).join('x'); var lC = function() { // this is NOT a named function return largeStr; }; return lC; }
而下面這個有給函數命名:
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內存泄漏
這個工具獨一無二的一點是展示了瀏覽器原生對象(DOM節點,CSS規則)和JavaScript對象之間的雙向引用。這能幫助你發現因為忘記解除引用游離的DOM子節點而導致的難以發覺的內存泄漏。
DOM內存泄漏可能會超出你的想象。看下下面的例子 – #tree對象什么時候被GC呢?
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樹分離。
支配者視圖(Dominators view)
支配者視圖顯示了堆圖的支配者樹。支配者視圖跟控制(Containment)視圖很像,但是沒有屬性名。這是因為支配者可能會是一個沒有直接引用的對象,就是說這個支配者樹不是堆圖的生成樹。但這是個有用的視圖能幫助我們很快的定位內存增長點。
注意:在Chrome Canary中,支配者視圖能夠在DevTools中的Settings > Show advanced heap snapshot properties 開啟,重啟DevTools生效。
例子:嘗試這個例子(在新tab標簽中打開)來練習如何找到內存增長點。可以進一步嘗試下一個例子retaining paths and dominators。
對象分配跟蹤器
對象跟蹤器整合了heap profiler的快照增量更新分析和Timeline面板的記錄。跟其它工具一樣,記錄對象的堆配置需要啟動記錄,執行一系列操作,然后停止記錄然后進行分析。
對象跟蹤器不間斷的記錄堆快照(頻率達到了每50毫秒!),結束時記錄最后一個快照。該堆分配分析器顯示對象在哪被創建並定位它的保留路徑。
開啟並使用對象分析器
開始使用對象分析器: 1. 確認你使用的是最新版的Chrome Canary。
- 打開DeveTools並點擊齒輪圖標(譯者:沒明白這步有什么用)。
- 現在,打開Profiler面板,你就能看到”Record Heap Allocations”的選項。
上面的柱條表示在堆中生成的新對象。高度就對應了相應對象的大小,它的顏色表示了這個對象是否在最后拍的那個快照中還在:藍色柱表示在timeline最后這個對象還在,灰色柱表示這個對象在timeline中生成,但結束前已經被內存回收了。
上面的例子中,一個動作執行了10次。同一個程序保留了5個對象,所以最后5個藍色柱條被保留了。但這最后留下的柱存在潛在的問題。你可以用timeline上的滑動條縮小到那個特定的快照並找到這個分配的對象。
點擊一個堆中的對象就能在堆快照的下面部分顯示它的保留總內存樹。檢查這個對象的保留總內存樹能夠給你足夠的信息來了解為什么這個對象沒有被回收,然后你就能對代碼做相應的修改來去掉不必要的引用。
內存分析FAQ
問:我不能看到對象的所有屬性,我也看到它們的非字符串值!為什么?
並非所有屬性都完整的保存在JavaScript堆中。其中有些是通過執行原生代碼的getters方法來獲取的。這些屬性沒有在堆快照中捕獲,是為了防止對getters方法的調用和避免程序狀態的改變,如果這些getters方法不是”純(pure)”的functions。同樣,非字符串的值,如數字,沒有被捕獲是為了減少快照的大小。
問:@符號后面的數字是什么意思 – 是地址還是ID呢?這個ID值真的是唯一的么?
這是對象ID。顯示對象的地址沒有意義,因為一個對象會在垃圾回收的時候被移除。這些對象IDs是真正的IDs – 就是說,它們在不同的快照間是唯一表示的。這樣就可以的堆狀態間進行精確的對比。維持這些IDs會給GC流程增加額外的開支,但這僅在記錄第一次堆快照時分配 – 如果堆分析儀沒有用到,就不會有額外的開支。
問:”死”(無法引用到的)對象被包含在快照中了么?
沒有,只有可以引用到的對象才會顯示在快照中。而且,拍快照前都會先自動執行GC操作。
注意:在寫這篇文章的時候,我們計划在拍快照的時候不再GC,防止堆尺寸的減少。現在已經是這樣了,但垃圾對象依然顯示在快照之外。
問:GC根是由什么組成的?
由很多部分組成:
- 原生對象圖;
- 符號表;
- VM線程中的棧;
- 編輯緩存;
- 控制器上下文;
- 全局控制器。
問:我得知可以使用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這些視圖之間的不同是什么?
你可以通過切換視圖來體驗它們的區別。
- Summary(概要)視圖能幫你通過構造函數分組尋找對象(和對象的內存使用)。該視圖對找出DOM內存泄漏很有幫助。
- Comparison(對照)視圖能夠通過顯示哪些對象內存被正確的回收了來搜尋內存泄漏。通常在一個操作前后記錄兩個(或更多)的內存使用快照。它是通過察看釋放的內存和引用數目的差導來察看是否有內存泄漏,並找到原因。
- Containment(控制)視圖對對象結構有更好的展示,幫助我們分析全局作用域(如 window)中對象引用情況來找到是什么保留了這些對象。它能讓你分析閉包並深入到對象更深層去查看。
- Dominators(支配者)視圖能用來幫助我們確認沒有多余的對象還掛在某個位置(如那些被引用了的),和確認對象的刪除/垃圾回收真正起了作用。
問:堆分析儀中的constructor(一組)內容代表什么?
- (global property) - 全局對象(像 ‘window’)和引用它的對象之間的中間對象。如果一個對象由構造函數Person生成並被全局對象引用,那么引用路徑就是這樣的: [global] > (global property) > Person。這跟一般的直接引用彼此的對象不一樣。我們用中間對象是有性能方面的原因,全局對象改變會很頻繁,非全局變量的屬性訪問優化對全局變量來說並不適用。
- (roots) - constructor中roots的內容引用它所選中的對象。它們也可以是由引擎自主創建的一些引用。這個引擎有用於引用對象的緩存,但是這些引用不會阻止引用對象被回收,所以它們不是真正的強引用(FIXME)。
- (closure) - 一些函數閉包中的一組對象的引用
- (array, string, number, regexp) - 一組屬性引用了Array,String,Number或正則表達式的對象類型
- (compiled code) - 簡單來說,所有東西都與compoled code有關。Script像一個函數,但其實對應了<script>的內容。SharedFunctionInfos (SFI)是函數和compiled code之間的對象。函數通常有內容,而SFIS沒有(FIXME)。
- HTMLDivElement, HTMLAnchorElement, DocumentFragment 等 – 你代碼中對elements或document對象的引用。
在你的程序的生命周期中生成的很多其它的對象,包括事件監聽器或自定義對象,可以在下面的controllers中找到:
問:我在做內存分析時需要關閉Chrome里可能會產生影響的什么功能么?
我們建議在用Chrome DevTools做內存分析時,你可以使用關閉所有擴展功能的隱身模式,或設置用戶文件夾為(--user-data-dir="")后再打開Chrome。
應用,擴展甚至console中的記錄都會對你的分析有潛在的影響,如果你想讓你的分析可靠的話,禁用這些吧。
寫在最后的話
今天的JavaScript引擎已經具有很強的能力,能夠自動回收代碼產生的內存垃圾。就是說,它們只能做到這樣了,但我們的應用仍然被證明會因為邏輯錯誤而產生內存泄漏。使用相應的工具來找到應用的瓶頸,記住,不要靠猜 – 測試它。
幫助實例
診斷內存泄漏
盡管很多內容在本文章中已經提到了,但一系列測試內存相關的問題的例子還是很有用的,下面是一組DOM節點內存泄漏的例子。你可能希望在測試你的更復雜的頁面或應用前先用這些例子做試驗。
- Example 1: Growing memory
- Example 2: Garbage collection in action
- Example 3: Scattered objects
- Example 4: Detached nodes
- Example 5: Memory and hidden classes
- Example 6: Leaking DOM nodes
- Example 7: Eval is evil (almost always)
- Example 8: Recording heap allocations
- Example 9: DOM leaks bigger than expected
- Example 10: Retaining path
- Example 11: Last exercise