垃圾回收策略

标记清除

JavaScript 自动垃圾收集最常用的方式: 标记清除. 当变量进入环境(函数中声明一个变量)时, 就将这个变量标记为: “进入环境”, 进入环境的变量使用的内存, 在代码执行过程中就有可能会用到, 所以不能回收. 当变量离开环境时, 则将其标记为: “离开环境”, 在下一次垃圾回收时, 清理响应内存.
垃圾收集器咋运行的时候会给存储在内存中的所有变量都加上标记(可以使用任何方式, 重要的是标记策略), 然后去掉环境中正在使用的变量和有被引用的变量的标记, 还有标记的变量会被认为准备删除的变量. 原因是环境中的变量已经无法访问到这些变量了. 最后垃圾收集器清除对应的内存, 销毁那些带标记的值并回收他们所占用的空间.

引用计数

另外还有JavaScript中不太常见的垃圾收集策略: 引用计数. 引用计数的含义是跟踪记录每个值被引用的次数, 当声明了一个变量并将一个引用类型值赋给该变量时, 则这个值的引用次数就是1, 被引用次数 +1, 取消引用次数-1, 当这个值的引用计数为 0时, 说明这个值不会再被其他变量引用, 可以被销毁了. 当垃圾回收器下次运行时, 会释放对应的内存.

引用计数的策略容易出现循环引用, 简单说就是 ObjectA 和 ObjectB 通过各自属性相互引用, 引用计数永远不会变成0, 对用的内存也就不会被回收也就是内存泄漏. 解决办法就是打破引用环就可以了. JavaScript 中在不使用变量后将变量置为 null, 以防止内存泄漏. OC 和 Swift中 使用就是引用计数的垃圾回收策略, 通过弱引用的方式防止循环引用.

性能问题

垃圾回收是周期性运行的, 变量数量多垃圾回收的工作量会很大, 垃圾回收的时间间隔会很影响性能.

内存管理

不使用变量后置为 null.

V8 内存管理

V8 内存构成

Node 构建于 V8 引擎之上, JavaScript 进行前端开发时内存管理的问题不会那么明显. 对于后端程序内存管理就很重要了, 长时间运行的程序一旦出现内存泄漏, 最后服务器的内存资源都会被耗尽的.

Nodejs 常驻内存组成:

  • 代码区(Code Segment): 存放即将执行的代码片段
  • 栈(Stack): 存放即将执行的代码片段
  • 堆(Heap): 存放对象实体, 闭包上下文
  • 堆外内存: 不通过 V8 分配, 也不受 V8 管理. (例如: Buffer对象的数据)

    栈内存又分为很多区域:

  • 新生代内存区:大多数的对象被分配在这里,这个区域很小但是垃圾回收特别频繁

  • 老生代指针区:属于老生代,这里包含了大多数可能存在指向其他对象的指针的对象,大多数从新生代晋升的对象会被移动到这里
  • 老生代数据区:属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针
  • 大对象区:这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象
  • 代码区:代码对象,也就是包含JIT之后指令的对象,会被分配在这里。唯一拥有执行权限的内存区
  • Cell区、属性Cell区、Map区:存放Cell、属性Cell和Map,每个区域都是存放相同大小的元素,结构简单

    但是为了简单就只介绍新老生代的内存管理. (原文找不到了, 只找到了快照 http://webcache.googleusercontent.com/search?q=cache:KwKkZfJ1QycJ:alinode.alicdn.com/blog/37+&cd=19&hl=zh-TW&ct=clnk&gl=us)

V8 垃圾回收

  • 栈内存分配和回收非常直接, 当程序离开某作用域(某函数执行完成后), 其指针下移(出栈), 该作用域的局部变量会出栈, 内存回收. (闭包引用的变量是存储在堆内存中的, 下面有例子)
  • 堆内存回收采用垃圾回收机制进行堆内存的管理, 也是开发中可能造成内存泄漏的部分.
内存限制

老生代内存64位系统下约为1.4G,32位系统下约为0.7G,新生代内存64位系统下约为32MB,32系统下约为16MB

通过

1
2
node --max-old-space-size=xxx(单位MB)
node --max-new-space-size=xxx(单位KB)

设置新生代内存以及老生代内存来破解默认的内存限制。

回收机制
  • 新生代垃圾回收

    新对象都会被分配到新生代, 当新生代空间不足以分配新对象时, 将触发新生代的垃圾回收.
    新生代的对象主要通过 Scavenge 算法进行垃圾回收, 这是一种采用复制的方式实现内存回收的算法.
    Sacvenge 算法将新生代的总空间一分为二, 只使用其中一个, 另一个处于闲置, 等待垃圾回收时使用. 使用中的那块空间成为 From, 闲置的空间称为 To. 当新生代触发垃圾回收时, V8 将 From 空间中所有应该存活下来的对象依次复制到 To 空间.

    有两种情况不会将对象复制到 To 空间, 而是晋升至老年代:

    1. 对象此前已经经历过一次新生代垃圾回收, 这次依旧应该存活, 则晋升至老年代.
    2. To 空间已经使用了 25%, 则将对象直接晋升至老年代. 因为 From 空间复制完成后, To 空间会被作为 From 空间使用, 会为新对象分配内存, 剩余内存太少会影响程序的新对象分配.

    From 空间所有应该存活的对象都复制完成后, 原本的 From 空间将被释放, 成为闲置空间, 原本 To 空间则成为使用 From 空间, 两个空间进行了角色翻转.

    Scavenge 算法优缺点:

    • 优点: 只复制活着的对象, 而根据统计学指导, 新生代中大多数对象寿命都不长, 长期存活对象少, 所以 Scavenge 的效率很高
      Scavenge 是依次连续复制, To 空间不存在内存碎片.
      
    • 缺点: 因为 Scavenge 的复制方式会将新生代对半划分, 导致内存的空间利用率不高.
  • 老生代垃圾回收

    老生代中的对象都是经历过一个或多次垃圾回收的对象, 而且老生代空间要比新生代大得多(一般老生代空间是新生代空间的40 倍), 使用 Scavenge 算法需要复制的对象太多会导致效率降低, 而且空间利用率也很低, 所以老生代不是采用 Scavenge 算法进行垃圾回收的, 而是采用 标记清除和标记整理.

    当老生代触发垃圾回收时, V8 会将需要存活的对象打上标记, 然后将没有标记的对象, 也就是死亡的对象清除, 就完成了一次标记清除.
    被清除的对象是不连续的内存空间, 导致老生代产生很多的闲置的内存碎片, 可能并没有足够大连续的空间存储较大的对象, 此时需要解决空间碎片, 也就是内存整理, 标记整理算法就是j将存活的对象移向内存一侧, 使其连续. 因为标记整理算法需要移动大量的内存空间, 执行耗费大量时间和资源, 因此只有当剩余空间无法存储新的大的对象时才会触发标记整理算法

    增量标记清除

    早期的老生代垃圾回收机制, 采用全停顿, 也就是垃圾回收时程序运行会被暂. 浏览器时代使用内存不多, 卡顿不是很明显, Nodejs 时代后台程序使用大量内存, 全停顿方式很容易带来明显的迟滞, 标记阶段很容易引起卡顿, 因此后期 V8 采用增量标记, 将标记阶段分为若干小步骤, 每个步骤控制在 5ms内, 每执行一段时间标记行为, 就继续运行 JS 程序, 交替进行, 类似于 操作系统的时间片轮转, 提高程序的流畅度.