浅谈 JavaScript 垃圾回收机制

github 获取更多资源

https://github.com/ChenMingK/WebKnowledges-Notes 在线阅读:https://www.kancloud.cn/chenmk/web-knowledges/1080520javascript

垃圾回收机制

对垃圾回收算法而言,其核心思想就是如何判断内存再也不使用了 比较古老的说法是 引用计数标记清除前端

引用计数

引用计数算法定义“内存再也不使用”的标准很简单,就是看一个对象是否有指向它的引用。若是没有其余对象指向它了,说明该对象已经再也不需了。java

// 建立一个对象 person,他有两个指向属性 age 和 name 的引用
var person = {
    age: 12,
    name: 'aaaa'
};
 
person.name = null // 虽然设置为null,但由于 person 对象还有指向 name 的引用,所以name 不会回收
 
var p = person
person = 1        // 原来的 person 对象被赋值为 1,但由于有新引用 p 指向原 person 对象,所以它不会被回收
p = null           // 原 person 对象已经没有引用,很快会被回收

由上面能够看出,引用计数算法是个简单有效的算法。但它却存在一个致命的问题:循环引用。若是两个对象相互引用,尽管他们已再也不使用,垃圾回收器不会进行回收,致使内存泄露。好比下面这样node

function cycle () {
    var o1 = {}
    var o2 = {}
    o1.a = o2
    o2.a = o1
    return "Cycle reference!"
}
cycle()

标记清除

标记清除算法将“再也不使用的对象”定义为“没法达到的对象”。简单来讲,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还须要使用的。那些没法由根部出发触及到的对象被标记为再也不使用,稍后进行回收。 从这个概念能够看出,没法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是没法触及的对象)。但反之未必成立。git

V8引擎垃圾回收机制

能够阅读这篇文章,最近看 《深刻浅出 Node.js》淘到些 V8 垃圾回收机制的介绍。github

V8 的垃圾回收机制与内存限制

在通常的后端开发语言中,基本的内存使用上没有什么限制,然而在 Node 中经过 JavaScript 使用内存时会发现只能使用部份内存(64 位系统下约为 1.4 GB,32 位系统下约为 0.7 GB)。在这样的限制下,将会致使 Node 没法直接操做大内存对象,好比没法将一个 2GB 的文件读入内存中进行字符串分析处理。(stream 模块解决了这个问题)web

形成这个问题的主要缘由在于 Node 基于 V8 构建,V8 的内存管理机制在浏览器的应用场景下绰绰有余,但在 Node 中却限制了开发者。因此咱们有必要知晓 V8 的内存管理策略。算法

V8 的对象分配

在 V8 中,全部的 JavaScript 对象(object)都是经过堆来进行分配的,Node 提供了 V8 中内存使用量的查看方式,以下:shell

process.memoryUsage()
{ rss: 21434368,
  heapTotal: 7159808,
  heapUsed: 4455120,
  external: 8224 }

其中,heapTotal 和 heapUsed 是 V8 的堆内存使用状况,前者是已申请到的堆内存,后者是当前使用的量。若是已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到堆的大小超过 V8 的限制为止。 至于 V8 为什么要限制堆的大小,主要是内存过大会致使垃圾回收引发 JavaScript 线程暂停执行的时间增加,应用的性能和响应会直线降低,这样的状况不只仅是后端服务没法接受,前端浏览器也没法接受。所以,在当时的考虑下直接限制堆内存是一个好的选择。 不过 V8 也提供了选项让咱们打开这个限制,Node 在启动时能够传递以下的选项:后端

node --max-old-space-size=1700 test.js // 单位为 MB 设置老生代的内存空间
node --max-new-space-size=1024 test.js // 单位为 KB 设置新生代的内存空间

上述参数在 V8 初始化时生效,一旦生效就不能再改变。

V8 的垃圾回收机制

V8 的垃圾回收策略主要基于分代式垃圾回收机制,在实际应用中,人们发现没有一种垃圾回收算法可以胜任全部的场景,由于对象的生存周期长短不一,不一样的算法只能针对特定状况具备最好的效果。所以,现代的垃圾回收算法按对象的存活时间将内存的垃圾回收进行不一样的分代,而后分别对不一样分代的内存施以更高效的算法。 在 V8 中,主要将内存分为新生代和老生代。新生代的对象为存活时间较短的对象,老生代的对象为存活时间较长或常驻内存的对象。

在这里插入图片描述

Scavenge 算法 在分代的基础上,新生代的对象主要经过 Scavenge 算法进行垃圾回收,在 Scavenge 的具体实现中,主要采用了 Cheney 算法。 Cheney 算法是一种采用复制的方式实现的垃圾回收算法,它将堆内存一分为二,每一部分空间称为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另外一个处于闲置状态。处于使用状态的 semispace 空间称为 From 空间,处于闲置状态的空间称为 To 空间。

在这里插入图片描述 当咱们分配对象时,先是在 From 空间中进行分配。当开始进行垃圾回收时,会检查 From 空间的存活对象,这些存活对象将被复制到 To 空间中,而非存活对象占用的空间将被释放。 完成复制后,From 空间和 To 空间的角色发生对换。

  • Scavenge 的缺点是只能使用堆内存中的一半
  • Scavenge 是典型的牺牲空间换取时间的算法,适合应用于新生代中,由于新生代中对象的生命周期较短
  • 当一个对象通过屡次复制仍然存活时,它将会被认为是生命周期较长的对象,其随后会被移动到老生代中,这一过程称为晋升

Mark-Sweep & Mark-Compact 老生代中的对象生命周期较长,存活对象占较大比重,V8 在老生代主要采用 Mark-Sweep 和 Mark-Compact 相结合的方式进行垃圾回收 Mark-Sweep:标记清除,其分为标记和清除两个阶段。在标记阶段遍历堆中的全部对象,并标记活着的对象,在清除阶段只清除没有被标记的对象。Mark-Sweep 最大的问题在于进行一次标记清除回收后,内存空间会出现不连续的状态,内存碎片会对后续的内存分配形成问题,好比碎片空间不足以分配一个大对象致使提早触发垃圾回收。 因而就有了 Mark-Compact:标记整理,简单来讲就是标记完成后加一个整理阶段,存活对象往一端移动(合并),整理完成后直接清理掉边界外的内存。

在这里插入图片描述 Incremental Marking 为了不出现 JavaScript 应用逻辑与垃圾回收器看到的不一致的状况,垃圾回收的 3 种基本算法须要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为全停顿(stop-the-world)。 对于新生代来讲,全停顿的影响不大,可是对于老生代就须要改善。 为了下降全堆垃圾回收带来的停顿时间,V8 采用了增量标记(incremental marking)的技术,大概是将本来一口气停顿完成的动做拆分为许多小“步进”,每作完一“步进”就让 JavaScript 应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。

在这里插入图片描述 V8 后续还引入了延迟清理(lazy sweeping)、增量式整理(incremental compaction)、并发标记 等技术,感兴趣的能够自行了解。

查看垃圾回收日志

启动时添加 --trace_gc 参数,这样在进行垃圾回收时,将会从标准输出中打印垃圾回收的日志信息。 下面是一段示例,执行结束后,将会在 gc.log 文件中获得全部垃圾回收信息:

node --trace_gc -e "var a = []; for (var i = 0; i < 1000000; i++) a.push(new Array(100));" > gc.log

经过在 Node 启动时使用 --prof 参数,能够获得 V8 执行时的性能分析数据:

node --prof test.js
相关文章
相关标签/搜索