一、前言
当内存不再占用,此时需要内存管理对不使用的内存区域进行清空操作,这就是所谓的垃圾回收机制。
高级语言解释器内嵌了垃圾回收器,主要是跟踪内存的分配和使用状况,以便当分配的内存不再使用时,自动释放。
二、垃圾回收策略
在内存管理中,垃圾回收策略常用的有引用计数清除和标记-清除。
1、引用计数清除
引用: 在内存管理的环境中,如果一个对象可以有能力访问另一个对象的属性方法,这就叫做这个对象引用另一个对象。(此处对象不仅仅指Object对象,还可表示函数作用域)
引用计数清除: 如果一个对象没有被其他对象引用的话,那么该对象就会被垃圾回收器清理。
限制:当两个对象出现循环引用的时候,此时,垃圾回收器就会判定该对象存在引用,不会对其进行回收。这就可能造成内存泄漏问题。
function circularReference() {
// 创建一个对象A
let objA = {a:1}
// 创建一个对象B
let objB = {b:2}
// objA.a 属性引用 objB 对象
objA.a = objB
//objB.b 属性引用 objA 对象
objB.b = objA
// 此时,objA 和 objB 就会出现循环引用
}
circularReference()
2、标记-清除
这个策略是从根对象开始,遍历收集所有引用的对象和未引用的对象。
例如上面循环引用的问题,如果circularReference 函数执行完成之后,在全局环境中,无法再访问这两个对象,这两个对象就会被标记为清理的对象。这样可以避免循环引用的问题。
三、存储空间类型
在 V8 引擎的存储空间可以大致分为堆和栈。
1、 栈和堆
栈指的是调用栈,栈的特点是先进先出。同时,栈的内存地址是连续的,在创建和释放内存地址时,只需要对指针进行上下移动,适合函数的调用。
1.1 栈的存储类型
因为栈的内存是连续的,这就造成它所占用的内存区域极为受限。主要是用来存储局部变量和函数调用,基本数据类型也保存在栈内存中。
1.2 堆的存储类型
堆内存主要存储的是对象和动态数据。这是内存中区域最大的块,也是垃圾回收(GC)发生的地方。
1.3 两者关联
因为基础数据类型占用的内存空间都是固定长度的,所以存储在栈内存中。而引用类型占用的内存大小不确定,因此使用堆内存进行存储,栈内存中,存储的是引用类型指向堆内存的地址。
2、堆
因为垃圾回收重点指的是在堆内存中发生的过程,因此有必要对其展开来讲。
2.1 堆的存储区域
V8 引擎初始化内存空间,主要将堆分为以下几个区域:
- 新生代内存区
- 老生代内存区
- 大对象区
- 代码区
- 单元区、属性单元区、Map区
1. 新生代区
新生代区一般分配临时内存,存储时间短。被分为两个 semi-space区域,通常新创建的对象会被存储在这两个区域中。
- from space: 表示正在使用的内存区域
- to space: 表示当前闲置的内存区域
2. 老生代区
老生代区一般分配常驻内存,存储的值较为长久。也被分为两个区域Old pointer space和Old data space。主要用来管理GC之后,还存留的指针和数据信息。
- Old pointer space: 主要保存在GC之后,还引用其他对象的指针。
- Old data space: 主要存储在新生代的两个semi-space区域,GC之后的数据。包括没有指向其他对象的数据,例如: String、Number、Arrary等。
3. 大对象区
该区域的存储大小,大大超过了其他存储区域的限制。每一个大对象,都会有独立的内存区域,大对象区域不会被垃圾回收。
4. 代码区
这是一个实时编译(JIT)代码块的区域。此区域不仅可以在内存中编译执行,也可以执行大对象区域的代码。
5. 单元区、属性单元区、Map区
These spaces contain Cells, PropertyCells, and Maps, respectively. Each of these spaces contains objects which are all the same size and has some constraints on what kind of objects they point to, which simplifies collection.
2.2 堆内存的生命周期
1. 新生代区
from: 表示当前正在使用的内存区域,to: 表示当前闲置的内存区域
- 首先创建一个obj对象,他会被分配到from space
- 当进行垃圾回收时,V8将from部分的对象检查一遍,清理没有再引用的对象
- 清理后,如果还存活的对象,会被复制到 to space,然后清理所有 from space中的对象
- 复制完成之后,from space 和 to space进行互换。此时from 转换为to,to 转换为sapce。
这种算法被称为Scavenge算法。
2. 老生代区
新生代中多次进行垃圾回收,仍然存在的对象,会被转移到空间较大的老生代内存中。以下两种情况:
- 在垃圾回收过程中,发现某个对象仍然存留在新生代区,那么将会被转移到老生代区
- 在 from 和 to 进行反转的过程中,如果 to 中的使用量已经超过了 25% ,那么就讲 from 中的对象直接转移到老生代区。
- 标记清除
该阶段分为标记阶段和清除阶段。
标记阶段,会遍历堆中所有的对象,然后对代码执行环境中正在使用的变量和引用变量进行取消标记,剩下的就是需要被标记的变量。
清除阶段,对标记的变量进行内存回收。
- 标记整理
因为堆存储的对象是不连续的,因此,清理之后会产生大量的内存碎片。
V8的解决方案很简单,将还存留的对象向端边靠拢,其余空间直接清空。
- 增量标记
由于JS是单线程的,在执行垃圾回收的过程中,会导致JS运行程序暂停执行。如果垃圾回收的时间过长,就会导致JS运行程序出现假死状态。
V8的解决方案是将标记的过程分割成一个个子标记过程,同时让JS运行程序与垃圾回收交替进行,直到标记完成。