Chrome開發者工具之JavaScript記憶體分析

edithfang發表於2015-03-15
儘管JavaScript使用垃圾回收進行自動記憶體管理,但有效的(effective)記憶體管理依然很重要。在這篇文章中我們將探討分析JavaScript web應用中的記憶體問題。在學習有關特性時請確保嘗試一下相關案例以提高你對這些工具在實踐中如何工作的認識。請閱讀記憶體 101(Memory 101)頁面來幫助你熟悉這篇文章中用到的術語。注意:我們將要用到的某些特性目前僅對Chrome Canary版瀏覽器可用。我們推薦使用這個版本來獲得最佳的工具,以分析你的應用程式的記憶體問題。

你需要思考的問題

總體來說,當你覺得你遇到了記憶體洩漏問題時,你需要思考三個問題:




術語和基本概念

本小節介紹在記憶體分析時使用的常用術語,這些術語在為其它語言做記憶體分析的工具中也適用。這裡的術語和概念用在了堆分析儀(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包裝物件引用。

連線字串是由一對字串合併成的物件,是合併後的結果。連線字串只在有需要時合併。像一連線字串的子字串需要被構建時。

比如:如果你連線ab,你得到字串(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)。
  • HTMLDivElementHTMLAnchorElementDocumentFragment 等 – 你程式碼中對elements或document物件的引用。


在你的程式的生命週期中生成的很多其它的物件,包括事件監聽器或自定義物件,可以在下面的controllers中找到:


問:我在做記憶體分析時需要關閉Chrome裡可能會產生影響的什麼功能麼?

我們建議在用Chrome DevTools做記憶體分析時,你可以使用關閉所有擴充套件功能的隱身模式,或設定使用者資料夾為(--user-data-dir="")後再開啟Chrome。



應用,擴充套件甚至console中的記錄都會對你的分析有潛在的影響,如果你想讓你的分析可靠的話,禁用這些吧。

寫在最後的話

今天的JavaScript引擎已經具有很強的能力,能夠自動回收程式碼產生的記憶體垃圾。就是說,它們只能做到這樣了,但我們的應用仍然被證明會因為邏輯錯誤而產生記憶體洩漏。使用相應的工具來找到應用的瓶頸,記住,不要靠猜 – 測試它。

幫助例項

診斷記憶體洩漏

儘管很多內容在本文章中已經提到了,但一系列測試記憶體相關的問題的例子還是很有用的,下面是一組DOM節點記憶體洩漏的例子。你可能希望在測試你的更復雜的頁面或應用前先用這些例子做試驗。

評論(1)

相關文章