JVM 并发标记
JVM 并发标记(Concurrent Marking)是现代垃圾回收器(如 CMS、G1、ZGC、Shenandoah)实现低停顿回收的核心技术。其原理如下:
1. 目标
- 减少 Stop-The-World 停顿时间,让应用线程和 GC 线程可以同时运行,大部分标记工作在应用运行时完成。
2. 基本流程
-
初始标记(Initial Mark)
- 短暂 Stop-The-World,标记 GC Roots 直接可达对象。
-
并发标记(Concurrent Mark)
- GC 线程和应用线程并发,从初始标记的对象出发,遍历整个对象图,标记所有可达对象。
- 这一步是并发标记的主体,极大减少了应用停顿。
-
重新标记(Remark)
- 再次 Stop-The-World,修正并发标记期间对象引用的变化,确保标记准确无误。
3. 三色标记法
-
并发标记通常采用三色标记算法(白、灰、黑):
- 白色:未访问过的对象(初始状态)。
- 灰色:已访问但引用还未全部处理的对象。
- 黑色:已访问且引用全部处理完的对象。
4. 写屏障机制
-
为了保证并发标记期间对象引用变化不会导致“漏标”,JVM 引入了写屏障(如 SATB 或增量更新):
- SATB(Snapshot-At-The-Beginning) :记录原引用,保证“标记开始时可达的对象”都被标记。
- 增量更新:记录新引用,保证新产生的可达对象不会漏标。
5. 浮动垃圾
- 在一次并发垃圾回收过程中,在标记开始时仍然是“活的”对象,但在 GC 执行期间变成了“不可达”的对象。由于标记阶段已经错过了它们,它们没有被当前这一轮 GC 回收,而“浮动”到下一次 GC 被清理。
6. 优点
- 极大降低 GC 停顿时间,提升应用响应性。
- 支持大堆和高并发场景。
总结:
JVM 并发标记通过三色标记法和写屏障机制,实现了应用线程和 GC 线程的并发工作,保证了标记的准确性和高效性,是现代低停顿垃圾回收器的核心。
JVM 并发标记逐步分析
1. 初始状态
- 所有对象都是白色(未被访问)。
- 应用线程正常运行。
2. 初始标记(Initial Mark,STW)
- 短暂停顿(Stop-The-World) 。
- 从 GC Roots(如栈、静态变量等)出发,将直接可达的对象标记为灰色(已访问但引用未处理),放入待处理队列。
- 其余对象仍为白色。
3. 并发标记(Concurrent Marking,应用线程和 GC 线程并发)
-
GC 线程和应用线程同时运行。
-
GC 线程不断从灰色队列取对象,将其引用的白色对象变为灰色,自己变为黑色(已完全处理)。
-
三色标记法在此阶段起作用:
- 白色:未访问,可能是垃圾。
- 灰色:待处理。
- 黑色:已处理完毕。
-
写屏障机制(如 SATB)启动:
- 当应用线程修改对象引用(如 A 的字段从 B 改为 C),写屏障会把原引用 B 记录到一个辅助结构中。
- 这样即使 B 还没被 GC 线程标记,也能保证它不会被漏标。
4. 重新标记(Remark,STW)
- 再次短暂停顿。
- GC 线程处理写屏障记录的所有对象,确保并发标记期间所有“本应存活”的对象都被正确标记为黑色。
- 修正并发期间对象引用的变化,消除漏标。
5. 并发清理/回收
- 标记完成后,GC 线程并发清理所有白色对象(不可达对象),回收内存。
- 黑色对象(存活对象)不会被回收。
6. 浮动垃圾
- 在一次并发垃圾回收过程中,在标记开始时仍然是“活的”对象,但在 GC 执行期间变成了“不可达”的对象。由于标记阶段已经错过了它们,它们没有被当前这一轮 GC 回收,而“浮动”到下一次 GC 被清理。
- 举例说明,在并发标记开始时,A 引用的是 B,并发标记阶段,应用线程把 A 引用 B 改为了 A 引用 C。因为写屏障只记录并发标记开始时的状态,所以 B 还是“活的”对象,会在下次 GC 时被清理。
7. 总结流程图
- 所有对象白色
- 初始标记(STW):GC Roots → 灰色
- 并发标记:灰色对象处理,引用变灰,自己变黑
- 写屏障:记录原引用,防止漏标
- 重新标记(STW):处理写屏障记录,补充标记
- 并发清理:回收白色对象
- 下次 GC 处理浮动垃圾
一句话总结:
并发标记通过三色标记法和写屏障机制,保证了在应用线程和 GC 线程并发运行时,所有“本应存活”的对象都被正确标记,极大降低了 GC 停顿时间。
引用更改问题
并发标记开始时,A 引用 B,并发标记过程中应用线程把它改成了 A 引用 C,那么 C 会不会被回收?
1. SATB 写屏障的核心点
- 当
A.ref = C
时,写屏障只记录旧引用 B,确保“标记开始时可达的对象”不会被漏标。 - 新引用 C 不会被写屏障记录,所以如果 C 在标记开始时不可达,理论上有漏标风险。
2. 为什么 C 不会被漏标?
-
如果 C 在赋值前已经是可达对象,它会在正常的标记流程中被标记为存活。
-
如果 C 是新分配的对象(如
C = new Object()
),- JVM 的 SATB 实现会在对象分配时立即将新对象标记为“活的” ,即使它还没被正式引用。
- 这通常通过分配时的 pre-mark 或 thread-local buffer 实现,确保新对象不会被 GC 漏标。
3. 总结
- SATB 写屏障只记录旧引用,不记录新引用。
- 新分配的对象会被立即标记为存活,避免被 GC 漏标。
- 这样,无论是旧对象还是新对象,都不会因为 SATB 的机制而被漏标。
一句话总结:
SATB 写屏障配合新对象分配时的即时标记,确保了所有“本应存活”的对象都不会被漏标,无论是旧引用还是新分配的对象。