Register for this year’s #ChromeDevSummit happening on Nov. 11-12 in San Francisco to learn about the latest features and tools coming to the Web. Request an invite on the Chrome Dev Summit 2019 website

內存術語

本部分將介紹內存分析中的常用術語,適用於不同語言的各種內存分析工具。

此處介紹的術語和概念適用於 Chrome DevTools 堆分析器。如果您之前使用過 Java、.NET 或其他內存分析器,那麼本部分內容對您來說將是全新的。

對象大小

將內存視爲具有原語類型(如數字和字符串)和對象(關聯數組)的圖表。形象一點,可以將內存表示爲一個由多個互連的點組成的圖表,如下所示:

內存的直觀表示

對象可通過以下兩種方式佔用內存:

  • 直接通過對象自身佔用。

  • 通過保持對其他對象的引用隱式佔用,這種方式可以阻止這些對象被垃圾回收器(簡稱 GC)自動處置。

使用 DevTools 中的堆分析器(一種用於檢查在“Profiles”下發現的內存問題的工具)時,您會看到多個信息列。Shallow SizeRetained Size 這兩個列比較引人注目,但它們表示什麼呢?

淺層大小和保留大小

淺層大小

這是對象自身佔用內存的大小。

典型的 JavaScript 對象會將一些內存用於自身的說明和保存中間值。通常,只有數組和字符串會有明顯的淺層大小。不過,字符串和外部數組的主存儲一般位於渲染器內存中,僅將一個小包裝器對象置於 JavaScript 堆上。

渲染器內存是渲染檢查頁面的進程的內存總和:原生內存 + 頁面的 JS 堆內存 + 頁面啓動的所有專用工作線程的 JS 堆內存。儘管如此,即使一個小對象也可能通過阻止其他對象被自動垃圾回收進程處理的方式間接地佔用大量內存。

保留大小

這是將對象本身連同其無法從 GC 根到達的相關對象一起刪除後釋放的內存大小。

GC 根句柄組成,這些句柄在從原生代碼引用 V8 外部的 JavaScript 對象時創建(本地或全局)。所有此類句柄都可以在 GC 根 > 句柄作用域GC 根 > 全局句柄下的堆快照內找到。本文檔對句柄的介紹沒有深入到瀏覽器實現的細節,可能讓您感到困惑。您不必擔心 GC 根和句柄。

存在很多內部 GC 根,其中的大部分都不需要用戶關注。從應用角度來看,存在以下種類的根:

  • Window 全局對象(位於每個 iframe 中)。堆快照中有一個距離字段,表示從 window 出發的最短保留路徑上的屬性引用數量。

  • 文檔 DOM 樹,由可以通過遍歷文檔到達的所有原生 DOM 節點組成。並不是所有的節點都有 JS 包裝器,不過,如果有包裝器,並且文檔處於活動狀態,包裝器也將處於活動狀態。

  • 有時,對象可能會被調試程序上下文和 DevTools 控制檯保留(例如,在控制檯評估後)。在調試程序中清除控制檯並移除活動斷點,創建堆快照。

內存圖從根開始,根可以是瀏覽器的 window 對象或 Node.js 模塊的 Global 對象。您無法控制此根對象的垃圾回收方式。

無法控制的對象

任何無法從根到達的對象都會被 GC 回收。

對象保留樹

堆是一個由互連的對象組成的網絡。在數學領域,此結構被稱爲“圖表”或內存圖。圖表由通過邊緣連接的節點組成,兩者都是給定標籤。

  • 節點或對象)使用構造函數(用於構建節點)的名稱進行標記。
  • 邊緣使用屬性的名稱進行標記。

瞭解如何使用堆分析器記錄分析。我們可以從下面的堆分析器記錄中看到一些引人注目的參數,例如距離:距離是指與 GC 根之間的距離。如果相同類型的幾乎所有對象的距離都相同,只有少數對象的距離偏大,則有必要進行調查。

距根的距離

支配項

支配對象由一個樹結構組成,因爲每個對象都有且僅有一個支配項。對象的支配項可能缺少對其所支配對象的直接應用;也就是說,支配項的樹不是圖表的生成樹。

在下面的圖表中:

  • 節點 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 位整型值(稱爲小整型 (SMI)),或
  • 堆對象,作爲堆數字引用。堆數字用於存儲不適合 SMI 格式的值(例如雙精度),或者在需要將值“包裝”起來時使用(例如在值上設置屬性)。

字符串可以存儲在以下位置:

  • VM 堆中,或
  • 渲染器內存中(外部)。將創建一個包裝器對象並用於訪問外部存儲空間,例如,外部存儲空間是存儲腳本源和從網頁接收(而不是複製到 VM 堆上)的其他內容的位置。

新 JavaScript 對象的內存分配自專用的 JavaScript 堆(或 VM 堆)。這些對象由 V8 的垃圾回收器管理,因此,只要存在一個對它們的強引用,它們就會一直保持活動狀態。

原生對象是 JavaScript 堆之外的任何對象。與堆對象相反,原生對象在其生命週期內不由 V8 垃圾回收器管理,並且只能使用其 JavaScript 包裝器對象從 JavaScript 訪問。

Cons 字符串是一種由存儲並聯接的成對字符串組成的對象,是串聯的結果。cons 字符串內容僅根據需要進行聯接。一個示例便是需要構造已聯接字符串的子字符串。

例如,如果您將 ab 串聯,您將獲得一個字符串 (a, b),它表示串聯結果。如果您稍後將 d 與該結果串聯,您將得到另一個 cons 字符串 ((a, b), d)。

數組 - 數組是一個具有數字鍵的對象。它們在 V8 VM 中廣泛使用,用於存儲大量數據。用作字典的成套鍵值對採用數組形式。

典型的 JavaScript 對象可以是兩個數組類型之一,用於存儲:

  • 命名屬性,以及
  • 數字元素

數字元素如果屬性數量非常少,可以將其存儲在 JavaScript 對象自身內部。

映射 - 一種用於說明對象種類及其佈局的對象。例如,可以使用映射說明用於快速屬性訪問的隱式對象層次結構。

對象組

每個原生對象組都由保持對彼此的相互引用的對象組成。例如,在 DOM 子樹中,每個節點都有一個指向其父級的鏈接,並鏈接到下一個子級和下一個同級,形成一個互連圖。請注意,原生對象不會在 JavaScript 堆中表示 - 這正是它們的大小爲什麼爲零的原因。相反,創建包裝器對象。

每個包裝器對象都會保持對相應原生對象的引用,用於將命令重定向到自身。這樣,對象組會保持包裝器對象。不過,這不會形成一個無法回收的循環,因爲 GC 非常智能,可以釋放包裝器對象不再被引用的對象組。但是,忘記釋放單個包裝器將保持整個組和關聯的包裝器。