內(nèi)存泄露是指計(jì)算機(jī)內(nèi)存逐漸丟失。當(dāng)某個(gè)程序總是無法釋放內(nèi)存時(shí),就會(huì)出現(xiàn)內(nèi)存泄露。JavaScript web 應(yīng)用程序可能會(huì)經(jīng)常遇到類似于本地程序中內(nèi)存泄露這樣的問題,比如泄露和膨脹,但是 JavaScript 有內(nèi)存回收機(jī)制可以解決此類問題。
盡管 JavaScript 使用了內(nèi)存回收機(jī)來自動(dòng)管理內(nèi)存,高效的內(nèi)存管理策略依然是相當(dāng)重要的。在本章中我們會(huì)詳細(xì)說明 JavaScript web 應(yīng)用程序中的內(nèi)存問題。在學(xué)習(xí)某些特性的時(shí)候請(qǐng)嘗試這些示例,這可以增進(jìn)你對(duì)于工具運(yùn)行原理的認(rèn)識(shí)。
在開始之前,請(qǐng)查看 Memory 101 頁(yè)面來熟悉一下相關(guān)的專業(yè)術(shù)語(yǔ)。
注意:我們?cè)诤竺媸褂玫挠行┨匦允侵挥?Chrome Canary 才支持的。我們建議使用此版本的工具,這樣您就可以對(duì)您的應(yīng)用程序做出最佳的內(nèi)存分析。
通常情況下,當(dāng)你認(rèn)為你的程序出現(xiàn)內(nèi)存泄露的時(shí)候,你需要問自己三個(gè)問題:
視圖內(nèi)容
這個(gè)部分介紹了內(nèi)存分析中常見的術(shù)語(yǔ),即使是在其他語(yǔ)言的內(nèi)存分析工具中,這些術(shù)語(yǔ)也同樣有用。這里所說的術(shù)語(yǔ)和概念是用于堆探查器界面以及相應(yīng)文檔中的。
了解這些術(shù)語(yǔ)后,你們就能更加高效地使用這個(gè)工具。如果你曾經(jīng)使用 Java、.Net 或者其它內(nèi)存分析器,那么該篇的內(nèi)容對(duì)你而言就是一次提升。
請(qǐng)將內(nèi)存狀況想象為一副圖片,圖中有著一些基本類型(像是數(shù)字以及字符串等)和對(duì)象(關(guān)聯(lián)數(shù)組)。如果像下面這樣將圖中的內(nèi)容用一些相互連接的點(diǎn)來表示,可能有助于你對(duì)此的理解:
對(duì)象可以通過兩種方式來獲取內(nèi)存:
當(dāng)使用 DevTools 中的堆分析器(一種用于查找“配置文件”下的內(nèi)存問題的工具)的時(shí)候,你會(huì)發(fā)現(xiàn)你所看到的是幾列信息。其中最重要的就是 Shallow Size 以及 Retained Size,不過,這兩列究竟意味著什么呢?
這是指對(duì)象本身獲得的內(nèi)存大小。
典型的 JavaScript 對(duì)象會(huì)獲得一些保留的內(nèi)存,用于他們的描述以及存儲(chǔ)即時(shí)產(chǎn)生的值。通常情況下,只有數(shù)組和字符串才會(huì)有比較明顯的淺層大小。不過,字符串和外部數(shù)組往往在渲染內(nèi)存中有它們自己的主存儲(chǔ)器,對(duì) JavaScript 堆只露出一點(diǎn)包裝后的對(duì)象。
渲染內(nèi)存是指所監(jiān)視的頁(yè)面被渲染的過程中使用的內(nèi)存:原本分配的內(nèi)存 + 該頁(yè)面在 JS 堆中的內(nèi)存 + 所有因?yàn)樵擁?yè)面而導(dǎo)致的 JS 堆中其他對(duì)象的內(nèi)存開銷。然而,即使是一個(gè)小的對(duì)象也可以通過阻止垃圾回收器自動(dòng)回收其他對(duì)象來間接保有大量的內(nèi)存。
這是指對(duì)象以及其相關(guān)的對(duì)象一起被刪除后所釋放的內(nèi)存大小,并且 GC roots 無法到達(dá)該處。
GC roots 是由在從原生代碼的 V8 之外引用 JavaScript 對(duì)象的時(shí)候所創(chuàng)建的句柄(局部或者全局的)構(gòu)成的。這些句柄可以再堆的快照中 GC roots > Handle scope 以及 GC roots > Global handles 中找到。在沒有談及瀏覽器實(shí)現(xiàn)的細(xì)節(jié)的情況下,就在本文中說明句柄會(huì)令讀者感到困惑,故而關(guān)于句柄的細(xì)節(jié)本文不做講解。事實(shí)上,無論 GC roots 還是句柄,都不是你需要擔(dān)心的東西。
內(nèi)部的 GC roots 有很多,不過用戶對(duì)其中的大部分都不感興趣。從應(yīng)用程序的角度來說,有下面這么幾種 roots:
注意:我們推薦讀者在清空控制臺(tái)并且調(diào)試器中沒有活躍的斷點(diǎn)的情況下來做堆的快照。
下面的內(nèi)存就是由一個(gè)根節(jié)點(diǎn)開始的,這個(gè)根節(jié)點(diǎn)可能是瀏覽器的 window 對(duì)象或者是 Node.js 模塊的 Global 對(duì)象。你并不需要知道這個(gè)對(duì)象是如何被回收的。
任何無法被根節(jié)點(diǎn)取得的元素夠?qū)⒈换厥铡?/p>
提示:Shallow 和 Retained size 都用字節(jié)來表示數(shù)據(jù)。
就像我們前面所說的,堆就是由相互連接的對(duì)象構(gòu)成的網(wǎng)絡(luò)。在數(shù)學(xué)的世界中,這種結(jié)構(gòu)稱作圖或者內(nèi)存圖。一個(gè)圖是由節(jié)點(diǎn)和邊構(gòu)成的,而節(jié)點(diǎn)又是由邊連接起來的,其中節(jié)點(diǎn)和邊都有相應(yīng)的標(biāo)簽。
在本文后面的內(nèi)容中,你將會(huì)學(xué)到如何使用堆探查器來記錄資料。在堆分析器記錄中我們可以看到包括 Distance 在內(nèi)的幾欄:Distance 指的是從根節(jié)點(diǎn)到當(dāng)前節(jié)點(diǎn)的距離。有一種情況是值得探究的,那就是幾乎所有同類的對(duì)象都有著相同的距離,但是有一小部分對(duì)象的 Distance 的值要比其他對(duì)象大一些。
主導(dǎo)者對(duì)象是由樹形結(jié)構(gòu)組成的,因?yàn)槊總€(gè)對(duì)象都只有一個(gè)主導(dǎo)者。一個(gè)對(duì)象的支配者不一定直接引用它所主導(dǎo)的對(duì)象,也就是說,支配樹并不是圖的生成樹。
在上面的圖中:
在下面的例子中,節(jié)點(diǎn) #3 是 #10 的主導(dǎo)者,但是 #7 節(jié)點(diǎn)也在由 GC 到 #10 節(jié)點(diǎn)的,每條簡(jiǎn)單路徑上。因此,如果對(duì)象 B 存在于從根節(jié)點(diǎn)到對(duì)象 A 的,每條簡(jiǎn)單路徑上,那么對(duì)象 B 就是對(duì)象 A 的主導(dǎo)者。
在本節(jié)中,我們所講的是對(duì)應(yīng) V8 JavaScript 虛擬機(jī)(V8 VM 或者 VM)的內(nèi)存方面的話題。這些內(nèi)容對(duì)于理解堆快照為何是上面所看到的那個(gè)樣子很有幫助。
JavaScript 中有三種主要類型:
這些類型在樹中都是葉子節(jié)點(diǎn)或者終結(jié)節(jié)點(diǎn),并且它們不能引用其它值。
數(shù)字類型可以像下面這樣存儲(chǔ):
字符串可以被存儲(chǔ)在:
新的 JavaScript 對(duì)象的內(nèi)存是由特定的 JavaScript 堆(或者說 VM 堆)分配的。這些對(duì)象由 V8 垃圾回收器管理,并且只要存在一個(gè)對(duì)他們的強(qiáng)引用就不會(huì)被回收。
本地對(duì)象指的是不在 JavaScript 堆中存儲(chǔ)的一切對(duì)象。本地對(duì)象和堆對(duì)象相反,其生存周期不由 V8 垃圾回收器管理,并且只能通過封裝它們的 JavaScript 對(duì)象來使用。
Cons string 是一個(gè)保存了成對(duì)字符串的對(duì)象,并且該對(duì)象會(huì)將字符串拼接起來,最后的結(jié)果是串聯(lián)后的字符串。拼接后的 cons string 的內(nèi)容只有在需要的時(shí)候才會(huì)出現(xiàn)。一個(gè)比較好的例子就是,如果想獲取某個(gè)字符串的子串,就必須利用函數(shù)進(jìn)行構(gòu)建。
舉個(gè)例子,如果你將 a 和 b 對(duì)象串聯(lián),那么你將獲得一個(gè)字符串(a,b) 用于表示拼接后的結(jié)果。如果你之后又加入了一個(gè)對(duì)象 d,那么你將活的另一個(gè)字符串((a,b),d)。
數(shù)組 - 一個(gè)數(shù)組就是有著數(shù)字鍵的對(duì)象。他們廣泛應(yīng)用在 V8 VM 中,用于存儲(chǔ)大量數(shù)據(jù)。在字典這樣的數(shù)據(jù)結(jié)構(gòu)中鍵值對(duì)的集合就是利用數(shù)組來備份的。
一個(gè)典型的用于存儲(chǔ)的 JavaScript 對(duì)象可以是下列兩種數(shù)組類型之一:
如果想要存儲(chǔ)的是少量的屬性,那么它們可以直接在 JavaScript 對(duì)象中存儲(chǔ)。
Map - 一個(gè)對(duì)象,用于描述對(duì)象及其布局的種類。舉個(gè)例子,maps 用于描述快速屬性訪問的隱式對(duì)象結(jié)構(gòu)。
每個(gè)本地的對(duì)象組都是由保持彼此相互引用的對(duì)象組成的。以一個(gè) DOM 子樹為例,在該樹中,每一個(gè)節(jié)點(diǎn)都一個(gè)指向父節(jié)點(diǎn)的連接,以及指向孩子節(jié)點(diǎn)和兄弟節(jié)點(diǎn)的鏈接,由此,所有的節(jié)點(diǎn)連成了一張圖。需要注意的是,本地對(duì)象并不會(huì)在 JavaScript 堆中出席那,所以它們的大小是 0。相應(yīng)的,對(duì)于每個(gè)要使用本地對(duì)象都會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的封裝對(duì)象。
每個(gè)封裝對(duì)象都含有一個(gè)對(duì)相應(yīng)的本地對(duì)象的引用,這是為了能夠?qū)⒚钪囟ㄏ虻奖镜貙?duì)象上。而對(duì)象組則含有這些封裝的對(duì)象,但是,這并不會(huì)造成一個(gè)無法回收的死循環(huán),因?yàn)槔厥掌鲿?huì)自動(dòng)釋放不在引用的封裝對(duì)象。但是一旦忘記了釋放某個(gè)封裝對(duì)象就可能造成整個(gè)組以及相關(guān)封裝對(duì)象都無法被釋放。
注意:在 Chrome 中分析內(nèi)存問題時(shí),一個(gè)比較好的方法就是配置 clean-room testing 環(huán)境。
如果某個(gè)頁(yè)面消耗了大量?jī)?nèi)存,可以在執(zhí)行有可能占用大量?jī)?nèi)存的活動(dòng)時(shí)使用 Chrome 任務(wù)管理器的內(nèi)存這一欄來監(jiān)視頁(yè)面所占用的內(nèi)存。如果要使用任務(wù)管理器,點(diǎn)擊 menu > Tools 或者使用快捷鍵 Shift
+ Esc
。
打開之后,右鍵點(diǎn)擊列頭部分然后啟用 JavaScript memory 列。
要解決問題的第一步就是要先擁有找出問題的能力。這意味著能夠創(chuàng)建一個(gè)用于基本問題測(cè)量的可重復(fù)性測(cè)試。如果沒有一個(gè)可復(fù)用的程序,你就沒辦法有效地衡量問題。另外,如果連測(cè)試基線都沒有的話,就沒辦法知道做出的改變是否提高了程序的性能。
時(shí)間軸面板對(duì)于發(fā)現(xiàn)問題出現(xiàn)的時(shí)間非常有幫助。頁(yè)面或者應(yīng)用程序加載或者進(jìn)行交互時(shí),它會(huì)給出整個(gè)流程的時(shí)間消耗的完整概述。所有的事件,從加載資源到解析 JavaScript、計(jì)算樣式、垃圾回收以及重繪都會(huì)出現(xiàn)在時(shí)間軸上。
在尋找內(nèi)存問題的時(shí)候,時(shí)間軸面板的 Memory view 可以用來追溯:
想要了解在內(nèi)存分析時(shí)找出可能造成內(nèi)存泄露的問題的更多信息,請(qǐng)查看 Zack Grossbart 寫的 Memory profiling with the Chrome DevTools
首先要做的事情就是找出你認(rèn)為可能造成內(nèi)存泄露的活動(dòng)。這種活動(dòng)可能是任何事情,就像是在站點(diǎn)上進(jìn)行定位、鼠標(biāo)的懸停事件、點(diǎn)擊事件或者是與頁(yè)面交互時(shí)可能對(duì)性能產(chǎn)生消極影響的事件。
在時(shí)間軸面板中,開始記錄(Ctrl
+ E
或者 Cmd
+ E
)然后執(zhí)行你想測(cè)試的活動(dòng)序列。要強(qiáng)制進(jìn)行垃圾回收,點(diǎn)擊底部的垃圾圖標(biāo)()。
在下圖中我們可以發(fā)現(xiàn)有些節(jié)點(diǎn)沒有被回收,而這些節(jié)點(diǎn)所對(duì)應(yīng)的圖案就是內(nèi)存泄露的圖案樣式:
如果在幾次迭代后你看見了一個(gè)鋸齒形的圖案(在內(nèi)存面板的頂部),這就說明你分配了大量短生存期的對(duì)象。但是,如果這個(gè)操作序列并沒有使內(nèi)存保留下來,或者 DOM 節(jié)點(diǎn)的數(shù)量并沒有下降到剛開始執(zhí)行時(shí)的那個(gè)基線上,那么你有很好的理由來懷疑這里發(fā)生了內(nèi)存泄露。
一旦你確認(rèn)了存在問題,你就可以借助 Profiles panel 中的 heap profiler 找出問題的來源。
示例:你可以嘗試一下這個(gè)例子來鍛煉一下如何高效使用時(shí)間軸內(nèi)存模式。
垃圾回收器(就像是 V8)能夠定位到你的程序處于生存期的對(duì)象以及已經(jīng)死亡的對(duì)象,甚至是無法訪問到的對(duì)象。
如果垃圾回收器(GC)由于某些邏輯錯(cuò)誤沒能回收你的 javaScript 中已死亡的對(duì)象,那么它們所消耗的內(nèi)存將無法被再次使用。像這樣的情況最終會(huì)隨著時(shí)間推移而使得你的應(yīng)用程序的執(zhí)行速率不斷變慢。
如果你在編寫代碼時(shí),即使是不再需要的變量以及事件監(jiān)聽器依舊被其他代碼所引用,最終就會(huì)出現(xiàn)這種情況。當(dāng)這些引用存在的時(shí)候,垃圾回收器就沒辦法正確清理這些對(duì)象。
在你的應(yīng)用程序的生存期間會(huì)有一些 DOM 元素更新/死亡,別忘了檢出并消除引用了這些元素的變量。檢查可能引用了其他對(duì)象(或者其他 DOM 元素)的對(duì)象的屬性,并留意可能隨著時(shí)間的推移不斷增長(zhǎng)的變量緩存。
在配置面板中,選擇 Take Heap Snapshot,然后點(diǎn)擊 Start 或者使用 Cmd
+ E
或 Ctrl
+ E
快捷鍵。
最初快照是存在渲染內(nèi)存中的,當(dāng)你點(diǎn)擊快照?qǐng)D標(biāo)來查看它的時(shí)候,它將會(huì)被傳輸?shù)?DevTools 中。當(dāng)快照載入到 DevTools 中并被解析后,快照標(biāo)題下面會(huì)出現(xiàn)一個(gè)數(shù)字,該數(shù)字表示所有可訪問的 JavaScript 對(duì)象的總大?。?/p>
示例:嘗試使用這個(gè)例子來監(jiān)測(cè)時(shí)間軸匯總內(nèi)存的使用情況。
點(diǎn)擊清除全部配置圖標(biāo)()可以清楚快照(DevTools 中和渲染內(nèi)存中都會(huì)刪除掉):
注意:直接關(guān)閉 DevTools 窗口并不會(huì)刪除渲染內(nèi)存中的配置文件。當(dāng)重新打開 DevTools 窗口的時(shí)候,所有之前生成的快照都會(huì)在快照列表中出現(xiàn)。
記得之前文章中提到過,你可以從 DevTools 中強(qiáng)制進(jìn)行垃圾回收,并且這可以成為你的快照工作流中的一部分。當(dāng)生成一個(gè)堆快照的時(shí)候,DevTools 會(huì)自動(dòng)進(jìn)行垃圾回收。在時(shí)間軸中該過程可以通過點(diǎn)擊垃圾桶按鈕()輕松實(shí)現(xiàn)。
示例:嘗試這個(gè)例子并使用堆分析器來進(jìn)行分析。你應(yīng)該看到(對(duì)象)項(xiàng)目分配次數(shù)。
一份快照可以用不同的視角來查看,這樣可以更好地適應(yīng)不同的需求。要在視圖間切換,使用視圖底部的選擇器:
一共有三種默認(rèn)視圖:
在設(shè)置面板中可以啟用主導(dǎo)視圖 - 顯示了主導(dǎo)樹的內(nèi)容,并且可以用于找到聚集點(diǎn)。
對(duì)象的屬性以及屬性值屬于不同類型并且有著相應(yīng)的顏色。每個(gè)屬性都會(huì)有四種類型之一:
被命名為 System 這樣的對(duì)象是沒有相應(yīng)的 JavaScript 類型的。他們是 JavaScript 虛擬機(jī)的對(duì)象系統(tǒng)的一部分。V8 將大多數(shù)內(nèi)部對(duì)象分配到和用戶 JS 對(duì)象相同的堆中,所以這些都只是 V8 內(nèi)部?jī)?nèi)容。
要在堆中找到某個(gè)對(duì)象,你可以使用 Ctrl
+ F
來打開搜索框,然后輸入對(duì)象的 ID。
最開始的時(shí)候,快照是在總結(jié)視圖中打開的,顯示了對(duì)象的整體情況,并且該視圖可以展開以顯示實(shí)例信息:
頂級(jí)入口是 "total" 行,他們展示了:
想上圖那樣展開 total line 之后,其所有的實(shí)例都會(huì)顯示出來。對(duì)于每個(gè)實(shí)例,它的 shallow size 和 retained size 都會(huì)在相應(yīng)列中展示出來。在 @ 字符后面的數(shù)字就是對(duì)象的 ID,該 ID 允許你在每個(gè)對(duì)象的基礎(chǔ)上比較堆的快照。
示例:通過這個(gè)頁(yè)面來了解如何使用總結(jié)視圖。
請(qǐng)記住,黃色的對(duì)象表示有 JavaScript 對(duì)象引用了它們,而紅色的對(duì)象是指從一個(gè)黃色背景節(jié)點(diǎn)引用的分離節(jié)點(diǎn)。
這個(gè)視圖用于比較不同的快照,這樣,你就可以通過比較它們的不同之處來找出出現(xiàn)內(nèi)存泄露的對(duì)象。想要弄清楚一個(gè)特定的程序是否造成了泄露(比如,通常是相對(duì)的兩個(gè)操作,就像是打開文檔,然后關(guān)閉它,是不會(huì)留下內(nèi)存垃圾的),你可以嘗試下列步驟:
在比較視圖中,兩份快照間的不同之處會(huì)展示出來。當(dāng)展開一個(gè)總?cè)肟跁r(shí),添加以及刪除的對(duì)象實(shí)例會(huì)顯示出來:
示例:嘗試這個(gè)例子(在選項(xiàng)卡中打開)來了解如何使用比較視圖來監(jiān)測(cè)內(nèi)存泄露。
包含視圖本質(zhì)上就像是你的應(yīng)用程序?qū)ο蠼Y(jié)構(gòu)的俯視圖。它使你能夠查看到函數(shù)閉包內(nèi)部,甚至是觀察到那些組成 JavaScript 對(duì)象的虛擬機(jī)內(nèi)部對(duì)象,借助該視圖,你可以了解到你的應(yīng)用底層占用了多少內(nèi)存。
這個(gè)視圖提供了多個(gè)接入點(diǎn):
下面是常見的包含視圖的例子:
示例:通過這個(gè)頁(yè)面(在新的選項(xiàng)卡中打開)來嘗試如何在該視圖中找到閉包和事件處理器。
為函數(shù)命名有助于你在快照中分辨不同的閉包。舉個(gè)例子,下面這個(gè)函數(shù)沒有命名:
function createLargeClosure() {
var largeStr = new Array(1000000).join('x');
var lC = function() {
// this is NOT a named function
return largeStr;
};
return lC;
}
而下面這個(gè)是命名后的函數(shù):
function createLargeClosure()
{
var largeStr = new Array(1000000).join('x');
var lC = function lC() {
// this IS a named function
return largeStr;
}; return lC;
}
示例:嘗試一下這個(gè)例子來分析閉包對(duì)內(nèi)存的影響。你可能會(huì)對(duì)下面這個(gè)例子感興趣,它可以讓你深入了解堆內(nèi)存分配
該工具的一大特點(diǎn)就是它能夠顯示瀏覽器本地對(duì)象(DOM 結(jié)點(diǎn),CSS 規(guī)則)以及 JavaScript 對(duì)象間的雙向依賴關(guān)系。這有助于發(fā)現(xiàn)因?yàn)橥浄蛛x DOM 子樹而導(dǎo)致的不可見的泄露。
DOM 泄露肯能比你想象中的要多??紤]下面這個(gè)例子 - 什么時(shí)候 #tree 會(huì)被回收?
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 包含了對(duì)其父親(父節(jié)點(diǎn))的引用并遞歸到 #tree,所以只有當(dāng) leafRef 失效的時(shí)候 #tree 下的整棵樹才能被回收。
示例:嘗試這個(gè)例子有助于你理解 DOM 節(jié)點(diǎn)中哪里容易出現(xiàn)泄露以及如何找到它們。你也可以繼續(xù)嘗試后面這個(gè)例子DOM 泄露斌想象的要更多。
想要了解更多關(guān)于 DOM 泄露以及內(nèi)存分析的基礎(chǔ)內(nèi)容,請(qǐng)參閱 Gonzalo Ruiz de Villa 編寫的 Finding and debugging memory leaks with the Chrome DevTools。
總結(jié)視圖和包含視圖更加容易找到本地對(duì)象 - 在視圖中有對(duì)應(yīng)本地對(duì)象的入口節(jié)點(diǎn):
示例:嘗試這個(gè)示例(在新選項(xiàng)卡中打開)來體驗(yàn)分離的 DOM 樹。
主導(dǎo)視圖顯示了堆圖的主導(dǎo)樹,從形式上來看,主導(dǎo)視圖有點(diǎn)像是包含視圖,但是缺少了某些屬性。這是因?yàn)橹鲗?dǎo)者對(duì)象可能會(huì)缺少對(duì)它的直接引用,也就是說,主導(dǎo)樹不是生成樹。
注意:在 Chrome Canary 中,主導(dǎo)視圖可以在 Settings > Show advance snapshots properties 中啟用,重啟瀏覽器之后就可以選擇主導(dǎo)視圖了。
示例:嘗試這個(gè)例子(在新選項(xiàng)卡中打開)來看看你能不能找到積累點(diǎn)。隨后可以嘗試運(yùn)行 retainning paths and dominators。
對(duì)象追蹤器結(jié)合了堆分析器中快照的詳細(xì)信息以及時(shí)間軸的增量更新以及追蹤信息。跟這些工具相似,追蹤對(duì)象堆的分配過程包括開始記錄,執(zhí)行一系列操作,以及停止記錄并分析。
對(duì)象分析器在記錄中周期性生成快照(大概每 50 毫秒就會(huì)生成一次),并且在記錄最后停止時(shí)也會(huì)生成一份快照。堆分配配置文件顯示了對(duì)象在哪里創(chuàng)建并且標(biāo)識(shí)出了保留路徑。
開啟并使用對(duì)象追蹤器
要開始使用對(duì)象追蹤器:
頂欄的條形圖表示對(duì)象什么時(shí)候在堆中被找到。每個(gè)條形圖的高度對(duì)應(yīng)最近分配的對(duì)象的大小,而其顏色則說明這些對(duì)象在最后的快照中是否還處于生存周期:藍(lán)色表示在時(shí)間軸的最后該對(duì)象依舊存在,灰色則說明對(duì)象在時(shí)間軸內(nèi)被分配,但是已經(jīng)被垃圾回收器回收了。
在上面的例子中,一個(gè)操作被執(zhí)行了10次。這個(gè)簡(jiǎn)單的程序加載了五個(gè)對(duì)象,所以顯示了五個(gè)藍(lán)色的條形圖案。但是最左邊的條形圖表明了一個(gè)潛在的問題。接下來你可以使用時(shí)間軸中的滑動(dòng)條來放大這一特定的快照,然后查看最近被分配到這一點(diǎn)上的對(duì)象。
點(diǎn)擊堆中的某個(gè)特定對(duì)象會(huì)在堆快照的頂部顯示其保留樹。檢查對(duì)象的保留路徑會(huì)讓你明白為什么對(duì)象沒有被回收,并且你可以在代碼中做出變動(dòng)來一出不需要的引用。
Q:我并沒有看到對(duì)象的所有屬性,我也沒看到那些非字符串 的值,為什么?
不是所有的屬性都儲(chǔ)存在 JavaScript 堆中。其中有些是通過執(zhí)行了本地代碼的獲取器來實(shí)現(xiàn)的。這樣的屬性不會(huì)在堆快照中被捕獲,因?yàn)橐苊庹{(diào)用獲取器的消耗并且要避免程序聲明的變化(當(dāng)獲取器不是“純”方法的時(shí)候)。同樣的,非字符串值,像是數(shù)字等為了縮小快照的大小也沒有捕獲。
Q:在 *@* 字符后面的數(shù)字意味著什么 - 這是一個(gè)地址或者 ID 嗎?ID 的值是不是唯一的?
這是對(duì)象 ID。顯示對(duì)象的地址毫無意義,因?yàn)閷?duì)象的地址在垃圾回收期間會(huì)發(fā)生偏移。這些對(duì)象 ID 是真正的 ID - 也就是說,他們?cè)谏娴亩鄠€(gè)快照都會(huì)存在,并且其值是唯一的。這就使得你可以精確地比較兩個(gè)不同時(shí)期的堆狀態(tài)。維護(hù)這些 ID 增加了垃圾回收周期的開銷,但是這只在第一份堆快照生成后才初始化 - 如果堆配置文件沒有使用到的話,就沒有開銷。
Q:“死亡”的(無法到達(dá))對(duì)象是否會(huì)包含在快照中?
不會(huì),只有可到達(dá)的對(duì)象才會(huì)在快照中出現(xiàn)。并且,生成一份快照的時(shí)候總是會(huì)先開始進(jìn)行垃圾回收。
注意:在編寫代碼的時(shí)候,我們希望避免這種垃圾回收方式以減少在生成堆快照時(shí),已使用的堆大小的變動(dòng)。這個(gè)還在實(shí)現(xiàn)中,但是垃圾回收依舊會(huì)在快照之外執(zhí)行。
Q:GC 根節(jié)點(diǎn)是由什么組成的?
許多東西:
Q:教程中說使用堆分析器以及時(shí)間軸內(nèi)存視圖來查找內(nèi)存泄露。首先應(yīng)該使用什么工具呢?
時(shí)間軸,使用該工具可以在你意識(shí)到頁(yè)面開始變慢的時(shí)候檢測(cè)出過高的內(nèi)存使用量。速度變慢是典型的內(nèi)存泄露癥狀,當(dāng)然也有可能是由其他情況造成的 - 也許你的頁(yè)面中有一些圖片或者是網(wǎng)絡(luò)存在瓶頸,所以要確認(rèn)你是否修復(fù)了實(shí)際的問題。
要診斷內(nèi)存是不是造成問題的原因,打開時(shí)間軸面板的內(nèi)存視圖。點(diǎn)擊紀(jì)錄按鈕然后開始與程序交互,重復(fù)你覺得出現(xiàn)問題的操作。停止記錄,顯示出來的圖片表示分配給應(yīng)用程序的內(nèi)存狀態(tài)。如果圖片顯示消耗的內(nèi)存總量一直在增長(zhǎng)(繼續(xù)沒有下落)則說明很有可能出現(xiàn)了內(nèi)存泄露。
一個(gè)正常的應(yīng)用,其內(nèi)存狀態(tài)圖應(yīng)該是一個(gè)鋸齒形的曲線圖,因?yàn)閮?nèi)存分配后會(huì)被垃圾回收器回收。這一點(diǎn)是毋庸置疑的 - 在 JavaScript 中的操作總會(huì)有所消耗,即使是一個(gè)空的 requestAnimationFrame 也會(huì)出現(xiàn)鋸齒形的圖案,這是無法避免的。只要確保沒有尖銳的圖形,就像是大量分配這樣的情況就好,因?yàn)檫@意味著在另一側(cè)會(huì)產(chǎn)生大量的垃圾。
你需要在意的是,這條曲線陡度的增加速率。在內(nèi)存視圖中,還有DOM 節(jié)點(diǎn)計(jì)數(shù)器,文檔計(jì)數(shù)器以及事件監(jiān)聽計(jì)數(shù)器,這些在診斷中都是非常有用的。DOM 節(jié)點(diǎn)使用原生內(nèi)存,并且不會(huì)直接影響到 JavaScript 內(nèi)存圖表。
如果你感覺程序中出現(xiàn)了內(nèi)存泄露,堆分析器可以幫助你找到內(nèi)存泄露的來源。
Q:我注意到在堆快照中有一些 DOM 節(jié)點(diǎn),其中有些是紅色的并且表明是 “分離的 DOM 樹” 而其他的是黃色的,這意味著什么?
你會(huì)注意到這些節(jié)點(diǎn)有著不同的顏色,紅色的節(jié)點(diǎn)(其背景較暗)沒有 JavaScript 對(duì)其的直接引用,但是依舊處于生存期,因?yàn)樗麄兪欠蛛x的 DOM 樹的一部分??赡軙?huì)有一些節(jié)點(diǎn)在 JavaScript 引用的樹中(可能是閉包或者變量)但是卻剛好阻止了整棵 DOM 樹被回收。
黃色的節(jié)點(diǎn)(其背景也是黃色的)則是有 JavaScript 對(duì)象直接引用的。在同一個(gè)分離 DOM 樹中查找黃色節(jié)點(diǎn)來鎖定 JavaScript 中的引用。從 DOM 窗口到達(dá)相關(guān)元素應(yīng)該是一條屬性鏈(比如,window.foo.bar[2].baz)
下面是關(guān)于獨(dú)立節(jié)點(diǎn)在整幅圖中位置的一個(gè)動(dòng)畫:
例子:嘗試這個(gè)關(guān)于獨(dú)立節(jié)點(diǎn)例子,通過這個(gè)例子你可以看到節(jié)點(diǎn)在時(shí)間軸中的變化過程,并且你可以生成堆快照來找到獨(dú)立節(jié)點(diǎn)。
Q:Shallow 以及 Retained Size 表示什么?它們之間有什么區(qū)別?
實(shí)際上,對(duì)象在內(nèi)存中的停留是有兩種方式的 - 通過一個(gè)其他處于生存期的對(duì)象直接保留在內(nèi)存中(比如 window 和 document 對(duì)象)或者通過保留對(duì)本地渲染內(nèi)存中某些部分的引用而隱式地保留在內(nèi)存中(就像 DOM 對(duì)象)。后者會(huì)導(dǎo)致相關(guān)的對(duì)象無法被內(nèi)存回收器自動(dòng)回收,最終造成泄漏。而對(duì)象本身含有的內(nèi)存大小則是 shallow size(一般來說數(shù)組和字符串有著比較大的 shallow size)。
如果某個(gè)對(duì)象阻止了其他對(duì)象被回收,那么不管這個(gè)對(duì)象有多大,它所占用的內(nèi)存都將是巨大的。當(dāng)一個(gè)對(duì)象被刪除時(shí)可以回收的內(nèi)存大小則被稱為保留量。
Q:在構(gòu)建器以及保留視圖中有大量的數(shù)據(jù)。如果我發(fā)現(xiàn)存在泄漏的時(shí)候,應(yīng)該從哪里開始找起?
一般來說從你的樹中保留的第一個(gè)對(duì)象開始找起是個(gè)好辦法,因?yàn)楸槐A舻膬?nèi)容是按照距離排序的(也就是到 window 的距離)。
一般來說,保留的對(duì)象中,有著最短距離的通常是最有可能造成內(nèi)存泄漏的。
Q:總結(jié),比較,主導(dǎo)和包含視圖都有哪些不同?
屏幕的底端可以選擇不同的數(shù)據(jù)視圖以實(shí)現(xiàn)不同的作用。
Q:在堆分析器中不同的構(gòu)建器入口對(duì)應(yīng)什么功能?
<script>
標(biāo)簽對(duì)應(yīng)。SharedFunctionInfos(SFI)是在函數(shù)和編譯后的代碼之間的對(duì)象。函數(shù)通常會(huì)有上下文,而 SFI 則沒有。其他的很多對(duì)象在你看來就像是在你代碼的生存期內(nèi)產(chǎn)生的,這些對(duì)象可能包含了事件監(jiān)聽器以及特定對(duì)象,就像是下面這樣:
Q:在 Chrome 中為了不影響到我的圖表有什么功能是應(yīng)該關(guān)閉的嗎?
在 Chrome DevTools 中使用設(shè)置的時(shí)候,推薦在化名模式下并關(guān)閉所有擴(kuò)展功能或者直接通過特定用戶數(shù)據(jù)目錄來啟動(dòng) Chrome(--user-data-dir="")。
如果希望圖表盡可能的精確的話,那么應(yīng)用,擴(kuò)展插件甚至是控制臺(tái)日志都可能隱式地影響到你的圖表。
今天的 JavaScript 引擎在多種情況下都可以自動(dòng)清理代碼中產(chǎn)生的垃圾。也就是說,它們只能做到這里了,而我們的代碼中仍然會(huì)由于邏輯問題出現(xiàn)內(nèi)存泄露。請(qǐng)運(yùn)用這些工具來找出你的瓶頸,并記住,不要去猜測(cè)它,而是去測(cè)試。
更多建議: