本節說明記憶體分析中使用的常見術語,適用於各種語言的各種記憶體分析工具。
本文所述的術語和概念均是指 Chrome 開發人員工具堆積分析器。如果您曾使用過 Java、.NET 或其他記憶體分析器,這篇文章可能會讓您回顧相關知識。
物件大小
請將記憶體視為包含原始類型 (例如數字和字串) 和物件 (關聯陣列) 的圖表。這可能會以圖表呈現,其中包含多個相互連結的點,如下所示:
物件可透過兩種方式儲存記憶體:
- 直接由物件本身。
- 隱含地保留其他物件的參照,因此可防止垃圾收集器 (簡稱 GC) 自動處置這些物件。
在 DevTools 中使用堆積分析器 (用於調查「記憶體」面板中所發現的記憶體問題的工具) 時,您可能會看到幾個不同的資訊欄。其中兩個值得注意的值是「Shallow Size」和「Retained Size」,但這兩個值代表什麼?
淺層大小
這是物件本身所保留的記憶體大小。
一般 JavaScript 物件會保留一些記憶體,用於說明和儲存即時值。通常只有陣列和字串可以有顯著的淺層大小。不過,字串和外部陣列通常會將主要儲存空間放在轉譯器記憶體中,只在 JavaScript 堆疊上公開一個小型包裝函式物件。
轉譯器記憶體是轉譯檢查頁面的程序所有記憶體:原生記憶體 + 頁面的 JS 堆積記憶體 + 頁面啟動的所有專屬工作站的 JS 堆積記憶體。不過,即使是小型物件,也可以間接保留大量記憶體,因為這樣可防止自動垃圾收集程序處理其他物件。
保留大小
這是物件本身和其無法從 GC 根存取的依附物件遭到刪除後,釋出的記憶體大小。
GC 根是由句柄組成,這些句柄是在從原生程式碼參照 V8 以外的 JavaScript 物件時建立 (本機或全域)。所有這類句柄都可以在堆積區快照的 GC 根 > 句柄範圍 和 GC 根 > 全域句柄 下方找到。在本說明文件中說明句柄,而不深入探討瀏覽器實作方式的細節,可能會造成混淆。GC 根目錄和句柄都不是您需要擔心的東西。
內部 GC 根目錄很多,但大多數都不是使用者感興趣的內容。從應用程式角度來看,根有以下幾種:
- Window 全域物件 (位於每個 iframe 中)。堆積集快照中含有距離欄位,這是從視窗中找到最短保留路徑的屬性參照數量。
- 文件 DOM 樹狀結構,包含可透過遍歷文件存取的所有原生 DOM 節點。並非所有元素都會有 JS 包裝函式,但如果有,則包裝函式會在文件運作期間保持運作。
- 有時物件可能會由偵錯工具背景和開發人員工具控制台保留 (例如在控制台評估後)。在偵錯工具中,使用清除主控台且沒有有效中斷點的堆積快照。
記憶體圖表會從根節點開始,該節點可能是瀏覽器的 window
物件,或 Node.js 模組的 Global
物件。您無法控制這個根物件的 GC 方式。
從根目錄無法存取的任何項目都會遭到 GC。
物件保留樹狀結構
堆積是連結在一起的物件網路。在數學領域中,這種結構稱為「圖表」或「記憶體圖表」。圖表是由節點和邊緣所組成,兩者都會指定標籤。
- 節點 (或物件) 會使用用於建構節點的建構函式名稱進行標記。
- 邊會使用屬性的名稱標示。
瞭解如何使用堆積分析工具記錄設定檔。在下列堆積分析器記錄中,我們可以看到一些引人注目的內容,包括距離:距離 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 物件表示法
原始類型有三種:
- 數字 (例如3.14159..)
- 布林值 (true 或 false)
- 字串 (例如「Werner Heisenberg」)
這些節點無法參照其他值,且一律為葉節點或終端節點。
數字可以儲存為下列格式:
- 立即 31 位元整數值,稱為「小整數」 (SMIs),或
- 堆積物件,稱為堆積數。堆積數字可用於儲存不符合 SMI 格式的值,例如雙精度數,或是當值需要封箱時,例如設定屬性。
字串可以儲存在下列任一位置:
- VM 堆積,或
- 在 轉譯器的記憶體中外部儲存。系統會建立包裝函式物件,並用於存取外部儲存空間,例如從網路接收的腳本來源和其他內容,而非複製到 VM 堆積上。
系統會從專屬的 JavaScript 堆積 (或VM 堆積) 配置新 JavaScript 物件的記憶體。這些物件由 V8 的垃圾收集器管理,因此只要有至少一個強烈參照,這些物件就會持續運作。
原生物件是指不在 JavaScript 堆積中的所有物件。與堆積物件相反,原生物件在整個生命週期中不會受到 V8 垃圾收集器的管理,而且只能透過 JavaScript 使用其 JavaScript 包裝函式物件存取。
Cons 字串是組合後儲存的字串組合物件,也是連接的結果。cons string 內容的彙整作業只會在需要時執行。舉例來說,如果需要建構已彙整字串的子字串,就會發生這種情況。
舉例來說,如果您連接 a 和 b,您會得到字串 (a, b),代表連接的結果。如果您稍後將 d 連結至該結果,您會得到另一個連結字串 ((a, b), d)。
陣列:陣列是含有數字鍵的物件。這些類別廣泛用於 V8 VM 中,用於儲存大量資料。用於字典的鍵/值組合集會由陣列備份。
一般 JavaScript 物件可分為以下兩種陣列類型,用於儲存資料:
- 命名屬性,以及
- 數字元素
如果屬性數量很少,則可將這些屬性儲存在 JavaScript 物件本身的內部。
Map:用於說明物件類型及其版面配置的物件。舉例來說,圖可用於描述隱含物件階層,以便快速存取屬性。
物件群組
每個原生物件群組都由彼此持有彼此參照的物件組成。舉例來說,DOM 子樹狀結構中的每個節點都有一個連結至其父項的連結,以及連結至下一個子項和下一個同胞節點,因此形成連結圖表。請注意,原生物件不會顯示在 JavaScript 堆積中,因此大小為零。而是建立包裝函式物件。
每個包裝函式物件都會保留對應原生物件的參照,以便將指令重新導向至該物件。在自身的輪替中,物件群組會保留包裝函式物件。不過,這不會造成無法收集的循環,因為 GC 會聰明地釋放已不再參照包裝函式的物件群組。但如果忘記釋放單一包裝函式,整個群組和相關的包裝函式都會保留。