jsliang 求职系列 - 19 - 垃圾回收

3,227 阅读6分钟

一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 前言
三 栈垃圾回收
四 堆垃圾回收
五 新生代和老生代
 5.1 新生代 - 副垃圾回收器
 5.2 老生代 - 主垃圾回收器
六 全停顿
七 参考文献

二 前言

返回目录

原始数据类型是存储在栈空间中的,引用类型的数据是存储在堆空间中的。

不过有些数据被使用之后,可能就不再需要了,我们把这种数据称为垃圾数据。

如果这些垃圾数据一直保存在内存中,那么内存会越用越多。

所以我们需要对这些垃圾数据进行回收,以释放有限的内存空间。

这时候就引入了垃圾回收机制。

垃圾回收策略分为 手动回收 和 自动回收 两种:

  • 手动回收:C/C++ 语言可以通过 free() 这些代码来决定何时分配内存、何时销毁内存。
  • 自动回收:JavaScript、Python、Java 等产生的垃圾数据是由垃圾回收器来释放的。

三 栈垃圾回收

返回目录

当函数执行结束,JavaScript 引擎通过向下移动 ESP 指针(记录调用栈当前执行状态的指针),来销毁该函数保存在栈中的执行上下文(变量环境、词法环境、this、outer)。

如果有代码:

function foo(){
  var a = 1;
  var b = { name: 'jsliang' };
  function showName(){
    var c = 2;
    var d = { name: 'zhazhaliang' };
  }
  showName();
}
foo();

在上面代码中,根据浏览器策略:

  1. 先到全局执行上下文
  2. 再到 foo 函数执行上下文
  3. 最后到 showName 函数执行上下文

根据栈的特性,此时 JavaScript 情况应为:

调用栈
showName 函数执行上下文
foo 函数执行上下文
全局执行上下文

然后开始释放过程:

  1. 执行到 showName 函数的时候,创建 showName 函数的执行上下文,并将 showName 函数的执行上下文压入栈中。
  2. 以此同时,我们会有一个记录当前执行状态的指针(称为 ESP),指向调用栈中 showName 函数的执行上下文,表示当前正在执行 showName 函数。
  3. 当 showName 函数执行完毕,函数执行流程进入 foo 函数,这时候 ESP 就下移到 foo 函数的执行上下文。这个下移操作就是销毁 showName 函数执行上下文的过程。

如果 showName 函数下面还调用了另一个函数,那么它会覆盖 showName 函数的内容,用来存放另一个函数的执行上下文。

就是说:当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁保存栈中的执行上下文。

四 堆垃圾回收

返回目录

在上面,我们将栈中的垃圾通过 ESP 进行回收了:

function foo(){
  var a = 1;
  var b = { name: 'jsliang' };
  function showName(){
    var c = 2;
    var d = { name: 'zhazhaliang' };
  }
  showName();
}
foo();

但是,里面的 b 和 d 其实是依旧存在于堆中的。

要回收对象的垃圾数据,就需要 JavaScript 中的垃圾回收器了。

五 新生代和老生代

返回目录

代际假说特点:

  1. 大部分对象存活时间很短
  2. 不被销毁的对象,会活的更久

这两个特点不仅仅适用于 JavaScript,同样适用于大多数的动态语言,如 Java、Python 等。

V8 中会把堆分为 新生代 和 老生代 两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。

新生区的容量没有老生区那么大,所以 V8 分别使用 2 个不同的垃圾回收器,来高效实施垃圾回收:

  • 副垃圾回收器。主要负责新生代的垃圾回收。
  • 主垃圾回收器。主要负责老生代的垃圾回收。

5.1 新生代 - 副垃圾回收器

返回目录

算法:Scavenge 算法

对象区域空闲区域
......

原理:

  1. 把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。
  2. 新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
  3. 先对对象区域中的垃圾做标记,标记完成之后,把这些存活的对象复制到空闲区域中
  4. 完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。

对象晋升策略:经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

5.2 老生代 - 主垃圾回收器

返回目录

  • 算法:标记 - 清除(Mark-Sweep)算法
  • 原理:
  1. 标记:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
  2. 清除:将垃圾数据进行清除。

对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存。

这时候就需要标记整理。


  • 算法:标记 - 整理(Mark-Compact)算法
  • 原理:
  1. 标记:和标记 - 清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素标记为活动对象。
  2. 整理:让所有存活的对象都向内存的一端移动
  3. 清除:清理掉端边界以外的内存

六 全停顿

返回目录

由于 JavaScript 是单线程的,所以一旦执行垃圾回收算法,那正在执行的 JavaScript 脚本需要暂停下来,等垃圾回收完毕之后再恢复脚本执行。

这种行为叫 全停顿(Stop-The-World)。

如果堆中的数据较多,那么回收需要时间,会造成页面卡顿状态,所以为了降低这个卡顿,V8 将标记过程分为一个一个子标记过程,同时垃圾回收标记和 JavaScript 应用逻辑交替进行。

  • 优化算法:增量标记(Incremental Marking)算法
  • 原理:
  1. 为了降低老生代的垃圾回收而造成的卡顿
  2. V8 把一个完整的垃圾回收任务拆分为很多小的任务
  3. 让垃圾回收标记和 JavaScript 应用逻辑交替进行

七 参考文献

返回目录


jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。
基于 github.com/LiangJunron… 上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。