一 目录

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

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

二 前言

返回目录

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

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

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

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

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

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

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

三 栈垃圾回收

返回目录

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

如果有代码:

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();

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

要回收对象的垃圾数据,就需要 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 国际 许可协议 进行许可。<br/>基于 https://github.com/LiangJunrong/document-library 上的作品创作。<br/>本许可协议授权之外的使用权限可以从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处获得。

jsliang
393 声望31 粉丝

一个充满探索欲,喜欢折腾,乐于扩展自己知识面的终身学习斜杠程序员