垃圾回收和内存回收管理

    2020年11月5日 星期四
    下午3:04

    内存在何时释放?
    引用类型是在没有引用之后,通过 V8 的 GC 自动回收,值类型如果处于闭包的情况下,会在闭包没有引用之后才会被 GC 回收,在非闭包的情况下会在 V8 的新生代切换的时候回收。

    常见的内存泄漏
    在 JS 中内存泄漏的原因是本该被清除的对象,被可到达对象引用之后,未被正确的清除而常驻内存。

    1. 全局变量
    2. 闭包
    3. 事件监听
    4. 大缓存
    5. 请求堆积
    6. 计时器
    7. 回调函数

    V8 的内存管理机制

    1、内存管理模型
    Node 程序运行中,此进程所占用的内存称之为常驻内存,常驻内存由以下部分组成:
    • 代码区(Code Segment):存放即将执行的代码片段
    • 栈(Stack):存放局部变量
    • 堆(Heap):存放对象,闭包上下文
    • 堆外内存:不通过 V8 分配,也不受 V8 管理。Buffer 对象的数据存放于此

    除堆外内存,其余均由 V8 管理。
    • 栈(Stack)的分配与回收非常直接,当程序离开作用域后,其栈指针下移(回退),整个作用域内的局部变量都会出栈,内存回收
    • 最复杂的部分是堆(Heap)的管理,V8 使用垃圾回收机制进行堆的内存管理,也是开发中可能造成内存泄漏的部分。
    通过 process.memoryUsage() 可以查看此 node 进程内存的使用情况。

    process.memoryUsage() { rss: 27684864, // Resident Set Size 的缩写,常驻内存总大小,包含所有 C++ 和 Javascript 对象与代码 heapTotal: 9236480, // 堆内存总大小 heapUsed: 4565816, // 堆内存使用大小 external: 1579662, // V8 管理的绑定到 Javascript 的 C++ 对象的内存使用情况 arrayBuffers: 9410, // 分配给 ArrayBuffer 和 SharedArrayBuffer 的内存,其中就包括了所有 Node.js 的 Buffer,这个包含在 external 中 }

    2、堆内存限制
    默认情况下,V8 为堆内存分配的内存在32位系统下是0.7G,在64位系统下是1.4G。也就是说,如果你想要用一个 node 程序读取一个 2g 的文件到内存,在默认情况下是无法实现的。不过我们可以通过 node 的启动命令来更改 V8 位堆设置的内存上限:
    // 更改老年代内存
    —max-old-space-size=3000 // 单位为MB
    // 更改新生代内存
    —max-semi-space-size=1024 // 单位为MB

    3、V8 的垃圾回收机制

    3.1、V8 的内存分代
    新生代: 年轻的新对象,未经历过垃圾回收机制或者仅经历过一次
    老生代:存活时间较长的老对象,经历过一次或者更多次垃圾回收的对象
    新创建的对象都会分配到新生代中,当新生代的剩余内存不足以分配新对象时,将会触发垃圾回收。

    3.2、新生代的垃圾回收
    新生代主要通过 Scavenge 算法进行垃圾回收,这是一种采用复制的方式实现的内存回收算法。Scavenge 算法将新生代的总空间分成两个同等大小的 semispace,使用其中一个,另外一个闲置,等待垃圾回收的时候使用。使用中的那块称为 From 空间,未使用的称为 To 空间。当新生代触发垃圾回收的时候,会检查 From 空间存活的对象并复制到 To 空间,然后清空 From 空间,再交换 From 空间和 To 空间的位置。
    有两种情况不会将对象复制到 To 空间而是直接晋升到老生代:
    • 对象此前已经经历过一次新生代的垃圾回收,这次依旧存活。
    • To 空间已经使用了25%,则将此对象直接晋升至老生代。

    3.3、老生代的垃圾回收
    老生代主要存放的是生存周期比较长的对象。内存按照1MB分页,并且都按照1MB对齐。新生代的内存是连续的,而老生代的内存是分散的,以链表的形式串联起来。

    1. 对老生代进行第一次扫描,标记存活的
    2. 对老生代进行第二次扫描,清除未被标记的对象
    3. 将存活的对象往内存的一端移动
    4. 清除掉存活对象边界外的内存

    Mark-Sweep(标记清除)
    在标记阶段遍历堆中所有的对象,并标记活着的对象,随后在清除的阶段只清除未被标记的对象。

    Mark-Compact(标记整理)
    标记清除最大的问题是在一次标记清除回收之后。,内存会出现不连续的状态,会对后续的内存分配造成问题。Mark-Compact 是在 Mark-Sweep 的基础上演化而来,在整理的过程中,将活着的对象往一端移动,移动完成之后,清理掉边界外的内存。V8在堆内存空间不足以给新生代晋升过来的对象分配内存时才会使用 Mark-Compact 算法。

    GC Roots
    window、global、内建对象、栈变量

    并发标记(Mark Compact)
    该优化允许 Javascript 程序在垃圾回收器扫描堆进行标记操作的时候可以继续运行,并发标记比在主线程上标记节省了60%-70%的时间,chrome64 和 node.js 10中默认开启。

    三色标记法
    V8 使用每个对象的两个标记位和一个标记工作表来实现标记。两个标记位编码三种颜色:白色(00),灰色(10)和黑色(11)。最初所有对象都是白色,意味着收集器还没有发现他们。当收集器发现一个对象时,将其标记为灰色并推入到标记工作表中。当收集器从标记工作表中弹出对象并访问他的所有字段时,灰色就会变成黑色。这种方案被称作三色标记法。当没有灰色对象时,标记结束。所有剩余的白色对象无法达到,可以被安全的回收。

    强三色不变性
    黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象

    弱三色不变性
    所有被黑色对象引用的白色对象都处于灰色保护状态(直接或间接从灰色对象可达)

    减少暂停标记
    在 nodejs 8.11.1 版本所使用的 v8 版本从 stop-the-world 标记切换到增量标记。在增量标记期间,垃圾收集器将标记工作分解为更小的块,并且允许应用程序在块之间运行。提高了应用程序的响应速度。
    增量标记期间应用程序需要通知垃圾回收器关于改变对象图的所有操作。V8 使用 Dijkstra 风格的写屏障(write-barrier)机制来实现通知。在 Javascript 中,每次形如 object.field = value 的写操作之后,V8 会插入 write-barrier 代码:
    // 调用 object.field = value 之后
    write_barrier(object, field_offset, value) {
    if (color(object) == black && color(value) == white) {
    set_color(value, grey);
    marking_worklist.push(value);
    }
    }