
这段内容主要讲解了**追踪式垃圾收集(Tracing GC)的核心理论基础以及三种主要算法的演进过程。**
1. 理论基石:分代收集理论 (Generational Collection)
现代商业虚拟机(如HotSpot)之所以高效,是因为它们不是"一视同仁"地回收内存,而是根据对象存活的时间长短,将堆内存划分为新生代(Young Gen)和老年代(Old Gen)。
这种设计建立在三条假说之上:
-
弱分代假说: 绝大多数对象都是"朝生夕灭"的(一出生很快就变成垃圾)。
-
强分代假说: 熬过越多次GC的对象,就越难以消亡(活得越久,越不容易死)。
-
跨代引用假说: 跨代引用(老年代引用新生代)相对于同代引用仅占极少数。
设计推论:
基于前两条假说,新生代适合高频回收(关注"死"的),老年代适合低频回收(关注"活"的)。基于第三条假说,为了解决跨代扫描问题,引入了记忆集(Remembered Set),将老年代切块,只标记有跨代引用的块,从而避免在回收新生代时扫描整个老年代。
2. 三大核心算法演进
垃圾收集算法的发展本质上是对内存空间利用率 和执行效率(停顿时间)的权衡。
2.1 标记-清除算法 (Mark-Sweep)
这是最基础的算法,后续算法多是基于此改进。
-
原理:
-
标记: 找出所有需要回收(或存活)的对象。
-
清除: 统一回收被标记的对象。
-
-
缺点:
-
效率不稳定: 垃圾越多,标记和清除的操作越慢。
-
内存碎片化: 清除后会产生大量不连续的内存碎片,导致无法为大对象分配空间,从而提前触发下一次GC。
-
2.2 标记-复制算法 (Mark-Copy)
为了解决"效率"和"碎片"问题而生,主要用于新生代。
-
原理(半区复制): 将内存分为两块,每次只用一块。满时将存活对象复制到另一块,清空当前块。
-
优化(Appel式回收):
-
由于新生代98%的对象都会死,不需要1:1划分。
-
HotSpot布局: 1个 Eden 区 + 2个 Survivor 区(比例 8:1:1)。
-
利用率: 每次使用 Eden + 1个 Survivor(90%空间),只浪费10%。
-
逃生门: 如果存活对象超过10%,通过分配担保机制直接进入老年代。
-
-
优点: 运行高效,无内存碎片。
-
缺点: 需要浪费一部分空间(原版浪费50%,优化版浪费10%)。

2.3 标记-整理算法 (Mark-Compact)
针对老年代对象存活率高的特点设计。
-
原理: 标记过程同Mark-Sweep,但后续不是直接清除,而是让所有存活对象向内存一端移动,然后清理边界外的内存。
-
权衡(Trade-off):
-
移动对象: 会导致应用程序暂停(Stop The World),增加延迟,但内存规整,后续分配和访问快(高吞吐量)。
-
不移动对象: GC停顿短,但内存碎片化严重,分配慢。
-
-
应用: 关注吞吐量的收集器(如Parallel Scavenge)使用此算法;关注延迟的(如CMS)平时用标记-清除,碎片严重时才用标记-整理。
3. 算法对比总结
| 特性 | 标记-清除 (Mark-Sweep) | 标记-复制 (Mark-Copy) | 标记-整理 (Mark-Compact) |
|---|---|---|---|
| 适用区域 | 老年代 (如CMS) | 新生代 (主流) | 老年代 (主流) |
| 空间开销 | 无 (但有碎片) | 高 (需预留Swap区) | 无 |
| 移动对象 | 否 | 是 | 是 |
| 内存碎片 | 严重 | 无 | 无 |
| 主要优点 | 实现简单,无需移动对象 | 效率极高,无碎片 | 无碎片,内存规整 |
| 主要缺点 | 碎片化导致频繁GC | 空间利用率受限 (浪费10%-50%) | 移动对象需暂停应用 (STW) |
4. 关键术语定义 (防止混淆)
文中特别强调了不同GC类型的定义,这在阅读GC日志时非常重要:
-
Minor GC / Young GC: 只回收新生代(最常见)。
-
Major GC / Old GC: 只回收老年代(目前只有CMS收集器有单独的Old GC行为)。注意:有时Major GC也被指代整堆收集,需看上下文。
-
Mixed GC: 回收整个新生代 + 部分老年代(G1收集器特有)。
-
Full GC: 回收整个Java堆和方法区(代价最大,应尽量避免)。
问答
第一部分:分代收集理论基础
Q1:当前主流商用虚拟机的垃圾收集器大多遵循什么理论进行设计?这个理论的基础是什么?
A: 大多遵循"分代收集"(Generational Collection)理论。
它的基础是两个分代假说(经验法则):
-
弱分代假说:绝大多数对象都是朝生夕灭的(生命周期很短)。
-
强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。
-
引入跨代引用假说:跨代引用相对于同代引用仅占极少数,据此建立记忆集(Remembered Set)。
Q2:基于分代假说,垃圾收集器的一致设计原则是什么?这样做有什么好处?
A: 设计原则是:应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(熬过GC的次数)分配到不同的区域之中存储。通常分为新生代(Young Generation)和老年代(Old Generation)。
好处:
-
针对新生代:如果一个区域大多数对象都是朝生夕灭的,把它们集中在一起,每次回收只关注如何保留少量存活对象,能以极低代价回收大量空间。
-
针对老年代:如果剩下的都是难以消亡的对象,把它们集中在一起,可以使用较低的频率来回收。
-
总体:同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
Q3:分代收集理论面临的一个明显困难是什么?如何解决?
A: 困难是跨代引用。对象不是孤立的,老年代对象可能引用新生代对象。在进行Minor GC(只回收新生代)时,为了确保可达性分析正确,理论上需要遍历整个老年代来查找对新生代的引用,这会带来巨大的性能负担。
解决方案:
引入跨代引用假说(跨代引用相对于同代引用仅占极少数),并据此建立记忆集(Remembered Set)。
记忆集在新生代中建立,它把老年代划分成小块,只标识出哪一块内存存在跨代引用。发生Minor GC时,只需扫描记忆集标识出的包含跨代引用的小块内存,而无需扫描整个老年代。
第二部分:三大核心垃圾收集算法
Q4:请简述"标记-清除"(Mark-Sweep)算法的过程及其主要缺点。
A:
过程:分为"标记"和"清除"两个阶段。首先标记出所有需要回收的对象(或存活对象),标记完成后,统一回收掉所有被标记的对象(或未被标记的对象)。
主要缺点:
-
执行效率不稳定:如果堆中包含大量对象且大部分需要回收,必须进行大量标记和清除动作,效率随对象数量增长而降低。
-
内存空间的碎片化问题:清除后会产生大量不连续的内存碎片。碎片太多会导致以后需要分配大对象时,无法找到足够连续内存而提前触发下一次GC。
Q5:"标记-复制"(Mark-Copy)算法主要是为了解决什么问题而提出的?它非常适合应用于哪个年代?
A: 它主要是为了解决标记-清除算法在面对大量可回收对象时执行效率低的问题(以及碎片问题)。
它非常适合应用于新生代,因为新生代对象具有"朝生夕灭"的特点,存活率低,复制成本小。
Q6:请描述原始的"半区复制"算法以及HotSpot虚拟机中优化的"Appel式回收"策略。
A:
-
原始半区复制:将可用内存按容量划分为大小相等的两块,每次只使用一块。当这一块用完,就将还存活的对象复制到另一块上,然后清理掉已使用过的那一块。缺点是内存利用率只有50%。
-
Appel式回收(HotSpot优化):鉴于新生代98%对象熬不过第一轮收集,不需要1:1划分。
-
做法:把新生代分为一块较大的Eden空间和两块较小的Survivor空间(HotSpot默认比例8:1:1)。
-
过程:每次分配只使用Eden和其中一块Survivor。发生GC时,将Eden和该Survivor中存活的对象一次性复制到另一块Survivor空间,然后清理掉Eden和已用过的那块Survivor。
-
空间利用率:每次新生代可用空间为90%(80% Eden + 10% Survivor),只有10%被"浪费"。
-
Q7:在Appel式回收中,如果Survivor空间不足以容纳一次Minor GC后存活的对象怎么办?
A: 需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)。这些存活对象将通过分配担保机制直接进入老年代。
Q8:为什么老年代不能直接使用"标记-复制"算法?"标记-整理"(Mark-Compact)算法是如何工作的?
A:
原因:老年代对象存活率较高,使用复制算法会进行较多的复制操作,效率降低。更关键的是,如果不想浪费50%空间,就需要额外的空间进行分配担保,以应对所有对象都100%存活的极端情况,而老年代一般没有其他区域为其担保。
标记-整理算法工作过程:
标记过程与"标记-清除"一样,但后续步骤不是直接清理可回收对象,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
Q9:对比"标记-清除"和"标记-整理",它们本质差异是什么?移动存活对象有什么优缺点?
A:
本质差异:前者是非移动式的回收算法,后者是移动式的。
移动存活对象的优缺点(权衡):
-
缺点(风险):在老年代这种大量对象存活的区域移动对象并更新引用,是一种极为负重的操作,且必须全程暂停用户应用程序(Stop The World),增加延迟。
-
优点(收益):如果不移动对象,内存碎片问题只能依赖复杂的内存分配器(如分区空闲分配链表)来解决,这会增加内存访问的负担,进而影响应用程序的吞吐量。移动对象虽然回收时更复杂,但能获得规整的内存空间,使内存分配和访问更高效,从整体吞吐量来看是划算的。