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

2018-03-01 18:50 更新

JavaScript 內(nèi)存分析

內(nèi)存泄露是指計算機內(nèi)存逐漸丟失。當某個程序總是無法釋放內(nèi)存時,就會出現(xiàn)內(nèi)存泄露。JavaScript web 應(yīng)用程序可能會經(jīng)常遇到類似于本地程序中內(nèi)存泄露這樣的問題,比如泄露和膨脹,但是 JavaScript 有內(nèi)存回收機制可以解決此類問題。

盡管 JavaScript 使用了內(nèi)存回收機來自動管理內(nèi)存,高效的內(nèi)存管理策略依然是相當重要的。在本章中我們會詳細說明 JavaScript web 應(yīng)用程序中的內(nèi)存問題。在學(xué)習(xí)某些特性的時候請嘗試這些示例,這可以增進你對于工具運行原理的認識。

在開始之前,請查看 Memory 101 頁面來熟悉一下相關(guān)的專業(yè)術(shù)語。

注意:我們在后面使用的有些特性是只有 Chrome Canary 才支持的。我們建議使用此版本的工具,這樣您就可以對您的應(yīng)用程序做出最佳的內(nèi)存分析。

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

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

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

image_0

視圖內(nèi)容

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

這個部分介紹了內(nèi)存分析中常見的術(shù)語,即使是在其他語言的內(nèi)存分析工具中,這些術(shù)語也同樣有用。這里所說的術(shù)語和概念是用于堆探查器界面以及相應(yīng)文檔中的。

了解這些術(shù)語后,你們就能更加高效地使用這個工具。如果你曾經(jīng)使用 Java、.Net 或者其它內(nèi)存分析器,那么該篇的內(nèi)容對你而言就是一次提升。

對象的大小

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

thinkgraph

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

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

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

images

Shallow size

這是指對象本身獲得的內(nèi)存大小。

典型的 JavaScript 對象會獲得一些保留的內(nèi)存,用于他們的描述以及存儲即時產(chǎn)生的值。通常情況下,只有數(shù)組和字符串才會有比較明顯的淺層大小。不過,字符串和外部數(shù)組往往在渲染內(nèi)存中有它們自己的主存儲器,對 JavaScript 堆只露出一點包裝后的對象。

渲染內(nèi)存是指所監(jiān)視的頁面被渲染的過程中使用的內(nèi)存:原本分配的內(nèi)存 + 該頁面在 JS 堆中的內(nèi)存 + 所有因為該頁面而導(dǎo)致的 JS 堆中其他對象的內(nèi)存開銷。然而,即使是一個小的對象也可以通過阻止垃圾回收器自動回收其他對象來間接保有大量的內(nèi)存。

Retained size

這是指對象以及其相關(guān)的對象一起被刪除后所釋放的內(nèi)存大小,并且 GC roots 無法到達該處。

GC roots 是由在從原生代碼的 V8 之外引用 JavaScript 對象的時候所創(chuàng)建的句柄(局部或者全局的)構(gòu)成的。這些句柄可以再堆的快照中 GC roots > Handle scope 以及 GC roots > Global handles 中找到。在沒有談及瀏覽器實現(xiàn)的細節(jié)的情況下,就在本文中說明句柄會令讀者感到困惑,故而關(guān)于句柄的細節(jié)本文不做講解。事實上,無論 GC roots 還是句柄,都不是你需要擔(dān)心的東西。

內(nèi)部的 GC roots 有很多,不過用戶對其中的大部分都不感興趣。從應(yīng)用程序的角度來說,有下面這么幾種 roots:

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

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

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

dontcontrol

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

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

對象的保留樹

就像我們前面所說的,堆就是由相互連接的對象構(gòu)成的網(wǎng)絡(luò)。在數(shù)學(xué)的世界中,這種結(jié)構(gòu)稱作或者內(nèi)存圖。一個圖是由節(jié)點和邊構(gòu)成的,而節(jié)點又是由邊連接起來的,其中節(jié)點和邊都有相應(yīng)的標簽。

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

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

images

主導(dǎo)者

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

dominatorsspanning

在上面的圖中:

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

在下面的例子中,節(jié)點 #3#10 的主導(dǎo)者,但是 #7 節(jié)點也在由 GC 到 #10 節(jié)點的,每條簡單路徑上。因此,如果對象 B 存在于從根節(jié)點到對象 A 的,每條簡單路徑上,那么對象 B 就是對象 A 的主導(dǎo)者。

dominator

V8 的細節(jié)

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

JavaScript 對象的表示

JavaScript 中有三種主要類型:

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

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

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

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

字符串可以被存儲在:

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

新的 JavaScript 對象的內(nèi)存是由特定的 JavaScript 堆(或者說 VM 堆)分配的。這些對象由 V8 垃圾回收器管理,并且只要存在一個對他們的強引用就不會被回收。

本地對象指的是不在 JavaScript 堆中存儲的一切對象。本地對象和堆對象相反,其生存周期不由 V8 垃圾回收器管理,并且只能通過封裝它們的 JavaScript 對象來使用。

Cons string 是一個保存了成對字符串的對象,并且該對象會將字符串拼接起來,最后的結(jié)果是串聯(lián)后的字符串。拼接后的 cons string 的內(nèi)容只有在需要的時候才會出現(xiàn)。一個比較好的例子就是,如果想獲取某個字符串的子串,就必須利用函數(shù)進行構(gòu)建。

舉個例子,如果你將 ab 對象串聯(lián),那么你將獲得一個字符串(a,b) 用于表示拼接后的結(jié)果。如果你之后又加入了一個對象 d,那么你將活的另一個字符串((a,b),d)。

數(shù)組 - 一個數(shù)組就是有著數(shù)字鍵的對象。他們廣泛應(yīng)用在 V8 VM 中,用于存儲大量數(shù)據(jù)。在字典這樣的數(shù)據(jù)結(jié)構(gòu)中鍵值對的集合就是利用數(shù)組來備份的。

一個典型的用于存儲的 JavaScript 對象可以是下列兩種數(shù)組類型之一:

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

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

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

對象組

每個本地的對象組都是由保持彼此相互引用的對象組成的。以一個 DOM 子樹為例,在該樹中,每一個節(jié)點都一個指向父節(jié)點的連接,以及指向孩子節(jié)點和兄弟節(jié)點的鏈接,由此,所有的節(jié)點連成了一張圖。需要注意的是,本地對象并不會在 JavaScript 堆中出席那,所以它們的大小是 0。相應(yīng)的,對于每個要使用本地對象都會創(chuàng)建一個對應(yīng)的封裝對象。

每個封裝對象都含有一個對相應(yīng)的本地對象的引用,這是為了能夠?qū)⒚钪囟ㄏ虻奖镜貙ο笊?。而對象組則含有這些封裝的對象,但是,這并不會造成一個無法回收的死循環(huán),因為垃圾回收器會自動釋放不在引用的封裝對象。但是一旦忘記了釋放某個封裝對象就可能造成整個組以及相關(guān)封裝對象都無法被釋放。

先決條件以及一些有用的提示

Chrome 任務(wù)管理器

注意:在 Chrome 中分析內(nèi)存問題時,一個比較好的方法就是配置 clean-room testing 環(huán)境。

如果某個頁面消耗了大量內(nèi)存,可以在執(zhí)行有可能占用大量內(nèi)存的活動時使用 Chrome 任務(wù)管理器的內(nèi)存這一欄來監(jiān)視頁面所占用的內(nèi)存。如果要使用任務(wù)管理器,點擊 menu > Tools 或者使用快捷鍵 Shift + Esc。

image

打開之后,右鍵點擊列頭部分然后啟用 JavaScript memory 列。

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

要解決問題的第一步就是要先擁有找出問題的能力。這意味著能夠創(chuàng)建一個用于基本問題測量的可重復(fù)性測試。如果沒有一個可復(fù)用的程序,你就沒辦法有效地衡量問題。另外,如果連測試基線都沒有的話,就沒辦法知道做出的改變是否提高了程序的性能。

時間軸面板對于發(fā)現(xiàn)問題出現(xiàn)的時間非常有幫助。頁面或者應(yīng)用程序加載或者進行交互時,它會給出整個流程的時間消耗的完整概述。所有的事件,從加載資源到解析 JavaScript、計算樣式、垃圾回收以及重繪都會出現(xiàn)在時間軸上。

在尋找內(nèi)存問題的時候,時間軸面板的 Memory view 可以用來追溯:

  • 總共分配的內(nèi)存 - 內(nèi)存的使用量是否增長了?
  • DOM 節(jié)點的數(shù)量。
  • 文檔的數(shù)量
  • 分配的事件監(jiān)聽器的數(shù)量。

image

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

驗證存在的問題

首先要做的事情就是找出你認為可能造成內(nèi)存泄露的活動。這種活動可能是任何事情,就像是在站點上進行定位、鼠標的懸停事件、點擊事件或者是與頁面交互時可能對性能產(chǎn)生消極影響的事件。

在時間軸面板中,開始記錄(Ctrl + E 或者 Cmd + E)然后執(zhí)行你想測試的活動序列。要強制進行垃圾回收,點擊底部的垃圾圖標(collect-garbage)。

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

nodescollect

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

image

一旦你確認了存在問題,你就可以借助 Profiles panel 中的 heap profiler 找出問題的來源。

示例:你可以嘗試一下這個例子來鍛煉一下如何高效使用時間軸內(nèi)存模式。

垃圾回收

垃圾回收器(就像是 V8)能夠定位到你的程序處于生存期的對象以及已經(jīng)死亡的對象,甚至是無法訪問到的對象。

如果垃圾回收器(GC)由于某些邏輯錯誤沒能回收你的 javaScript 中已死亡的對象,那么它們所消耗的內(nèi)存將無法被再次使用。像這樣的情況最終會隨著時間推移而使得你的應(yīng)用程序的執(zhí)行速率不斷變慢。

如果你在編寫代碼時,即使是不再需要的變量以及事件監(jiān)聽器依舊被其他代碼所引用,最終就會出現(xiàn)這種情況。當這些引用存在的時候,垃圾回收器就沒辦法正確清理這些對象。

在你的應(yīng)用程序的生存期間會有一些 DOM 元素更新/死亡,別忘了檢出并消除引用了這些元素的變量。檢查可能引用了其他對象(或者其他 DOM 元素)的對象的屬性,并留意可能隨著時間的推移不斷增長的變量緩存。

堆分析器

生成快照

在配置面板中,選擇 Take Heap Snapshot,然后點擊 Start 或者使用 Cmd + ECtrl + E 快捷鍵。

image

最初快照是存在渲染內(nèi)存中的,當你點擊快照圖標來查看它的時候,它將會被傳輸?shù)?DevTools 中。當快照載入到 DevTools 中并被解析后,快照標題下面會出現(xiàn)一個數(shù)字,該數(shù)字表示所有可訪問的 JavaScript 對象的總大?。?/p>

image

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

清除快照

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

image

注意:直接關(guān)閉 DevTools 窗口并不會刪除渲染內(nèi)存中的配置文件。當重新打開 DevTools 窗口的時候,所有之前生成的快照都會在快照列表中出現(xiàn)。

記得之前文章中提到過,你可以從 DevTools 中強制進行垃圾回收,并且這可以成為你的快照工作流中的一部分。當生成一個堆快照的時候,DevTools 會自動進行垃圾回收。在時間軸中該過程可以通過點擊垃圾桶按鈕(collect-garbage)輕松實現(xiàn)。

force

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

在快照視圖間切換

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

image

一共有三種默認視圖:

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

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

查看代碼顏色

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

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

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

找到特定對象

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

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

總結(jié)視圖

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

image

頂級入口是 "total" 行,他們展示了:

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

想上圖那樣展開 total line 之后,其所有的實例都會顯示出來。對于每個實例,它的 shallow size 和 retained size 都會在相應(yīng)列中展示出來。在 @ 字符后面的數(shù)字就是對象的 ID,該 ID 允許你在每個對象的基礎(chǔ)上比較堆的快照。

示例:通過這個頁面來了解如何使用總結(jié)視圖。

請記住,黃色的對象表示有 JavaScript 對象引用了它們,而紅色的對象是指從一個黃色背景節(jié)點引用的分離節(jié)點。

比較視圖

這個視圖用于比較不同的快照,這樣,你就可以通過比較它們的不同之處來找出出現(xiàn)內(nèi)存泄露的對象。想要弄清楚一個特定的程序是否造成了泄露(比如,通常是相對的兩個操作,就像是打開文檔,然后關(guān)閉它,是不會留下內(nèi)存垃圾的),你可以嘗試下列步驟:

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

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

image

示例:嘗試這個例子(在選項卡中打開)來了解如何使用比較視圖來監(jiān)測內(nèi)存泄露。

包含視圖

包含視圖本質(zhì)上就像是你的應(yīng)用程序?qū)ο蠼Y(jié)構(gòu)的俯視圖。它使你能夠查看到函數(shù)閉包內(nèi)部,甚至是觀察到那些組成 JavaScript 對象的虛擬機內(nèi)部對象,借助該視圖,你可以了解到你的應(yīng)用底層占用了多少內(nèi)存。

這個視圖提供了多個接入點:

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

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

image

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

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

為函數(shù)命名有助于你在快照中分辨不同的閉包。舉個例子,下面這個函數(shù)沒有命名:

function createLargeClosure() {  
    var largeStr = new Array(1000000).join('x');
    var lC = function() { 
        // this is NOT a named function
        return largeStr;  
    };  
    return lC;
}

而下面這個是命名后的函數(shù):

function createLargeClosure() 
{  
    var largeStr = new Array(1000000).join('x');
    var lC = function lC() { 
        // this IS a named function    
        return largeStr;  
    };  return lC;
}

domleaks

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

發(fā)現(xiàn) DOM 內(nèi)存泄露

該工具的一大特點就是它能夠顯示瀏覽器本地對象(DOM 結(jié)點,CSS 規(guī)則)以及 JavaScript 對象間的雙向依賴關(guān)系。這有助于發(fā)現(xiàn)因為忘記分離 DOM 子樹而導(dǎo)致的不可見的泄露。

DOM 泄露肯能比你想象中的要多??紤]下面這個例子 - 什么時候 #tree 會被回收?

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 包含了對其父親(父節(jié)點)的引用并遞歸到 #tree,所以只有當 leafRef 失效的時候 #tree 下的整棵樹才能被回收。

treegc

示例:嘗試這個例子有助于你理解 DOM 節(jié)點中哪里容易出現(xiàn)泄露以及如何找到它們。你也可以繼續(xù)嘗試后面這個例子DOM 泄露斌想象的要更多

想要了解更多關(guān)于 DOM 泄露以及內(nèi)存分析的基礎(chǔ)內(nèi)容,請參閱 Gonzalo Ruiz de Villa 編寫的 Finding and debugging memory leaks with the Chrome DevTools。

總結(jié)視圖和包含視圖更加容易找到本地對象 - 在視圖中有對應(yīng)本地對象的入口節(jié)點:

image

示例:嘗試這個示例(在新選項卡中打開)來體驗分離的 DOM 樹。

主導(dǎo)視圖

主導(dǎo)視圖顯示了堆圖的主導(dǎo)樹,從形式上來看,主導(dǎo)視圖有點像是包含視圖,但是缺少了某些屬性。這是因為主導(dǎo)者對象可能會缺少對它的直接引用,也就是說,主導(dǎo)樹不是生成樹。

注意:在 Chrome Canary 中,主導(dǎo)視圖可以在 Settings > Show advance snapshots properties 中啟用,重啟瀏覽器之后就可以選擇主導(dǎo)視圖了。

image

示例:嘗試這個例子(在新選項卡中打開)來看看你能不能找到積累點。隨后可以嘗試運行 retainning paths and dominators。

對象分配追蹤器

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

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

image

開啟并使用對象追蹤器

要開始使用對象追蹤器:

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

image

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

collected

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

image

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

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

Q:我并沒有看到對象的所有屬性,我也沒看到那些非字符串 的值,為什么?

不是所有的屬性都儲存在 JavaScript 堆中。其中有些是通過執(zhí)行了本地代碼的獲取器來實現(xiàn)的。這樣的屬性不會在堆快照中被捕獲,因為要避免調(diào)用獲取器的消耗并且要避免程序聲明的變化(當獲取器不是“純”方法的時候)。同樣的,非字符串值,像是數(shù)字等為了縮小快照的大小也沒有捕獲。

Q:在 *@* 字符后面的數(shù)字意味著什么 - 這是一個地址或者 ID 嗎?ID 的值是不是唯一的?

這是對象 ID。顯示對象的地址毫無意義,因為對象的地址在垃圾回收期間會發(fā)生偏移。這些對象 ID 是真正的 ID - 也就是說,他們在生存的多個快照都會存在,并且其值是唯一的。這就使得你可以精確地比較兩個不同時期的堆狀態(tài)。維護這些 ID 增加了垃圾回收周期的開銷,但是這只在第一份堆快照生成后才初始化 - 如果堆配置文件沒有使用到的話,就沒有開銷。

Q:“死亡”的(無法到達)對象是否會包含在快照中?

不會,只有可到達的對象才會在快照中出現(xiàn)。并且,生成一份快照的時候總是會先開始進行垃圾回收。

注意:在編寫代碼的時候,我們希望避免這種垃圾回收方式以減少在生成堆快照時,已使用的堆大小的變動。這個還在實現(xiàn)中,但是垃圾回收依舊會在快照之外執(zhí)行。

Q:GC 根節(jié)點是由什么組成的?

許多東西:

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

image

Q:教程中說使用堆分析器以及時間軸內(nèi)存視圖來查找內(nèi)存泄露。首先應(yīng)該使用什么工具呢?

時間軸,使用該工具可以在你意識到頁面開始變慢的時候檢測出過高的內(nèi)存使用量。速度變慢是典型的內(nèi)存泄露癥狀,當然也有可能是由其他情況造成的 - 也許你的頁面中有一些圖片或者是網(wǎng)絡(luò)存在瓶頸,所以要確認你是否修復(fù)了實際的問題。

要診斷內(nèi)存是不是造成問題的原因,打開時間軸面板的內(nèi)存視圖。點擊紀錄按鈕然后開始與程序交互,重復(fù)你覺得出現(xiàn)問題的操作。停止記錄,顯示出來的圖片表示分配給應(yīng)用程序的內(nèi)存狀態(tài)。如果圖片顯示消耗的內(nèi)存總量一直在增長(繼續(xù)沒有下落)則說明很有可能出現(xiàn)了內(nèi)存泄露。

一個正常的應(yīng)用,其內(nèi)存狀態(tài)圖應(yīng)該是一個鋸齒形的曲線圖,因為內(nèi)存分配后會被垃圾回收器回收。這一點是毋庸置疑的 - 在 JavaScript 中的操作總會有所消耗,即使是一個空的 requestAnimationFrame 也會出現(xiàn)鋸齒形的圖案,這是無法避免的。只要確保沒有尖銳的圖形,就像是大量分配這樣的情況就好,因為這意味著在另一側(cè)會產(chǎn)生大量的垃圾。

image

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

image

如果你感覺程序中出現(xiàn)了內(nèi)存泄露,堆分析器可以幫助你找到內(nèi)存泄露的來源。

Q:我注意到在堆快照中有一些 DOM 節(jié)點,其中有些是紅色的并且表明是 “分離的 DOM 樹” 而其他的是黃色的,這意味著什么?

你會注意到這些節(jié)點有著不同的顏色,紅色的節(jié)點(其背景較暗)沒有 JavaScript 對其的直接引用,但是依舊處于生存期,因為他們是分離的 DOM 樹的一部分??赡軙幸恍┕?jié)點在 JavaScript 引用的樹中(可能是閉包或者變量)但是卻剛好阻止了整棵 DOM 樹被回收。

image

黃色的節(jié)點(其背景也是黃色的)則是有 JavaScript 對象直接引用的。在同一個分離 DOM 樹中查找黃色節(jié)點來鎖定 JavaScript 中的引用。從 DOM 窗口到達相關(guān)元素應(yīng)該是一條屬性鏈(比如,window.foo.bar[2].baz

下面是關(guān)于獨立節(jié)點在整幅圖中位置的一個動畫:

detached-node

例子:嘗試這個關(guān)于獨立節(jié)點例子,通過這個例子你可以看到節(jié)點在時間軸中的變化過程,并且你可以生成堆快照來找到獨立節(jié)點。

Q:Shallow 以及 Retained Size 表示什么?它們之間有什么區(qū)別?

實際上,對象在內(nèi)存中的停留是有兩種方式的 - 通過一個其他處于生存期的對象直接保留在內(nèi)存中(比如 window 和 document 對象)或者通過保留對本地渲染內(nèi)存中某些部分的引用而隱式地保留在內(nèi)存中(就像 DOM 對象)。后者會導(dǎo)致相關(guān)的對象無法被內(nèi)存回收器自動回收,最終造成泄漏。而對象本身含有的內(nèi)存大小則是 shallow size(一般來說數(shù)組和字符串有著比較大的 shallow size)。

image

如果某個對象阻止了其他對象被回收,那么不管這個對象有多大,它所占用的內(nèi)存都將是巨大的。當一個對象被刪除時可以回收的內(nèi)存大小則被稱為保留量。

Q:在構(gòu)建器以及保留視圖中有大量的數(shù)據(jù)。如果我發(fā)現(xiàn)存在泄漏的時候,應(yīng)該從哪里開始找起?

一般來說從你的樹中保留的第一個對象開始找起是個好辦法,因為被保留的內(nèi)容是按照距離排序的(也就是到 window 的距離)。

image

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

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

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

image

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

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

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

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

image

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

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

image

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

結(jié)束語

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號