JavaScript 堆内存分析新工具 OneHeap

OneHeap 关注于运行中的 JavaScript 内存信息的展示,用可视化的方式还原了 HeapGraph,有助于理解 v8 内存管理。

背景

JavaScript 运行过程中的大部分数据都保存在堆 (Heap) 中,所以 JavaScript 性能分析另一个比较重要的方面是内存,也就是堆的分析。

利用 Chrome Dev Tools 可以生成应用程序某个时刻的堆快照 (HeapSnapshot),它较完整地记录了各种对象和引用的情况,堪称查找内存泄露问题的神器。 和 Profile 结果一样,快照可以被导出成 .heapsnapshot 文件。

heapsnapshot
heapsnapshot

上周发布了工具 OneProfile , 可以用来动态地展示 Profile 的结果,分析各种函数的调用关系。周末我用类似的思路研究了一下 .heapsnapshot 文件,做了这个网页小工具,把 Heap Snapshot 用有向图的方式展现出来。

screenshot
screenshot

OneHeap 名字的由来

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

目前还没有时间想一个高端、大气、上档次的名字,因为我供职的公司名叫 OneAPM ( 省去软广1000字,总之做性能监控很牛),所以就取名 OneHeap 啦。 它是 Toolkit 里的第二个。

如何生成 Heap Snapshot 文件

浏览器

使用 Chrome 打开 测试页面 按 F12 打开 Devtools,切换到 Profiles 页,选择 Take Heap Snapshot。稍等片刻,在生成的 Snapshot 上点击右键可以导出,文件后缀一般是 .heapsnapshot

Node.JS

如果你是 Node.JS 工程师,可以安装 heapdump 这个很有名的模块。

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

上面两种方法都可以生成 .heapsnapshot 文件,这个是用来测试的 nodejs.heapsnapshot

理解 .heapsnapshot 文件格式

打开测试用的 nodejs.heapsnapshot 文件,这是一个很大的 JSON 对象:

  1. snapshot 属性保存了关于快照的一些基本信息,如 uid,快照名,节点个数等

  2. nodes 保存了是所有节点的 id,name,大小信息等,对应 v8 源码里的 HeapGraphNode

  3. edges 属性保存了节点间的映射关系,对应 v8 源码的 HeapGraphEdge

  4. strings 保存了所有的字符串, nodesedges 中不会直接存字符串,而是存了字符串在 strings 中的索引

堆快照其实是一个有向图的数据结构,但是 .heapsnapshot 文件在存储的过程中使用了数组来存储图的结构,这一设计十分巧妙而且减少了所需磁盘空间的大小。

nodes 属性

nodes 是一个很长一维的数组,但是为了阅读方便,v8 在序列化的时候会自动加上换行。按照 v8 版本的不同,可能是5个一行,也可能是6个一行,如果是 6 个一行,则多出来的一个 trace_node_id 属性。

下标 属性 类型
n type number
n+1 name string
n+2 id number
n+3 self_size number
n+4 edge_count number

其中 type 是一个 0~12 的数字,目前的 Chrome 只有 0~9 这几个属性,它们对应的含义分别是

编号 属性 说明
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 也是一个一维数组,长度要比 nodes 大好几倍,并且相对于 nodes 要复杂一些:

下标 属性 类型
n type number
n+1 nameorindex stringornumber
n+2 to_node node

其中 type 是一个 0~6 的数字:

编号 属性 说明
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 的对应关系

如果知道某个节点的 id,是没有办法直接从 edges 中查出和它相邻的点的,因为 edges 并不是一个 from-to 的 Hash。想知道从一个节点出发 可到达那些节点,需要遍历一次 nodes

具体做法如下:

  1. 在遍历 nodes 前初始化一个变量 edge_offset,初始值是0,每遍历一个节点都会改变它的值。

  2. 遍历某个节点 Nx 的过程中:

从 Nx 出发的第一条 Edge

edges[ edge_offset ]      是 Edge 的类型
edges[ edge_offset +1 ]   是 Edge 的名称或下标
edges[ edge_offset +2 ]   是 Edge 指向的对象的节点类型在 `nodes` 里的索引

从 Nx 出发的第2条 Edge

edges[ edge_offset + 3 ]  
     ............         是下一个 Edge 
edges[ edge_offset + 5 ]

从 Nx 出发,一共有 edge_count 条 Edge

...
  1. 每遍历完一个节点,就在 edge_offset 上加 3 x edge_count,并回到步骤 2,直到所有节点都遍历完

步骤1到3 用伪代码表示就是:

edge_offset=0

// 遍历每一个节点
for(node in nodes){

  // edges 下标从 edge_offset 到 edge_offset + 3 x edge_count    都是 node 可以到达的点
  edge_offset+= 3 x node.edge_count
}

以上就是 .heapsnapshot 的文件格式定义了,基于这些发现,在结合一个前端绘图的库,就可以可视化的展示 Heap Snapshot 了。

OneHeap 使用说明

链接地址

使用 Chrome 打开: OneHeap

一些有意思的截图

@1

Node.JS

root
root

朴灵老师的《深入浅出Node.JS》有对 Buffer 的详细介绍,其中提到 Buffer 是 JavaScript 和 C++ 技术结合的典型代表

浏览器

dom
dom

很明显浏览器下多了 Window 和 Document 对象,而 Detached DOM tree 正是前端内存泄露的高发地。

Objects

root
root

最密集的那部分的中心是 Object 构造函数,如果把 Object 和 Array 构造函数隐藏,就变成了下面这样

root
root

MathConstructor

Math
Math

左上角是例如 自然对数E 这样的常量,v8源码

正则表达式

Regexp
Regexp

所有的正则表达式实例的 __proto__都指向 RegExp 构造函数,同时 RegExp 的 __proto__又指向 Object

Stream

Stream
Stream

在 Node.JS 中和 Stream 相关的几个类的设计和 Java 类似,都使用到装饰器的设计模式,层层嵌套, 例如 v8源码

参考资料

Heap Profiling

了解 JavaScript 应用程序中的内存泄漏

关于

本文相关的源码在: https://github.com/wyvernnot/javascriptperformancemeasurement/tree/gh-pages/heap_snapshot;

本文系 OneAPM 工程师原创。OneAPM 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问 OneAPM 官方博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容