蜀道难难不难 發表於 2020-4-9 17:38:00

JavaScript中V8引擎内存问题

<h2 class="pgc-h-arrow-right">简介</h2>
<p>V8 是谷歌开发的高性能 JavaScript 引擎,该引擎使用 C++ 开发。目前主要应用在 Google Chrome 浏览器和 node.js 当中。</p>
<p>V8 自带的高性能垃圾回收机制,使开发者能够专注于程序开发中,极大的提高开发者的编程效率。但是方便之余,也会出现一些对新手来说比较棘手的问题:进程内存暴涨,cpu 飙升,性能很差等。这个时候,了解 V8 的内存结构和垃圾回收机制、知道如何进行性能调优就很有必要。本文主要讲述 V8 的内存管理和垃圾回收,后面会用示例代码结合 Chrome 的开发者工具进行分析;最后介绍了阿里的 node.js 应用服务解决方案 alinode。</p>
<p>&nbsp;</p>
<h2 class="pgc-h-arrow-right">V8 内存构成</h2>
<p>一个 V8 进程的内存通常由以下几个块构成:</p>
<ol start="1">
<li>新生代内存区(new space)大多数的对象都会被分配在这里,这个区域很小但是垃圾回收比较频繁;</li>
<li>老生代内存区(old space)<br>属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针;</li>
<li>大对象区(large object space)这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象区;</li>
<li>代码区(code space)<br>代码对象,会被分配在这里。唯一拥有执行权限的内存;</li>
<li>map 区(map space)<br>存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单。</li>

</ol>
<p>内存构成可以用下图来表示:</p>
<p>&nbsp;</p>
<div class="pgc-img"><img src="http://p1.pstatp.com/large/pgc-image/f405d4c182134afe9e6a703918abb115" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>

</div>
<p>其中带斜纹的是对应的内存块中未使用的内存空间。new space 通常很小(1~8M),它被分成了两部分,一部分叫做 inactive new space,一部分是激活状态,为啥会有激活和未激活之分的原因,下面会提到。old space 偏大,可能达几百兆。</p>
<p>&nbsp;</p>
<h2 class="pgc-h-arrow-right">V8 内存生命周期</h2>
<p>假设代码中有一个对象 jerry ,这个对象从创建到被销毁,刚好走完了整个生命周期,通常会是这样一个过程:</p>
<ol start="1">
<li>这个对象被分配到了 new space;</li>
<li>随着程序的运行,new space 塞满了,gc 开始清理 new space 里的死对象,jerry 因为还处于活跃状态,所以没被清理出去;</li>
<li>gc 清理了两遍 new space,发现 jerry 依然还活跃着,就把 jerry 移动到了 old space;</li>
<li>随着程序的运行,old space 也塞满了,gc 开始清理 old space,这时候发现 jerry 已经没有被引用了,就把 jerry 给清理出去了。</li>

</ol>
<p>第二步里,清理 new space 的过程叫做&nbsp;Scavenge,这个过程采用了空间换时间的做法,用到了上面图中的 inactive new space,过程如下:</p>
<ol start="1">
<li>当活跃区满了之后,交换活跃区和非活跃区,交换后活跃区变空了;</li>
<li>将非活跃区的两次清理都没清理出去的对象移动到 old space;</li>
<li>将还没清理够两次的但是活跃状态的对象移动到活跃区。</li>

</ol>
<p>第四步里,清理 old space 的过程叫做&nbsp;Mark-sweep&nbsp;,这块占用内存很大,所以没有使用 Scavenge,这个回收过程包含了若干次标记过程和清理过程:</p>
<ol start="1">
<li>标记从根(root)可达的对象为黑色;</li>
<li>遍历黑色对象的邻接对象,直到所有对象都标记为黑色;</li>
<li>循环标记若干次;</li>
<li>清理掉非黑色的对象。</li>

</ol>
<p>简单来说,Mark-sweep 就是把从根节点无法获取到的对象清理掉了。</p>
<p>&nbsp;</p>
<h2 class="pgc-h-arrow-right">使用 Chrome 调优前端代码</h2>
<blockquote class="pgc-blockquote-quote article-blockquote">
<p>注:本文截图里的 Chrome 版本为 Version 64.0.3282.140 (Official Build) (64-bit)</p>

</blockquote>
<h2 class="pgc-h-arrow-right">1. 查看内容构成</h2>
<p>在控制台获取当前页面的堆内存快照(heap snapshot):</p>
<div class="pgc-img"><img src="http://p1.pstatp.com/large/pgc-image/26ae6b8e01414236b0df5af2839b3079" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>

</div>
<p>为了便于观看,先在 console 里声明一个类并创建它的一些对象:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">class Jane {
}

class Tom {
constructor () {</span><span style="color: rgba(0, 0, 255, 1)">this</span>.jane = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Jane()
}
}

Array(</span>1000000).fill('')   .map(() =&gt; <span style="color: rgba(0, 0, 255, 1)">new</span> Tom())</pre>
</div>
<p>&nbsp;</p>
<p>获取成功后,可以看到一个表格:</p>
<div class="pgc-img"><img src="http://p1.pstatp.com/large/pgc-image/65ffd63c43fd4b768f27b272b5db3145" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<div class="pgc-img"><img src="http://p1.pstatp.com/large/pgc-image/65ffd63c43fd4b768f27b272b5db3145" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>&nbsp;</p>
<p>介绍一下几个关键的列:</p>
<ol start="1">
<li>Constructor:对象的类名;</li>
<li>Distance:对象到根的引用层级;</li>
<li>Objects Count:对象的数量;</li>
<li>Shallow Size: 对象本身占用的内存,不包括引用的对象所占内存;</li>
<li>Retained Size: 对象所占总内存,包含引用的其他对象所占内存;</li>
<li>Retainers:对象的引用层级关系。</li>
</ol>
<p>shallow size 和 retained size 的区别可以用红框里的 Tom 和 Jane 更直观的展示:Tom 的 shallow 占了 32M,retained 占用了 56M,这是因为 retained 包括了引用的指针对应的内存大小,即 tom.jane 所占用的内存;所以 Tom 的 retained 总和比 shallow 多出来的 24M 正好跟 Jane 占用的 24M 相同。retained size 可以理解为当回收掉该对象时可以释放的内存大小,在内存调优中具有重要参考意义。</p>
<p>&nbsp;</p>
<h2 class="pgc-h-arrow-right">2. 查看对象的引用关系</h2>
<p>这里使用一个稍复杂的代码来展示:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">class B {}

class A {
constructor () {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.b = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> B()
}
}

class BList {
constructor () {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.values =<span style="color: rgba(0, 0, 0, 1)"> []
}
push (b) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.values.push(b)
}
}

const aArray </span>= Array(1000000).fill('').map(() =&gt; <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> A())
const bList </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> BList()
aArray.forEach(a </span>=&gt; { bList.push(a.b) })</pre>
</div>
<pre><code>&nbsp;</code></pre>
<p>heap snapshot 如下图所示:</p>
<div class="pgc-img"><img src="http://p1.pstatp.com/large/pgc-image/e12a60d378ad470f8666391a53d2ccef" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>&nbsp;</p>
<p>红框中展示了该 B 实例被应用的三个位置,后面的 @?? 可以视为内存的地址,同样的地址意味着同一个对象。可以展开左边的箭头查看,这三个直接引用的地方分别是:</p>
<ol start="1">
<li>Blist.values 对应的指针;</li>
<li>A.b 对应的指针;</li>
<li>Blist.values 指向的数组的指针。</li>
</ol>
<p>可以观察到,A 的 retained size 现在和 shallow size 一样了,因为 A 的实例在 aArray 中被引用了;B 的两个 size 也一样了,因为在 A 中和 bList 中都有引用,销毁其本身并不会释放相应的内存。</p>
<p>&nbsp;</p>
<h2 class="pgc-h-arrow-right">3. 调试内存泄露</h2>
<p>如果你的网页在放久了的情况下内存越来越大甚至 tab 页崩溃,那就要考虑是否内存泄露了。通过 Chrome 的任务管理器可以看到 JavaScript 所占用的内存:</p>
<div class="pgc-img"><img src="http://p3.pstatp.com/large/pgc-image/8be9a27c5a104a6195b6524ab93f6ba9" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>通过 Performance 里的 record 也可以直观地看到内存的增长(需要勾上 Memory 选项):</p>
<div class="pgc-img"><img src="http://p3.pstatp.com/large/pgc-image/23f43a2367af4d5983ce96676f37914e" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>&nbsp;</p>
<p>用一个示例代码,结合 heap snapshot 来说明如何排查内存泄露:</p>
<p>&nbsp;</p>
<div class="cnblogs_code">
<pre>const a =<span style="color: rgba(0, 0, 0, 1)"> {}

setInterval(() </span>=&gt;<span style="color: rgba(0, 0, 0, 1)"> {
a </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> ArrayBuffer(1000000<span style="color: rgba(0, 0, 0, 1)">)
}, </span>100)</pre>
</div>
<p>&nbsp;</p>
<p>这段代码粘贴在控制台后,在控制台的 Memory 页面,隔 10s 取一个 heap snapshot:</p>
<div class="pgc-img"><img src="http://p3.pstatp.com/large/pgc-image/a2b6247f5f654eb2a2c5e241d439a3d4" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>&nbsp;</p>
<p>选中第二个和第三个,在选取观察类型的下拉菜单里选择「Comparison」,然后再选择右面的下拉菜单,选择上一个 snapshot:</p>
<div class="pgc-img"><img src="http://p3.pstatp.com/large/pgc-image/461c1c35d11a409385e92c99e444ed0c" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>&nbsp;</p>
<div class="pgc-img"><img src="http://p1.pstatp.com/large/pgc-image/0723e3aa8ea7422f8b83ac1cf2542476" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>这个时候后列表中的内容是当前的 snapshot 针对上一个的增加的部分,可以看到图中的 snapshot 14 比 snapshot 13 多出来的部分,跟 snapshot 13 比 snapshot 12 多出来的部分都有 ArrayBuffer,那么就可以确定 ArrayBuffer 导致了内存泄露。这个时候可以结合上面一节的「查看对象引用关系」来定位到类或者代码。</p>
<p>&nbsp;</p>
<h2 class="pgc-h-arrow-right">使用 alinode 调优 node.js 进程</h2>
<p>alinode 是阿里云出品的 node.js 应用服务解决方案,是一套基于社区 Node 改进的运行时环境和服务平台。使用 alinode 来调优 node.js 进程更加直观,便捷,而且具备系统监控、日志服务、nodejs 进程监控、报警等功能,非常强大。</p>
<p>使用 alinode 要经过以下步骤:</p>
<ol start="1">
<li>注册阿里云账号;</li>
<li>开通 alinode 服务;</li>
<li>创建 alinode 应用;</li>
<li>在自己服务器上安装 alinode 和 agenthub,配置好自己应用的 id 和 secret;</li>
<li>启动自己的 node 进程。</li>
</ol>
<p>下图是 alinode 某应用的一个实例的控制台:</p>
<p>&nbsp;</p>
<div class="pgc-img"><img src="http://p1.pstatp.com/large/pgc-image/0c37c593b70e428fba9bdbf680bcc0cd" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>&nbsp;</p>
<p>图中圈出来的是常用的几个指标:</p>
<ol start="1">
<li>进程存活时间线:线断了意味着可能进程重启了或者机器网络故障;</li>
<li>CPU;</li>
<li>内存;</li>
<li>GC 时间占比:一分钟内 gc 占用时间的百分比;一般认为小于 5% 属于正常状态,比例很大的话意味着 CPU 需要耗费很多时间在 gc 上,导致进程性能严重下降。这通常对应以下三种情况:</li>
<ol start="1">
<li>进程负载太高,需要增加服务节点;</li>
<li>进程内存泄露,导致不停的在 gc;</li>
<li>代码需要优化;</li>
</ol>
<li>CPU Profile:火焰图的形式来统计分析;</li>
<li>堆快照:可以通过点击「对快照」来生成 heap snapshot,可以下载下来通过 Chrome 来分析,也可以使用 alinode 自带的分析工具:</li>
</ol>
<p>&nbsp;</p>
<div class="pgc-img"><img src="http://p1.pstatp.com/large/pgc-image/5ed172ecec8a47a48fb6bda7e1b0fd68" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>&nbsp;</p>
<p>此外还有一个专门针对内存和 gc 的工具:GC Trace,这个是用来观察 gc 的过程,从而更直观的观察进程 gc 的步骤:</p>
<p>&nbsp;</p>
<div class="pgc-img"><img src="http://p1.pstatp.com/large/pgc-image/40c315f676e24b19916c907c2c4ab704" alt="JavaScript中V8引擎内存问题">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<p>在这里,你可以直观的看到堆大小的变化,可以看到每一次的 GC 是 Scavenge 还是 Mark-Sweep,可以看到每一次 gc 内存堆各类型的大小变化,有了这个强大的分析工具,你可以写出性能更高、更加稳定、响应速度更快的 node.js 代码。</p>
<p>&nbsp;</p>
<p>作者:Tom Wan<br>链接:https://zhuanlan.zhihu.com/p/33816534<br>来源:知乎<br>著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。</p><p>喜欢这篇文章?欢迎打赏~~</p><p><img src="https://img2020.cnblogs.com/blog/1313648/202012/1313648-20201207210415386-746901846.png" alt="" loading="lazy"></p><p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/cangqinglang/p/12668374.html
頁: [1]
查看完整版本: JavaScript中V8引擎内存问题