简介
V8 是谷歌开发的高性能 JavaScript 引擎,该引擎使用 C++ 开发。目前主要应用在 Google Chrome 浏览器和 node.js 当中。
V8 自带的高性能垃圾回收机制,使开发者能够专注于程序开发中,极大的提高开发者的编程效率。但是方便之余,也会出现一些对新手来说比较棘手的问题:进程内存暴涨,cpu 飙升,性能很差等。这个时候,了解 V8 的内存结构和垃圾回收机制、知道如何进行性能调优就很有必要。本文主要讲述 V8 的内存管理和垃圾回收,后面会用示例代码结合 Chrome 的开发者工具进行分析;最后介绍了阿里的 node.js 应用服务解决方案 alinode。
V8 内存构成
一个 V8 进程的内存通常由以下几个块构成:
- 新生代内存区(new space)大多数的对象都会被分配在这里,这个区域很小但是垃圾回收比较频繁;
- 老生代内存区(old space)
属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针; - 大对象区(large object space)这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象区;
- 代码区(code space)
代码对象,会被分配在这里。唯一拥有执行权限的内存; - map 区(map space)
存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单。
内存构成可以用下图来表示:
其中带斜纹的是对应的内存块中未使用的内存空间。new space 通常很小(1~8M),它被分成了两部分,一部分叫做 inactive new space,一部分是激活状态,为啥会有激活和未激活之分的原因,下面会提到。old space 偏大,可能达几百兆。
V8 内存生命周期
假设代码中有一个对象 jerry ,这个对象从创建到被销毁,刚好走完了整个生命周期,通常会是这样一个过程:
- 这个对象被分配到了 new space;
- 随着程序的运行,new space 塞满了,gc 开始清理 new space 里的死对象,jerry 因为还处于活跃状态,所以没被清理出去;
- gc 清理了两遍 new space,发现 jerry 依然还活跃着,就把 jerry 移动到了 old space;
- 随着程序的运行,old space 也塞满了,gc 开始清理 old space,这时候发现 jerry 已经没有被引用了,就把 jerry 给清理出去了。
第二步里,清理 new space 的过程叫做 Scavenge,这个过程采用了空间换时间的做法,用到了上面图中的 inactive new space,过程如下:
- 当活跃区满了之后,交换活跃区和非活跃区,交换后活跃区变空了;
- 将非活跃区的两次清理都没清理出去的对象移动到 old space;
- 将还没清理够两次的但是活跃状态的对象移动到活跃区。
第四步里,清理 old space 的过程叫做 Mark-sweep ,这块占用内存很大,所以没有使用 Scavenge,这个回收过程包含了若干次标记过程和清理过程:
- 标记从根(root)可达的对象为黑色;
- 遍历黑色对象的邻接对象,直到所有对象都标记为黑色;
- 循环标记若干次;
- 清理掉非黑色的对象。
简单来说,Mark-sweep 就是把从根节点无法获取到的对象清理掉了。
使用 Chrome 调优前端代码
注:本文截图里的 Chrome 版本为 Version 64.0.3282.140 (Official Build) (64-bit)
1. 查看内容构成
在控制台获取当前页面的堆内存快照(heap snapshot):
为了便于观看,先在 console 里声明一个类并创建它的一些对象:
class Jane { } class Tom { constructor () { this.jane = new Jane() } } Array(1000000) .fill('') .map(() => new Tom())
获取成功后,可以看到一个表格:
介绍一下几个关键的列:
- Constructor:对象的类名;
- Distance:对象到根的引用层级;
- Objects Count:对象的数量;
- Shallow Size: 对象本身占用的内存,不包括引用的对象所占内存;
- Retained Size: 对象所占总内存,包含引用的其他对象所占内存;
- Retainers:对象的引用层级关系。
shallow size 和 retained size 的区别可以用红框里的 Tom 和 Jane 更直观的展示:Tom 的 shallow 占了 32M,retained 占用了 56M,这是因为 retained 包括了引用的指针对应的内存大小,即 tom.jane 所占用的内存;所以 Tom 的 retained 总和比 shallow 多出来的 24M 正好跟 Jane 占用的 24M 相同。retained size 可以理解为当回收掉该对象时可以释放的内存大小,在内存调优中具有重要参考意义。
2. 查看对象的引用关系
这里使用一个稍复杂的代码来展示:
class B {} class A { constructor () { this.b = new B() } } class BList { constructor () { this.values = [] } push (b) { this.values.push(b) } } const aArray = Array(1000000).fill('').map(() => new A()) const bList = new BList() aArray.forEach(a => { bList.push(a.b) })
heap snapshot 如下图所示:
红框中展示了该 B 实例被应用的三个位置,后面的 @?? 可以视为内存的地址,同样的地址意味着同一个对象。可以展开左边的箭头查看,这三个直接引用的地方分别是:
- Blist.values 对应的指针;
- A.b 对应的指针;
- Blist.values 指向的数组的指针。
可以观察到,A 的 retained size 现在和 shallow size 一样了,因为 A 的实例在 aArray 中被引用了;B 的两个 size 也一样了,因为在 A 中和 bList 中都有引用,销毁其本身并不会释放相应的内存。
3. 调试内存泄露
如果你的网页在放久了的情况下内存越来越大甚至 tab 页崩溃,那就要考虑是否内存泄露了。通过 Chrome 的任务管理器可以看到 JavaScript 所占用的内存:
通过 Performance 里的 record 也可以直观地看到内存的增长(需要勾上 Memory 选项):
用一个示例代码,结合 heap snapshot 来说明如何排查内存泄露:
const a = {} setInterval(() => { a[Date.now()] = new ArrayBuffer(1000000) }, 100)
这段代码粘贴在控制台后,在控制台的 Memory 页面,隔 10s 取一个 heap snapshot:
选中第二个和第三个,在选取观察类型的下拉菜单里选择「Comparison」,然后再选择右面的下拉菜单,选择上一个 snapshot:
这个时候后列表中的内容是当前的 snapshot 针对上一个的增加的部分,可以看到图中的 snapshot 14 比 snapshot 13 多出来的部分,跟 snapshot 13 比 snapshot 12 多出来的部分都有 ArrayBuffer,那么就可以确定 ArrayBuffer 导致了内存泄露。这个时候可以结合上面一节的「查看对象引用关系」来定位到类或者代码。
使用 alinode 调优 node.js 进程
alinode 是阿里云出品的 node.js 应用服务解决方案,是一套基于社区 Node 改进的运行时环境和服务平台。使用 alinode 来调优 node.js 进程更加直观,便捷,而且具备系统监控、日志服务、nodejs 进程监控、报警等功能,非常强大。
使用 alinode 要经过以下步骤:
- 注册阿里云账号;
- 开通 alinode 服务;
- 创建 alinode 应用;
- 在自己服务器上安装 alinode 和 agenthub,配置好自己应用的 id 和 secret;
- 启动自己的 node 进程。
下图是 alinode 某应用的一个实例的控制台:
图中圈出来的是常用的几个指标:
- 进程存活时间线:线断了意味着可能进程重启了或者机器网络故障;
- CPU;
- 内存;
- GC 时间占比:一分钟内 gc 占用时间的百分比;一般认为小于 5% 属于正常状态,比例很大的话意味着 CPU 需要耗费很多时间在 gc 上,导致进程性能严重下降。这通常对应以下三种情况:
- 进程负载太高,需要增加服务节点;
- 进程内存泄露,导致不停的在 gc;
- 代码需要优化;
- CPU Profile:火焰图的形式来统计分析;
- 堆快照:可以通过点击「对快照」来生成 heap snapshot,可以下载下来通过 Chrome 来分析,也可以使用 alinode 自带的分析工具:
此外还有一个专门针对内存和 gc 的工具:GC Trace,这个是用来观察 gc 的过程,从而更直观的观察进程 gc 的步骤:
在这里,你可以直观的看到堆大小的变化,可以看到每一次的 GC 是 Scavenge 还是 Mark-Sweep,可以看到每一次 gc 内存堆各类型的大小变化,有了这个强大的分析工具,你可以写出性能更高、更加稳定、响应速度更快的 node.js 代码。
作者:Tom Wan
链接:https://zhuanlan.zhihu.com/p/33816534
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。