为什么要进行垃圾回收?
在程序执行的过程中,当部分数据不再被需要,就成为了垃圾数据。如果不能及时有效的回收这些垃圾数据,就会造成内存泄漏,浪费系统内存,从而导致程序运行速度变慢,甚至引起系统崩溃。因此,不同的编程语言采用了不同的策略,分为手动回收和自动回收。例如C++采用了手动回收策略,对内存的分配与释放均需要通过代码进行控制。而JavaScript采用了自动回收策略。
JavaScript的自动回收策略
在JavaScript中, 原始数据类型存储在栈空间中,引用数据类型存储在堆空间中。 因此,JavaScript需要分别对栈空间和堆空间进行垃圾回收。
栈空间中的垃圾回收策略
栈空间的垃圾回收,利用 记录当前执行状态的指针(ESP) 实现。
首先,ESP会指向当前正在执行的函数上下文A。当执行上下文A执行完成之后,ESP将会下移到执行上下文B。指针下移时,执行上下文A的内存变为无效内存。当执行上下文B调用新的执行上下文C时,执行上下文C就会覆盖原先执行上下文A所占用的内存。具体过程如下图所示:
因此,在栈空间中,JavaScript引擎通过下移ESP,来释放在栈空间中保存的执行上下文。
堆空间中的垃圾回收策略
堆空间的垃圾回收,利用 垃圾回收器 实现。
在垃圾回收中,一个经常涉及到的概念是代际假说,包括两点:
- 大部分对象在内存中存在的时间很短;
- 长期存活的对象,会存活的更久; 基于代际假说的观点,引用类型的垃圾回收常采取分代收集的策略,即将堆空间划分为新生代与老生代两个区域,分别使用副垃圾回收器与主垃圾回收器进行垃圾回收。具体如下:
| 垃圾回收器 | 回收代际 | 回收对象 |
|---|---|---|
| 副垃圾回收器 | 新生代 | 生存时间短 |
| 主垃圾回收器 | 老生代 | 生存时间长 |
副垃圾回收器
负责新生代的垃圾回收面对的主要问题是频繁的垃圾回收。
副垃圾回收器将新生代空间划分为两个区域,一半为对象区域,一半为空闲区域。所有的新来的对象均会存放在对象区域,当对象区域快被写满时,再进行垃圾回收。具体步骤为:
- 对象区域中的进行垃圾标记;
- 将存活的对象复制到空闲区,并在内存中有序排列;
- 复制完成后,对调空闲区域与对象区域; 在此过程中,复制操作的时间开销最大。为了减小时间开销,新生代的空间往往很小。这就导致新生代的空间很容易被填满,因此,JavaScript垃圾回收器采用了对象晋升策略。若一个对象在两次垃圾回收中,均未被垃圾回收器回收,则会被移动到老生代。
主垃圾回收器
负责老垃圾回收面对的主要问题是对象较大,且对象存活时间长。
对象较大,无法快速复制,使得主垃圾回收器不能采取与副垃圾回收期相同的回收策略。因此,主垃圾回收器往往分成两步进行垃圾回收:
-
标记
从根元素开始遍历,在遍历过程中,可达的对象标记为活动对象,不可达的对象标记为垃圾数据。
-
清除/整理
直接清除不可达对象,就像在内存中掏了一个个空洞,产生大量不连续的内存碎片。因此,可以直接将存活的对象移动到内存的一端,再清掉剩余的内存空间,避免产生内存碎片。
JavaScript程序运行在主线程上,当执行垃圾回收时,就需要暂停正在执行的JavaScript程序,直到垃圾回收完成。这种现象称为全停顿。
主垃圾回收器由于所占空间大,如不做优化,在垃圾回收的过程中会产生全停顿现象。为了降低主垃圾回收器执行回收任务带来的卡顿,往往选择将整体标记过程拆分为子标记过程,在JavaScript主线程中穿插执行。