JavaScript 堆內(nèi)存分析新工具 OneHeap

OneHeap 關(guān)注于運(yùn)行中的 JavaScript 內(nèi)存信息的展示,用可視化的方式還原了 HeapGraph,有助于理解 v8 內(nèi)存管理。

背景

JavaScript 運(yùn)行過程中的大部分?jǐn)?shù)據(jù)都保存在堆 (Heap) 中,所以 JavaScript 性能分析另一個(gè)比較重要的方面是內(nèi)存,也就是堆的分析。

利用 Chrome Dev Tools 可以生成應(yīng)用程序某個(gè)時(shí)刻的堆快照 (HeapSnapshot),它較完整地記錄了各種對(duì)象和引用的情況,堪稱查找內(nèi)存泄露問題的神器。 和 Profile 結(jié)果一樣,快照可以被導(dǎo)出成 .heapsnapshot 文件。

heapsnapshot
heapsnapshot

上周發(fā)布了工具 OneProfile , 可以用來動(dòng)態(tài)地展示 Profile 的結(jié)果,分析各種函數(shù)的調(diào)用關(guān)系。周末我用類似的思路研究了一下 .heapsnapshot 文件,做了這個(gè)網(wǎng)頁小工具,把 Heap Snapshot 用有向圖的方式展現(xiàn)出來。

screenshot
screenshot

OneHeap 名字的由來

There are only two hard things in Computer Science: cache invalidation and naming things. -- Phil Karlton

目前還沒有時(shí)間想一個(gè)高端、大氣、上檔次的名字,因?yàn)槲夜┞毜墓久?OneAPM ( 省去軟廣1000字,總之做性能監(jiān)控很牛),所以就取名 OneHeap 啦。 它是 Toolkit 里的第二個(gè)。

如何生成 Heap Snapshot 文件

瀏覽器

使用 Chrome 打開 測(cè)試頁面 按 F12 打開 Devtools,切換到 Profiles 頁,選擇 Take Heap Snapshot。稍等片刻,在生成的 Snapshot 上點(diǎn)擊右鍵可以導(dǎo)出,文件后綴一般是 .heapsnapshot。

Node.JS

如果你是 Node.JS 工程師,可以安裝 heapdump 這個(gè)很有名的模塊。

https://github.com/bnoordhuis/node-heapdump

上面兩種方法都可以生成 .heapsnapshot 文件,這個(gè)是用來測(cè)試的 nodejs.heapsnapshot

理解 .heapsnapshot 文件格式

打開測(cè)試用的 nodejs.heapsnapshot 文件,這是一個(gè)很大的 JSON 對(duì)象:

  1. snapshot 屬性保存了關(guān)于快照的一些基本信息,如 uid,快照名,節(jié)點(diǎn)個(gè)數(shù)等

  2. nodes 保存了是所有節(jié)點(diǎn)的 id,name,大小信息等,對(duì)應(yīng) v8 源碼里的 HeapGraphNode

  3. edges 屬性保存了節(jié)點(diǎn)間的映射關(guān)系,對(duì)應(yīng) v8 源碼的 HeapGraphEdge

  4. strings 保存了所有的字符串, nodesedges 中不會(huì)直接存字符串,而是存了字符串在 strings 中的索引

堆快照其實(shí)是一個(gè)有向圖的數(shù)據(jù)結(jié)構(gòu),但是 .heapsnapshot 文件在存儲(chǔ)的過程中使用了數(shù)組來存儲(chǔ)圖的結(jié)構(gòu),這一設(shè)計(jì)十分巧妙而且減少了所需磁盤空間的大小。

nodes 屬性

nodes 是一個(gè)很長一維的數(shù)組,但是為了閱讀方便,v8 在序列化的時(shí)候會(huì)自動(dòng)加上換行。按照 v8 版本的不同,可能是5個(gè)一行,也可能是6個(gè)一行,如果是 6 個(gè)一行,則多出來的一個(gè) trace_node_id 屬性。

下標(biāo) 屬性 類型
n type number
n+1 name string
n+2 id number
n+3 self_size number
n+4 edge_count number

其中 type 是一個(gè) 0~12 的數(shù)字,目前的 Chrome 只有 0~9 這幾個(gè)屬性,它們對(duì)應(yīng)的含義分別是

編號(hào) 屬性 說明
0 hidden Hidden node, may be filtered when shown to user.
1 array An array of elements.
2 string A string.
3 object A JS object (except for arrays and strings).
4 code Compiled code.
5 closure Function closure.
6 regexp RegExp.
7 number Number stored in the heap.
8 native Native object (not from V8 heap).
9 synthetic Synthetic object, usualy used for grouping snapshot items together.
10 concatenated string Concatenated string. A pair of pointers to strings.
11 sliced string Sliced string. A fragment of another string.
12 symbol A Symbol (ES6).

edges 屬性

edges 也是一個(gè)一維數(shù)組,長度要比 nodes 大好幾倍,并且相對(duì)于 nodes 要復(fù)雜一些:

下標(biāo) 屬性 類型
n type number
n+1 nameorindex stringornumber
n+2 to_node node

其中 type 是一個(gè) 0~6 的數(shù)字:

編號(hào) 屬性 說明
0 context A variable from a function context.
1 element An element of an array
2 property A named object property.
3 internal A link that can't be accessed from JS,thus, its name isn't a real property name (e.g. parts of a ConsString).
4 hidden A link that is needed for proper sizes calculation, but may be hidden from user.
5 shortcut A link that must not be followed during sizes calculation.
6 weak A weak reference (ignored by the GC).

nodes 和 edges 的對(duì)應(yīng)關(guān)系

如果知道某個(gè)節(jié)點(diǎn)的 id,是沒有辦法直接從 edges 中查出和它相鄰的點(diǎn)的,因?yàn)?edges 并不是一個(gè) from-to 的 Hash。想知道從一個(gè)節(jié)點(diǎn)出發(fā) 可到達(dá)那些節(jié)點(diǎn),需要遍歷一次 nodes。

具體做法如下:

  1. 在遍歷 nodes 前初始化一個(gè)變量 edge_offset,初始值是0,每遍歷一個(gè)節(jié)點(diǎn)都會(huì)改變它的值。

  2. 遍歷某個(gè)節(jié)點(diǎn) Nx 的過程中:

從 Nx 出發(fā)的第一條 Edge

edges[ edge_offset ]      是 Edge 的類型
edges[ edge_offset +1 ]   是 Edge 的名稱或下標(biāo)
edges[ edge_offset +2 ]   是 Edge 指向的對(duì)象的節(jié)點(diǎn)類型在 `nodes` 里的索引

從 Nx 出發(fā)的第2條 Edge

edges[ edge_offset + 3 ]  
     ............         是下一個(gè) Edge 
edges[ edge_offset + 5 ]

從 Nx 出發(fā),一共有 edge_count 條 Edge

...
  1. 每遍歷完一個(gè)節(jié)點(diǎn),就在 edge_offset 上加 3 x edge_count,并回到步驟 2,直到所有節(jié)點(diǎn)都遍歷完

步驟1到3 用偽代碼表示就是:

edge_offset=0

// 遍歷每一個(gè)節(jié)點(diǎn)
for(node in nodes){

  // edges 下標(biāo)從 edge_offset 到 edge_offset + 3 x edge_count    都是 node 可以到達(dá)的點(diǎn)
  edge_offset+= 3 x node.edge_count
}

以上就是 .heapsnapshot 的文件格式定義了,基于這些發(fā)現(xiàn),在結(jié)合一個(gè)前端繪圖的庫,就可以可視化的展示 Heap Snapshot 了。

OneHeap 使用說明

鏈接地址

使用 Chrome 打開: OneHeap

一些有意思的截圖

@1

Node.JS

root
root

樸靈老師的《深入淺出Node.JS》有對(duì) Buffer 的詳細(xì)介紹,其中提到 Buffer 是 JavaScript 和 C++ 技術(shù)結(jié)合的典型代表

瀏覽器

dom
dom

很明顯瀏覽器下多了 Window 和 Document 對(duì)象,而 Detached DOM tree 正是前端內(nèi)存泄露的高發(fā)地。

Objects

root
root

最密集的那部分的中心是 Object 構(gòu)造函數(shù),如果把 Object 和 Array 構(gòu)造函數(shù)隱藏,就變成了下面這樣

root
root

MathConstructor

Math
Math

左上角是例如 自然對(duì)數(shù)E 這樣的常量,v8源碼

正則表達(dá)式

Regexp
Regexp

所有的正則表達(dá)式實(shí)例的 __proto__都指向 RegExp 構(gòu)造函數(shù),同時(shí) RegExp 的 __proto__又指向 Object

Stream

Stream
Stream

在 Node.JS 中和 Stream 相關(guān)的幾個(gè)類的設(shè)計(jì)和 Java 類似,都使用到裝飾器的設(shè)計(jì)模式,層層嵌套, 例如 v8源碼

參考資料

Heap Profiling

了解 JavaScript 應(yīng)用程序中的內(nèi)存泄漏

關(guān)于

本文相關(guān)的源碼在: https://github.com/wyvernnot/javascriptperformancemeasurement/tree/gh-pages/heap_snapshot;

本文系 OneAPM 工程師原創(chuàng)。OneAPM 是應(yīng)用性能管理領(lǐng)域的新興領(lǐng)軍企業(yè),能幫助企業(yè)用戶和開發(fā)者輕松實(shí)現(xiàn):緩慢的程序代碼和 SQL 語句的實(shí)時(shí)抓取。想閱讀更多技術(shù)文章,請(qǐng)?jiān)L問 OneAPM 官方博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容