垃圾收集的基本理解
GC 的基本算法
标记清除
从根开始将可能被引用的对象用递归的方式进行标记(标记阶段),然后再从根开始将全部对象按顺序扫描一遍,将没有被标记的对象进行回收(清除阶段)。
大多数情况下,这种标记是通过对象内部的标志(Flag)来实现的。标记清除算法的处理时间,是和存活对象数与对象总数的总和相关的。
作为标记清除的变形,还有一种叫做标记压缩(Mark and Compact)的算法,即标记存活对象,压缩(复制)存活对象到内存一端,其他的内存重新回收使用。
这两个的区别是,一个标记然后清理没有标记的,一个是标记然后压缩标记的。
标记清理很明显会产生很多内存碎片,而标记压缩则是产生连续可用的内存块,缺点就是消耗的时间会比标记清理多。
复制收集
标记清除算法有一个缺点,就是在分配了大量对象,并且其中只有一小部分存活的情况下,所消耗的时间会大大超过必要的值,这是因为在清除阶段还需要对大量死亡对象进行扫描。
复制收集(Copy and Collection)则试图克服这一缺点。在这种算法中,会将从根开始被引用的对象复制到另外的空间中,然后,再将复制的对象所能够引用的对象用递归的方式不断复制下去。 复制完成之后,“死亡”对象就被留在了旧空间中。 再将旧空间废弃掉,就可以将死亡对象所占用的空间一口气全部释放出来。
这种方法就是只复制存活的对象,所以存活对象的多少会影响消耗的时间。
这种方法的缺点是需要消耗两倍的空间,一个新空间存之前的对象,一个是旧空间留下了“死亡”的对象。
引用计数
它的基本原理是,在每个对象中保存该对象的引用计数,当引用发生增减时对计数进行更新。
引用计数的增减,一般发生在变量赋值、对象内容更新、函数结束(局部变量不再被引用)等时间点。当一个对象的引用计数变为 0 时,则说明它将来不会再被引用,因此可以释放相应的内存空间。
优点
- 实现简单
- 当对象不再被引用的瞬间就会被释放,其他的 GC 机制中,要预测一个对象何时会被释放是很困难的。
- 而且,由于释放操作是针对每个对象个别执行的,因此和其他算法相比,由 GC 而产生的中断时间(Pause time)就比较短,这也是一个优点。
缺点
- 是无法释放循环引用的对象
例如下面的对象是没有其他对象引用的,但是彼此是有应用,就构成了一个独立的应用块。
A ───→ B
↑ |
| ↓
+───── C
- 引用计数管理并不适合并行处理
如果多个线程同时对引用计数进行增减的话,引用计数的值就可能会产生不一致的问题(结果则会导致内存错误)。为了避免这种
情况的发生,对引用计数的操作必须采用独占的方式来进行。如果引用操作频繁发生,每次都要使用加锁等并发控制机制的话,其开销也是不可小觑的。
GC 的进阶算法
GC 的基本算法,大体上都逃不出上述三种方式以及它们的衍生品。现在,通过对这三种方式进行融合,出现了一些更加高级的方式。这里介绍一下其中最有代表性的三种,即分代回收、增量回收和并行回收。有些情况下,也可以对这些方法中的几种进行组合使用。
分代回收
在一般的程序中大部分对象都会在短时间内成为垃圾,而经过一定时间依然存活的对象往往拥有较长的寿命。如果寿命长的对象更容易存活下来,寿命短的对象则会被很快废弃,那么到底怎样做才能让 GC 变得更加高效呢?如果对分配不久,诞生时间较短的“年轻”对象进行重点扫描,应该就可以更有效地回收大部分垃圾。基于这种思想,可以把内存分代,新创建的对象划分到young generation, 而经过几轮回收后依然存活的对象划分到old generation。那么只要仅仅扫描young generation,就可以回收掉废弃对象中的很大一部分。
Jvm Heap 内存分代抽象
+---------------------+----------------+----------------+-------------+ | Eden | S1 | S2 | Old Gen | | (Young Generation) | (Young Gen) | (Young Gen) | (Tenured) | +----------↓----------+------↓---------+------↓---------+-----↑-------+ │ │ │ │ ├────────── Minor GC (Copying) ────────────┤ │ │ (存活对象在 S1/S2 之间复制) │ │ └───────────────────⇄──────────────────────┘ │ │ Promotion (晋升阈值)
分代回收的实现过程
The young generation consists of eden and two survivor spaces. Most objects are initially allocated in eden. One survivor space is empty at any time, and serves as the destination of live objects in eden and the other survivor space during garbage collection; after garbage collection, eden and the source survivor space are empty. In the next garbage collection, the purpose of the two survivor spaces are exchanged. The one space recently filled is a source of live objects that are copied into the other survivor space. Objects are copied between survivor spaces in this way until they’ve been copied a certain number of times or there isn’t enough space left there. These objects are copied into the old Generation.
从上面的过程可以看出,分代回收也是有使用到 复制收集 算法的。
一个特别的问题
如果只扫描young generation,那么从老年代对新生代的引用就不会被检测到。 为了解决这个问题,在分代回收中,会
对对象的更新进行监视,将从老生代对新生代的引用,记录在一个叫做记录集(remembered set)的表中。在执行minor GC 的过程中,
这个记录集也作为一个根来对待。
要让分代回收正确工作,必须使记录集的内容保持更新。为此,在老生代到新生代的引用产生的瞬间,就必须对该引用进行记录,而负责执行这个操作的子程序,需要被嵌入到所有涉及对象更新操作的地方。
这个负责记录引用的子程序是这样工作的。设有两个对象:A 和 B,当对 A 的内容进行改写,并加入对 B 的引用时,如果 A 属于老生代对象,B 属于新生代对象,则将该引用添加到记录集中。
这种检查程序需要对所有涉及修改对象内容的地方进行保护,因此被称为写屏障(Write barrier)。写屏障不仅用于分代回收,同时也用在很多其他的 GC 算法中。
总结
分代回收通过减少 GC 中扫描的对象数量,达到缩短 GC 带来的平均中断时间的效果。不过由于还是需要进行大回收,因此最大中断时间并没有得到什么改善。从吞吐量来看,在对象
寿命假说能够成立的程序中,由于扫描对象数量的减少,可以达到非常不错的成绩。但是,其性能会被程序行为、分代数量、大回收触发条件等因素大幅度左右。
增量回收
在对实时性要求很高的程序中,比起缩短 GC 的平均中断时间,往往更重视缩短 GC 的最大中断时间。在这些对实时性要求很高的程序中,必须能够对 GC 所产生的中断时间做出预测。例如,可以将“最多只能中断 10 毫秒”作为附加条件。
在一般的 GC 算法中,作出这样的保证是不可能的,因为 GC 产生的中断时间与对象的数量和状态有关。因此,为了维持程序的实时性,不等到 GC 全部完成,而是将 GC 操作细分成多个部分逐一执行。这种方式被称为增量回收(Incremental GC)。
工作原理
-
分片执行:将完整的GC过程分解为多个增量步骤
-
交替执行:在增量步骤之间穿插应用程序的执行
-
渐进完成:经过多个增量步骤后完成整个GC过程
与传统GC的对比
特性 传统GC 增量GC
停顿时间 长(一次性完成) 短(分多次完成)
总耗时 较短 稍长(有额外开销)
适用场景 对延迟不敏感 需要低延迟
// 创建一些对象
let a = { name: "Object A" };
let b = { name: "Object B" };
a.ref = b;
b.ref = a;// 增量GC可能这样工作:
1. 标记阶段开始(标记根对象)- 停顿5ms
2. 程序继续执行- 用户操作可以响应
3. 标记子对象- 停顿5ms
4. 程序继续执行
5. 清理阶段- 停顿5ms
与传统GC的对比
特性 | 传统GC | 增量GC |
---|---|---|
停顿时间 | 长(一次性完成) | 短(分多次完成) |
总耗时 | 较短 | 稍长(有额外开销) |
适用场景 | 对延迟不敏感的系统 | 需要低延迟的系统 |
由于增量回收的过程是分步渐进式的,可以将中断时间控制在一定长度之内。另一方面,由于中断操作需要消耗一定的时间,GC 所消耗的总时间就会相应增加,正所谓有得必有失。
并行回收
并行回收的基本原理是,是在原有的程序运行的同时进行 GC 操作,这一点和增量回收是相似的。
不过,相对于在一个 CPU 上进行 GC 任务分割的增量回收来说,并行回收可以利用多CPU 的性能,尽可能让这些 GC 任务并行(同时)进行。由于软件运行和 GC 操作是同时进行的,因此就会遇到和增量回收相同的问题。为了解决这个问题,并行回收也需要用写屏障来对当前的状态信息保持更新。不过,让 GC 操作完全并行,而一点都不影响原有程序的运行,是做不到的。因此在 GC 操作的某些特定阶段,还是需要暂停原有程序的运行。
垃圾收集器G1简介
G1 is a generational, incremental, parallel, mostly concurrent, stop-the-world, and evacuating garbage collector which monitors pause-time goals in each of the stop-the-world pauses.
G1 的描述里就提到 分代,增量,并行等概念。
而 evacuation 也是类似 复制收集算法,只是基于G1的regions做了优化。
Evacuation
is a region-based, targeted form of copying and collection, optimized for modern, large-heap, multi-threaded applications.
Copy & collect algorithms
are simpler and operate on a coarse level (whole generation or semispace), with less adaptability.
References
松本行弘 - 代码的未来
Garbage-First (G1) Garbage Collector