Chrome開發(fā)工具 JavaScript 內(nèi)存分析

2018-03-01 18:50 更新

JavaScript 內(nèi)存分析

內(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)存分析。

應(yīng)該問自己的一些問題

通常情況下,當(dāng)你認(rèn)為你的程序出現(xiàn)內(nèi)存泄露的時(shí)候,你需要問自己三個(gè)問題:

  • 是不是我的頁(yè)面占用了太多的內(nèi)存?- 內(nèi)存時(shí)間軸視圖 以及 Chrome 任務(wù)管理器 可以幫助你來確認(rèn)是否占用了過多的內(nèi)存。內(nèi)存視圖在監(jiān)察過程中可以實(shí)時(shí)跟蹤 DOM 節(jié)點(diǎn)數(shù)目、文件以及 JS 事件監(jiān)聽器。有一條重要法則需要記?。罕苊獗A魧?duì)已經(jīng)不需要的 DOM 元素的引用,不必要的事件監(jiān)聽器請(qǐng)解除綁定,對(duì)于大量的數(shù)據(jù),在存儲(chǔ)時(shí)請(qǐng)注意不要存儲(chǔ)用不到的數(shù)據(jù)。
  • 我的頁(yè)面是不是沒有內(nèi)存泄露的問題?- 對(duì)象分配跟蹤器能夠讓你看到 JS 對(duì)象的實(shí)時(shí)分配過程,以此來降低內(nèi)存泄露的可能。你也可以使用堆探查器來記錄 JS 堆的狀態(tài),然后分析內(nèi)存圖并將其與堆狀態(tài)進(jìn)行比對(duì),就可以迅速發(fā)現(xiàn)那些沒有被垃圾回收器清理的對(duì)象。
  • 我的頁(yè)面應(yīng)該多久強(qiáng)制進(jìn)行一次垃圾回收? - 如果垃圾回收器總是處于垃圾回收狀態(tài),那么可能是你對(duì)象分配過于頻繁了。內(nèi)存時(shí)間軸視圖可以在你感興趣的地方停頓,方便你查看回收情況。

image_0

視圖內(nèi)容

術(shù)語(yǔ)以及基本原理

這個(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ì)你而言就是一次提升。

對(duì)象的大小

請(qǐng)將內(nèi)存狀況想象為一副圖片,圖中有著一些基本類型(像是數(shù)字以及字符串等)和對(duì)象(關(guān)聯(lián)數(shù)組)。如果像下面這樣將圖中的內(nèi)容用一些相互連接的點(diǎn)來表示,可能有助于你對(duì)此的理解:

thinkgraph

對(duì)象可以通過兩種方式來獲取內(nèi)存:

  • 直接通過它本身。
  • 通過包含對(duì)其它對(duì)象的引用,這樣就會(huì)阻止垃圾回收器(簡(jiǎn)稱 GC)自動(dòng)回收這些對(duì)象。

當(dāng)使用 DevTools 中的堆分析器(一種用于查找“配置文件”下的內(nèi)存問題的工具)的時(shí)候,你會(huì)發(fā)現(xiàn)你所看到的是幾列信息。其中最重要的就是 Shallow Size 以及 Retained Size,不過,這兩列究竟意味著什么呢?

images

Shallow 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)存。

Retained size

這是指對(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:

  • 窗口全局對(duì)象(在每一幀中)。在堆快照中,有一個(gè)距離域,其包含的是在窗口最短保留路徑上的屬性引用的數(shù)目。
  • 文檔 DOM 樹是由所有分析該文檔時(shí)能夠到達(dá)的 DOM 節(jié)點(diǎn)構(gòu)成的。并不是所有的節(jié)點(diǎn)都會(huì)有 JS 封裝,但是如果他們有封裝,那么只要文檔還在,這些節(jié)點(diǎn)就可以使用。
  • 有些時(shí)候,對(duì)象會(huì)被調(diào)試器上下文以及 DevTools 控制臺(tái)保留。(例如,在控制臺(tái)進(jìn)行評(píng)估后)

注意:我們推薦讀者在清空控制臺(tái)并且調(diào)試器中沒有活躍的斷點(diǎn)的情況下來做堆的快照。

下面的內(nèi)存就是由一個(gè)根節(jié)點(diǎn)開始的,這個(gè)根節(jié)點(diǎn)可能是瀏覽器的 window 對(duì)象或者是 Node.js 模塊的 Global 對(duì)象。你并不需要知道這個(gè)對(duì)象是如何被回收的。

dontcontrol

任何無法被根節(jié)點(diǎn)取得的元素夠?qū)⒈换厥铡?/p>

提示:Shallow 和 Retained size 都用字節(jié)來表示數(shù)據(jù)。

對(duì)象的保留樹

就像我們前面所說的,堆就是由相互連接的對(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)簽。

  • 節(jié)點(diǎn)(或者對(duì)象)是用創(chuàng)建對(duì)象的構(gòu)造函數(shù)標(biāo)記的。
  • 是用屬性名來標(biāo)記的。

在本文后面的內(nèi)容中,你將會(huì)學(xué)到如何使用堆探查器來記錄資料。在堆分析器記錄中我們可以看到包括 Distance 在內(nèi)的幾欄:Distance 指的是從根節(jié)點(diǎn)到當(dāng)前節(jié)點(diǎn)的距離。有一種情況是值得探究的,那就是幾乎所有同類的對(duì)象都有著相同的距離,但是有一小部分對(duì)象的 Distance 的值要比其他對(duì)象大一些。

images

主導(dǎo)者

主導(dǎo)者對(duì)象是由樹形結(jié)構(gòu)組成的,因?yàn)槊總€(gè)對(duì)象都只有一個(gè)主導(dǎo)者。一個(gè)對(duì)象的支配者不一定直接引用它所主導(dǎo)的對(duì)象,也就是說,支配樹并不是圖的生成樹。

dominatorsspanning

在上面的圖中:

  • 節(jié)點(diǎn) 1 主導(dǎo)了節(jié)點(diǎn) 2.
  • 節(jié)點(diǎn) 2 主導(dǎo)了節(jié)點(diǎn) 3,4,6
  • 節(jié)點(diǎn) 3 主導(dǎo)了節(jié)點(diǎn) 5
  • 節(jié)點(diǎn) 5 主導(dǎo)了節(jié)點(diǎn) 8
  • 節(jié)點(diǎn) 6 主導(dǎo)了節(jié)點(diǎn) 7

在下面的例子中,節(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)者。

dominator

V8 的細(xì)節(jié)

在本節(jié)中,我們所講的是對(duì)應(yīng) V8 JavaScript 虛擬機(jī)(V8 VM 或者 VM)的內(nèi)存方面的話題。這些內(nèi)容對(duì)于理解堆快照為何是上面所看到的那個(gè)樣子很有幫助。

JavaScript 對(duì)象的表示

JavaScript 中有三種主要類型:

  • 數(shù)字(比如,3.14159..)
  • 布爾值(true 或者 false)
  • 字符串 (比如 "Werner Heisenberg")

這些類型在樹中都是葉子節(jié)點(diǎn)或者終結(jié)節(jié)點(diǎn),并且它們不能引用其它值。

數(shù)字類型可以像下面這樣存儲(chǔ):

  • 相鄰的 31 位整數(shù)值,被稱為 small integers (SMIs)
  • 被稱為堆數(shù)字的堆對(duì)象。堆數(shù)字用于存儲(chǔ)不適合 SMI 形式的值,比如浮點(diǎn)類型,或者是需要封裝的值,比如設(shè)置其屬性值的類型。

字符串可以被存儲(chǔ)在:

  • 虛擬機(jī)的堆
  • 外部的渲染內(nèi)存。也就是當(dāng)創(chuàng)建或者使用一個(gè)封裝后的對(duì)象時(shí)需要使用的外部存儲(chǔ)器,比如,腳本資源以及其他從網(wǎng)上接收而不是賦值到虛擬機(jī)堆中存儲(chǔ)的內(nèi)容。

新的 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è)例子,如果你將 ab 對(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ù)組類型之一:

  • 命名的屬性
  • 數(shù)字元素

如果想要存儲(chǔ)的是少量的屬性,那么它們可以直接在 JavaScript 對(duì)象中存儲(chǔ)。

Map - 一個(gè)對(duì)象,用于描述對(duì)象及其布局的種類。舉個(gè)例子,maps 用于描述快速屬性訪問的隱式對(duì)象結(jié)構(gòu)。

對(duì)象組

每個(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 任務(wù)管理器

注意:在 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。

image

打開之后,右鍵點(diǎn)擊列頭部分然后啟用 JavaScript memory 列。

使用 DevTools 時(shí)間軸來找出內(nèi)存問題

要解決問題的第一步就是要先擁有找出問題的能力。這意味著能夠創(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)存 - 內(nèi)存的使用量是否增長(zhǎng)了?
  • DOM 節(jié)點(diǎn)的數(shù)量。
  • 文檔的數(shù)量
  • 分配的事件監(jiān)聽器的數(shù)量。

image

想要了解在內(nèi)存分析時(shí)找出可能造成內(nèi)存泄露的問題的更多信息,請(qǐng)查看 Zack Grossbart 寫的 Memory profiling with the Chrome DevTools

驗(yàn)證存在的問題

首先要做的事情就是找出你認(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)(collect-garbage)。

在下圖中我們可以發(fā)現(xiàn)有些節(jié)點(diǎn)沒有被回收,而這些節(jié)點(diǎn)所對(duì)應(yīng)的圖案就是內(nèi)存泄露的圖案樣式:

nodescollect

如果在幾次迭代后你看見了一個(gè)鋸齒形的圖案(在內(nèi)存面板的頂部),這就說明你分配了大量短生存期的對(duì)象。但是,如果這個(gè)操作序列并沒有使內(nèi)存保留下來,或者 DOM 節(jié)點(diǎn)的數(shù)量并沒有下降到剛開始執(zhí)行時(shí)的那個(gè)基線上,那么你有很好的理由來懷疑這里發(fā)生了內(nèi)存泄露。

image

一旦你確認(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 + ECtrl + E 快捷鍵。

image

最初快照是存在渲染內(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>

image

示例:嘗試使用這個(gè)例子來監(jiān)測(cè)時(shí)間軸匯總內(nèi)存的使用情況。

清除快照

點(diǎn)擊清除全部配置圖標(biāo)(image)可以清楚快照(DevTools 中和渲染內(nèi)存中都會(huì)刪除掉):

image

注意:直接關(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)擊垃圾桶按鈕(collect-garbage)輕松實(shí)現(xiàn)。

force

示例:嘗試這個(gè)例子并使用堆分析器來進(jìn)行分析。你應(yīng)該看到(對(duì)象)項(xiàng)目分配次數(shù)。

在快照視圖間切換

一份快照可以用不同的視角來查看,這樣可以更好地適應(yīng)不同的需求。要在視圖間切換,使用視圖底部的選擇器:

image

一共有三種默認(rèn)視圖:

  • 總結(jié) - 通過構(gòu)造器的名稱來分組顯示對(duì)象
  • 比較 - 顯示兩份快照間的不同之處
  • 包含 - 允許查看堆中的內(nèi)容

在設(shè)置面板中可以啟用主導(dǎo)視圖 - 顯示了主導(dǎo)樹的內(nèi)容,并且可以用于找到聚集點(diǎn)。

查看代碼顏色

對(duì)象的屬性以及屬性值屬于不同類型并且有著相應(yīng)的顏色。每個(gè)屬性都會(huì)有四種類型之一:

  • a:property - 有名稱的常規(guī)屬性,通過 .(點(diǎn))操作符或者 [](方括號(hào))符號(hào)來訪問,例如 ["foo bar"];
  • 0:element - 有數(shù)字下標(biāo)的常規(guī)屬性,使用 [](方括號(hào))來訪問。
  • a:context var - 函數(shù)上下文中的某個(gè)變量,在相應(yīng)的函數(shù)閉包中使用其名字就可以訪問。
  • a:system prop - 由 JavaScript 虛擬機(jī)添加的屬性,在 JavaScript 代碼中無法訪問。

被命名為 System 這樣的對(duì)象是沒有相應(yīng)的 JavaScript 類型的。他們是 JavaScript 虛擬機(jī)的對(duì)象系統(tǒng)的一部分。V8 將大多數(shù)內(nèi)部對(duì)象分配到和用戶 JS 對(duì)象相同的堆中,所以這些都只是 V8 內(nèi)部?jī)?nèi)容。

找到特定對(duì)象

要在堆中找到某個(gè)對(duì)象,你可以使用 Ctrl + F 來打開搜索框,然后輸入對(duì)象的 ID。

視圖的詳細(xì)內(nèi)容

總結(jié)視圖

最開始的時(shí)候,快照是在總結(jié)視圖中打開的,顯示了對(duì)象的整體情況,并且該視圖可以展開以顯示實(shí)例信息:

image

頂級(jí)入口是 "total" 行,他們展示了:

  • 構(gòu)造器,表示所有用這個(gè)構(gòu)造器創(chuàng)建的對(duì)象。
  • 對(duì)象實(shí)例的數(shù)量顯示在 # 這一列下。
  • Shallow size 這一列顯示了當(dāng)前構(gòu)造器創(chuàng)建的所有對(duì)象的 shallow size 總和。
  • Retained size 這一列顯示相同的對(duì)象集所對(duì)應(yīng)的最大 retained size。
  • Distance 顯示了從根節(jié)點(diǎn)開始,從節(jié)點(diǎn)的最短路徑到達(dá)當(dāng)前節(jié)點(diǎn)的距離。

想上圖那樣展開 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)存垃圾的),你可以嘗試下列步驟:

  • 在執(zhí)行操作前先生成一份快照。
  • 執(zhí)行操作(該操作涉及到你認(rèn)為出現(xiàn)內(nèi)存泄露的頁(yè)面)。
  • 執(zhí)行一個(gè)相對(duì)的操作(做出相反的交互行為,并重復(fù)多次)。
  • 生成第二份快照然后將視圖切換到比較視圖,將它與第一份快照對(duì)比。

在比較視圖中,兩份快照間的不同之處會(huì)展示出來。當(dāng)展開一個(gè)總?cè)肟跁r(shí),添加以及刪除的對(duì)象實(shí)例會(huì)顯示出來:

image

示例:嘗試這個(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):

  • DOMWindow objects - 這些是被認(rèn)作“全局”對(duì)象的對(duì)象。
  • GC roots - 虛擬機(jī)垃圾回收器實(shí)際實(shí)用的垃圾回收根節(jié)點(diǎn)。
  • Native objects - 指的是“推送”到 JavaScript 虛擬機(jī)內(nèi)以實(shí)現(xiàn)自動(dòng)化的瀏覽器對(duì)象,比如,DOM 節(jié)點(diǎn),CSS 規(guī)則(詳細(xì)內(nèi)容請(qǐng)見下一節(jié))

下面是常見的包含視圖的例子:

image

示例:通過這個(gè)頁(yè)面(在新的選項(xiàng)卡中打開)來嘗試如何在該視圖中找到閉包和事件處理器。

關(guān)于閉包的小提示

為函數(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;
}

domleaks

示例:嘗試一下這個(gè)例子來分析閉包對(duì)內(nèi)存的影響。你可能會(huì)對(duì)下面這個(gè)例子感興趣,它可以讓你深入了解堆內(nèi)存分配

發(fā)現(xiàn) DOM 內(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 下的整棵樹才能被回收。

treegc

示例:嘗試這個(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):

image

示例:嘗試這個(gè)示例(在新選項(xiàng)卡中打開)來體驗(yàn)分離的 DOM 樹。

主導(dǎo)視圖

主導(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)視圖了。

image

示例:嘗試這個(gè)例子(在新選項(xiàng)卡中打開)來看看你能不能找到積累點(diǎn)。隨后可以嘗試運(yùn)行 retainning paths and dominators。

對(duì)象分配追蹤器

對(duì)象追蹤器結(jié)合了堆分析器中快照的詳細(xì)信息以及時(shí)間軸的增量更新以及追蹤信息。跟這些工具相似,追蹤對(duì)象堆的分配過程包括開始記錄,執(zhí)行一系列操作,以及停止記錄并分析。

對(duì)象分析器在記錄中周期性生成快照(大概每 50 毫秒就會(huì)生成一次),并且在記錄最后停止時(shí)也會(huì)生成一份快照。堆分配配置文件顯示了對(duì)象在哪里創(chuàng)建并且標(biāo)識(shí)出了保留路徑。

image

開啟并使用對(duì)象追蹤器

要開始使用對(duì)象追蹤器:

  1. 確認(rèn)你安裝了最新的 Chrome Canary。
  2. 打開 DevTools 并點(diǎn)擊右邊下面的齒輪圖標(biāo)。
  3. 現(xiàn)在,在配置面板中,你可以看見一項(xiàng)名為 "Record Heap Allocations" 的配置。

image

頂欄的條形圖表示對(duì)象什么時(shí)候在堆中被找到。每個(gè)條形圖的高度對(duì)應(yīng)最近分配的對(duì)象的大小,而其顏色則說明這些對(duì)象在最后的快照中是否還處于生存周期:藍(lán)色表示在時(shí)間軸的最后該對(duì)象依舊存在,灰色則說明對(duì)象在時(shí)間軸內(nèi)被分配,但是已經(jīng)被垃圾回收器回收了。

collected

在上面的例子中,一個(gè)操作被執(zhí)行了10次。這個(gè)簡(jiǎn)單的程序加載了五個(gè)對(duì)象,所以顯示了五個(gè)藍(lán)色的條形圖案。但是最左邊的條形圖表明了一個(gè)潛在的問題。接下來你可以使用時(shí)間軸中的滑動(dòng)條來放大這一特定的快照,然后查看最近被分配到這一點(diǎn)上的對(duì)象。

image

點(diǎn)擊堆中的某個(gè)特定對(duì)象會(huì)在堆快照的頂部顯示其保留樹。檢查對(duì)象的保留路徑會(huì)讓你明白為什么對(duì)象沒有被回收,并且你可以在代碼中做出變動(dòng)來一出不需要的引用。

內(nèi)存分析的問題

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)是由什么組成的?

許多東西:

  • 內(nèi)置的對(duì)象映射
  • 符號(hào)表
  • 虛擬機(jī)線程棧
  • 編譯緩存
  • 處理范圍
  • 全局句柄

image

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)生大量的垃圾。

image

你需要在意的是,這條曲線陡度的增加速率。在內(nèi)存視圖中,還有DOM 節(jié)點(diǎn)計(jì)數(shù)器,文檔計(jì)數(shù)器以及事件監(jiān)聽計(jì)數(shù)器,這些在診斷中都是非常有用的。DOM 節(jié)點(diǎn)使用原生內(nèi)存,并且不會(huì)直接影響到 JavaScript 內(nèi)存圖表。

image

如果你感覺程序中出現(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 樹被回收。

image

黃色的節(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)畫:

detached-node

例子:嘗試這個(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)。

image

如果某個(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 的距離)。

image

一般來說,保留的對(duì)象中,有著最短距離的通常是最有可能造成內(nèi)存泄漏的。

Q:總結(jié),比較,主導(dǎo)和包含視圖都有哪些不同?

屏幕的底端可以選擇不同的數(shù)據(jù)視圖以實(shí)現(xiàn)不同的作用。

image

  • 總結(jié)視圖可以幫助你在基于構(gòu)造器名稱分組的狀態(tài)下尋找對(duì)象(它們的內(nèi)存使用狀況)。這個(gè)視圖對(duì)于追蹤 DOM 泄漏非常有用。
  • 比較視圖通過顯示對(duì)象是否被垃圾回收器清理了來幫助你追蹤內(nèi)存泄露。一般用于記錄并比較某個(gè)操作前后的兩個(gè)(或更多)內(nèi)存快照。具體的做法就是,檢查釋放內(nèi)存以及引用計(jì)數(shù)的增量來讓你確認(rèn)內(nèi)存泄露是否存在并找出其原因。
  • 包含視圖提供了關(guān)于對(duì)象結(jié)構(gòu)的一個(gè)良好的視角,讓我們可以分析在全局命名空間(比如 window)下的對(duì)象引用情況,以此來找出是什么讓它們保留下來了。這樣就可以從比較低的層次來分析閉包并深入對(duì)象內(nèi)部。
  • 主導(dǎo)視圖幫助我們確認(rèn)是否有意料外的對(duì)象引用依舊存在(它們應(yīng)該是有序地包含著的)以及垃圾回收確實(shí)處于運(yùn)行狀態(tài)。

Q:在堆分析器中不同的構(gòu)建器入口對(duì)應(yīng)什么功能?

  • (global property) - 在全局對(duì)象(就像是 window)和其引用的對(duì)象之間的中間對(duì)象。如果一個(gè)對(duì)象是用名為 Person 的構(gòu)造器創(chuàng)建的并且被一個(gè)全局對(duì)象持有,那么保留路徑看起來就是這樣的:[global] > (global property) > Person。這和對(duì)象直接引用其他對(duì)象的情況相反,但是我們引入中間對(duì)象是有著原因的。全局對(duì)象會(huì)周期性修改并且對(duì)于非全局對(duì)象訪問的優(yōu)化是個(gè)好方法,并且這個(gè)優(yōu)化不會(huì)對(duì)全局對(duì)象生效。
  • (roots) - 保留樹視圖中的根節(jié)點(diǎn)入口是指含有對(duì)選中對(duì)象的引用的入口。這些也可以是引擎處于其自身目的而創(chuàng)建的。引擎緩存了引用對(duì)象,但是這些引用全部都是弱類型的,因此它們不會(huì)阻止其他對(duì)象被回收。
  • (closure) - 通過函數(shù)閉包引用的一組對(duì)象的總數(shù)。
  • (array,string,number,regexp) - 引用了數(shù)組,字符串,數(shù)字或者常規(guī)表達(dá)式的對(duì)象屬性列表。
  • (compiled code) - 簡(jiǎn)單點(diǎn)說,所有事情都和編譯后的代碼相關(guān)。腳本類似于一個(gè)函數(shù)但是要和 <script> 標(biāo)簽對(duì)應(yīng)。SharedFunctionInfos(SFI)是在函數(shù)和編譯后的代碼之間的對(duì)象。函數(shù)通常會(huì)有上下文,而 SFI 則沒有。
  • HTMLDivElement,HTMLAnchorElement,DocumentFragment - 被你的代碼引用的特定類型的元素或者文檔對(duì)象的引用。

其他的很多對(duì)象在你看來就像是在你代碼的生存期內(nèi)產(chǎn)生的,這些對(duì)象可能包含了事件監(jiān)聽器以及特定對(duì)象,就像是下面這樣:

image

Q:在 Chrome 中為了不影響到我的圖表有什么功能是應(yīng)該關(guān)閉的嗎?

在 Chrome DevTools 中使用設(shè)置的時(shí)候,推薦在化名模式下并關(guān)閉所有擴(kuò)展功能或者直接通過特定用戶數(shù)據(jù)目錄來啟動(dòng) Chrome(--user-data-dir="")。

image

如果希望圖表盡可能的精確的話,那么應(yīng)用,擴(kuò)展插件甚至是控制臺(tái)日志都可能隱式地影響到你的圖表。

結(jié)束語(yǔ)

今天的 JavaScript 引擎在多種情況下都可以自動(dòng)清理代碼中產(chǎn)生的垃圾。也就是說,它們只能做到這里了,而我們的代碼中仍然會(huì)由于邏輯問題出現(xiàn)內(nèi)存泄露。請(qǐng)運(yùn)用這些工具來找出你的瓶頸,并記住,不要去猜測(cè)它,而是去測(cè)試。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)