V8引擎的垃圾回收机制

207 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

什么是垃圾回收机制,JavaScript和C的垃圾回收有何不同?

垃圾回收机制是指在代码执行时,所在代码运行的环境会进行内存管理的行为。

C是如何释放内存的呢?

同为弱类型语言的c/c++使用的是手动回收策略,从分配内存到执行代码逻辑再到释放内存。我们知道栈空间是有限的,在比较庞大的逻辑运行中需要使用到堆空间。首先用malloc()分配内存空间。而后在该内存空间中写入逻辑数据。使用结束后调用free()来释放这段内存空间。如果使用完毕还没有释放该段内存空间,我们把这种情况叫做内存泄漏。

JavaScript是如何释放内存的呢?

JavaScript使用的是自动垃圾回收的策略,目前比较主流的语言java,python等都是自动垃圾回收的机制。是通过运行环境中内置的垃圾回收器来释放的,JavaScript并没有放开可以手动开启垃圾回收机制的方法。但是这并不意味着我们在日常coding中就可以随意挥霍有限的内存空间。这是一个很大的误区。

接下来我们就来讲讲JavaScript的垃圾回收原理

垃圾回收的工作流程

第一步打标记: 标记内存空间中活动对象,和非活动对象。活动对象是正在使用的内存数据,非活动对象是已经使用完毕且不再使用的数据。

第二步回收: 回收掉已经标记为非活动对象的内存数据。

第三步内存碎片整理: 堆是不是一段连续的内存空间。当回收结束后,内存中就会出现大量的不连续的内存。这种不连续的空间我们叫做内存碎片。了解操作系统的同学知道,当一个任务需要较大的内存空间时,内存碎片过多会分配不了这个任务所需要的的内存空间导致程序的崩溃。所以最后一步就是整理这么不连续的空间,做到执行效率的最大化。

调用栈的垃圾回收

下面有请我们的老演员(代码):

var a = 2

function p2(b,c){
  return b+c
}
//p1中调用p2
function p1(b,c){
  var d = 10
  result = p2(b,c)
  return  a+result+d //2+9+10
}
//全局下调用p1
console.log(p1(3,6));//21

这是代码中的所有执行上下文入栈后的调用栈使用情况。

95280ed39a3d8063af9d6a26065770f.png

我们发现多了一个ESP指针,ESP指针指到谁说明谁就是处在执行状态。在p2函数执行完毕后,执行上下文应当出栈并且释放p2函数在堆所占用的内存空间。随后ESP指针下移。

我们可以理解为ESP指针是一直指向栈顶元素的

5908bce754d533ccaa0c4b69725a778.png

p1执行结束p1执行上下文出栈,释放p1所占用的内存,ESP指针下移

ba6c874642f06c4e95d9eb7e53a75e7.png

随后在全局打印p1的执行结果,全局上下文出栈,释放代码所占用的内存空间

以上就是栈的垃圾回收过程,下面我们带着调用栈的出栈过程看看存储在堆里的数据如何清理

堆的垃圾回收

  • 代价假说(The Generational Hypothesis):

其实一开始知道这个词儿的时候一直不理解,后面就知道了。这玩意儿是直译来的,不需要戴着有色眼镜去看它。

代际假说的特点:

  1. 大部分对象在内存中存在的时间是非常短暂的,或者说给对象分配内存后,很快就变得不可访问。
  2. 不死的对象,会活得更久。

我们可以这么理解这两句话:

第一,垃圾回收机制回收了谁?回收那些分配内存后,很快就变得不可访问的对象。

第二,垃圾回收机制不回收谁?如果存在还可以继续访问的对象,那么就让他活得久一点。

这就是JavaScript垃圾回收机制的设计思想

如何回收很快就变得不可访问的对象(分代收集)

V8引擎把堆空间的管理分为了两块区域:

  • 新生代区
  • 老生代区

新生代区(副垃圾回收器)

在新生代区中划分成对象区域(存放区域)、空闲区域(镜像区域)。

每当对象区域的即将存满时,自动执行一次垃圾清理操作(标记→回收→整理)。

标记阶段

把调用栈中的执行上下文遍历,从而判断堆空间中数据的可达性。标记为非活动对象

回收整理阶段

把标记为活动对象的复制到空闲区域。再把对象区域置为空,原空闲区域设置对象区域,原对象区域设置为空闲区域。这样一来内存碎片被整理成了连续的内存空间。

晋升机制

经过两次新生代垃圾回收机制还存在的对象,会被移至老生代区。

d8111466b41f6bfec7afb4a29c2e637.png

老生代区(主垃圾回收器)

老生代区存放的是一些,占用空间大或者存活时间长的对象,无非就是这两个特点。

首先遍历调用栈,标记非活动对象,释放非活动对象的占用空间。 唯一不同的是整理算法,如果和新生代区一样使用镜像复制的话,进行大文件复制会非常影响性能,而且会占用一半的内存空间。老生代整理是直接把存活的对象移动到堆的一端。从而可以把内存碎片整理为连续的空间。

f5ef1a629cc234bad13c8fa46d7f814.png

8ae3056889364b0d2796f70996a95a8.png