zoukankan      html  css  js  c++  java
  • JavaScript中V8引擎内存问题

    简介

    V8 是谷歌开发的高性能 JavaScript 引擎,该引擎使用 C++ 开发。目前主要应用在 Google Chrome 浏览器和 node.js 当中。

    V8 自带的高性能垃圾回收机制,使开发者能够专注于程序开发中,极大的提高开发者的编程效率。但是方便之余,也会出现一些对新手来说比较棘手的问题:进程内存暴涨,cpu 飙升,性能很差等。这个时候,了解 V8 的内存结构和垃圾回收机制、知道如何进行性能调优就很有必要。本文主要讲述 V8 的内存管理和垃圾回收,后面会用示例代码结合 Chrome 的开发者工具进行分析;最后介绍了阿里的 node.js 应用服务解决方案 alinode。

    V8 内存构成

    一个 V8 进程的内存通常由以下几个块构成:

    1. 新生代内存区(new space)大多数的对象都会被分配在这里,这个区域很小但是垃圾回收比较频繁;
    2. 老生代内存区(old space)
      属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针;
    3. 大对象区(large object space)这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象区;
    4. 代码区(code space)
      代码对象,会被分配在这里。唯一拥有执行权限的内存;
    5. map 区(map space)
      存放 Cell 和 Map,每个区域都是存放相同大小的元素,结构简单。

    内存构成可以用下图来表示:

    JavaScript中V8引擎内存问题

     

    其中带斜纹的是对应的内存块中未使用的内存空间。new space 通常很小(1~8M),它被分成了两部分,一部分叫做 inactive new space,一部分是激活状态,为啥会有激活和未激活之分的原因,下面会提到。old space 偏大,可能达几百兆。

    V8 内存生命周期

    假设代码中有一个对象 jerry ,这个对象从创建到被销毁,刚好走完了整个生命周期,通常会是这样一个过程:

    1. 这个对象被分配到了 new space;
    2. 随着程序的运行,new space 塞满了,gc 开始清理 new space 里的死对象,jerry 因为还处于活跃状态,所以没被清理出去;
    3. gc 清理了两遍 new space,发现 jerry 依然还活跃着,就把 jerry 移动到了 old space;
    4. 随着程序的运行,old space 也塞满了,gc 开始清理 old space,这时候发现 jerry 已经没有被引用了,就把 jerry 给清理出去了。

    第二步里,清理 new space 的过程叫做 Scavenge,这个过程采用了空间换时间的做法,用到了上面图中的 inactive new space,过程如下:

    1. 当活跃区满了之后,交换活跃区和非活跃区,交换后活跃区变空了;
    2. 将非活跃区的两次清理都没清理出去的对象移动到 old space;
    3. 将还没清理够两次的但是活跃状态的对象移动到活跃区。

    第四步里,清理 old space 的过程叫做 Mark-sweep ,这块占用内存很大,所以没有使用 Scavenge,这个回收过程包含了若干次标记过程和清理过程:

    1. 标记从根(root)可达的对象为黑色;
    2. 遍历黑色对象的邻接对象,直到所有对象都标记为黑色;
    3. 循环标记若干次;
    4. 清理掉非黑色的对象。

    简单来说,Mark-sweep 就是把从根节点无法获取到的对象清理掉了。

    使用 Chrome 调优前端代码

    注:本文截图里的 Chrome 版本为 Version 64.0.3282.140 (Official Build) (64-bit)

    1. 查看内容构成

    在控制台获取当前页面的堆内存快照(heap snapshot):

    JavaScript中V8引擎内存问题

     

    为了便于观看,先在 console 里声明一个类并创建它的一些对象:

    class Jane {
    }
    
    class Tom {
      constructor () {  this.jane = new Jane()
      }
    }
    
    Array(1000000)  .fill('')   .map(() => new Tom())

    获取成功后,可以看到一个表格:

    JavaScript中V8引擎内存问题

     

    JavaScript中V8引擎内存问题

     

    介绍一下几个关键的列:

    1. Constructor:对象的类名;
    2. Distance:对象到根的引用层级;
    3. Objects Count:对象的数量;
    4. Shallow Size: 对象本身占用的内存,不包括引用的对象所占内存;
    5. Retained Size: 对象所占总内存,包含引用的其他对象所占内存;
    6. 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 如下图所示:

    JavaScript中V8引擎内存问题

     

    红框中展示了该 B 实例被应用的三个位置,后面的 @?? 可以视为内存的地址,同样的地址意味着同一个对象。可以展开左边的箭头查看,这三个直接引用的地方分别是:

    1. Blist.values 对应的指针;
    2. A.b 对应的指针;
    3. Blist.values 指向的数组的指针。

    可以观察到,A 的 retained size 现在和 shallow size 一样了,因为 A 的实例在 aArray 中被引用了;B 的两个 size 也一样了,因为在 A 中和 bList 中都有引用,销毁其本身并不会释放相应的内存。

    3. 调试内存泄露

    如果你的网页在放久了的情况下内存越来越大甚至 tab 页崩溃,那就要考虑是否内存泄露了。通过 Chrome 的任务管理器可以看到 JavaScript 所占用的内存:

    JavaScript中V8引擎内存问题

     

    通过 Performance 里的 record 也可以直观地看到内存的增长(需要勾上 Memory 选项):

    JavaScript中V8引擎内存问题

     

    用一个示例代码,结合 heap snapshot 来说明如何排查内存泄露:

    const a = {}
    
    setInterval(() => {
      a[Date.now()] = new ArrayBuffer(1000000)
    }, 100)

    这段代码粘贴在控制台后,在控制台的 Memory 页面,隔 10s 取一个 heap snapshot:

    JavaScript中V8引擎内存问题

     

    选中第二个和第三个,在选取观察类型的下拉菜单里选择「Comparison」,然后再选择右面的下拉菜单,选择上一个 snapshot:

    JavaScript中V8引擎内存问题

     

    JavaScript中V8引擎内存问题

     

    这个时候后列表中的内容是当前的 snapshot 针对上一个的增加的部分,可以看到图中的 snapshot 14 比 snapshot 13 多出来的部分,跟 snapshot 13 比 snapshot 12 多出来的部分都有 ArrayBuffer,那么就可以确定 ArrayBuffer 导致了内存泄露。这个时候可以结合上面一节的「查看对象引用关系」来定位到类或者代码。

    使用 alinode 调优 node.js 进程

    alinode 是阿里云出品的 node.js 应用服务解决方案,是一套基于社区 Node 改进的运行时环境和服务平台。使用 alinode 来调优 node.js 进程更加直观,便捷,而且具备系统监控、日志服务、nodejs 进程监控、报警等功能,非常强大。

    使用 alinode 要经过以下步骤:

    1. 注册阿里云账号;
    2. 开通 alinode 服务;
    3. 创建 alinode 应用;
    4. 在自己服务器上安装 alinode 和 agenthub,配置好自己应用的 id 和 secret;
    5. 启动自己的 node 进程。

    下图是 alinode 某应用的一个实例的控制台:

    JavaScript中V8引擎内存问题

     

    图中圈出来的是常用的几个指标:

    1. 进程存活时间线:线断了意味着可能进程重启了或者机器网络故障;
    2. CPU;
    3. 内存;
    4. GC 时间占比:一分钟内 gc 占用时间的百分比;一般认为小于 5% 属于正常状态,比例很大的话意味着 CPU 需要耗费很多时间在 gc 上,导致进程性能严重下降。这通常对应以下三种情况:
      1. 进程负载太高,需要增加服务节点;
      2. 进程内存泄露,导致不停的在 gc;
      3. 代码需要优化;
    5. CPU Profile:火焰图的形式来统计分析;
    6. 堆快照:可以通过点击「对快照」来生成 heap snapshot,可以下载下来通过 Chrome 来分析,也可以使用 alinode 自带的分析工具:
    JavaScript中V8引擎内存问题

     

    此外还有一个专门针对内存和 gc 的工具:GC Trace,这个是用来观察 gc 的过程,从而更直观的观察进程 gc 的步骤:

    JavaScript中V8引擎内存问题

     

    在这里,你可以直观的看到堆大小的变化,可以看到每一次的 GC 是 Scavenge 还是 Mark-Sweep,可以看到每一次 gc 内存堆各类型的大小变化,有了这个强大的分析工具,你可以写出性能更高、更加稳定、响应速度更快的 node.js 代码。

    作者:Tom Wan
    链接:https://zhuanlan.zhihu.com/p/33816534
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 相关阅读:
    mac上命令行解压rar
    Mac上安装PHP、Apache、MySQL
    8款不错的 CI/CD工具
    Apache 强制Http跳转Https
    使用MySQL的mysqldump命令备份数据库和把数据库备份文件恢复
    MySQL主从复制和读写分离
    Nginx参数调优
    【原创】深入理解Docker容器和镜像 -- 分析了docker的命令含义
    Elasticsearch使用备忘
    通过HTTP RESTful API 操作elasticsearch搜索数据
  • 原文地址:https://www.cnblogs.com/cangqinglang/p/12668374.html
Copyright © 2011-2022 走看看