JVM优化之垃圾收集底层算法实现
- 三色标记
- 多标-浮动垃圾
- 多标
- 多标过程
- 浮动垃圾
- 处理浮动垃圾
- 总结
- 漏标-读写屏障
- 漏标
- 读写屏障
- 读屏障
- 写屏障
- 应用实例
- 总结
- 记忆集与卡表
- 记忆集
- 记忆集的作用
- 记忆集的实现
- 卡表
- 卡表的作用
- 卡表的实现
- 应用实例
- 总结
在并发标记过程中,用户线程并未终止,对象间的引用关系就有可能发生变化,多标和漏标的情况就有可能发生。
三色标记
详情见文章JVM优化之三色标记
多标-浮动垃圾
多标-浮动垃圾(Multi-phase Marking and Floating Garbage)是现代垃圾收集器中的概念,特别是在增量或并发垃圾收集器中会遇到。
多标
多标(Multi-phase Marking)是指垃圾收集器在多个阶段(或多个标记周期)中完成标记工作,而不是一次性完成整个堆的标记。这种方法的主要目的是减少垃圾收集过程中对应用程序的停顿时间。
多标过程
- 初始标记(Initial Mark):
这一阶段通常会引起短暂的停顿,垃圾收集器标记从根对象(GC Roots)直接引用的对象。
- 并发标记(Concurrent Mark):
在这一阶段,垃圾收集器与应用程序同时运行,继续标记所有可达的对象。
- 重新标记(Remark):
这一阶段也会引起短暂停顿,用于处理并发标记阶段期间应用程序的变动(即新创建或更新的引用)。
- 清除(Cleanup):
清除阶段回收不可达的对象。
浮动垃圾
浮动垃圾(Floating Garbage)指的是在并发垃圾收集过程中,由于应用程序继续运行并创建或修改对象引用,导致某些对象在当前垃圾收集周期中未被标记为可达,但实际上是可达的。这些对象不会在本次垃圾收集过程中被回收,而会在下一次垃圾收集中被正确处理。
具体来说,浮动垃圾的产生可以这样理解:
- 开始垃圾收集
假设垃圾收集器开始了一个新的标记周期。
- 并发执行
在并发标记阶段,应用程序继续运行并创建了新的对象或修改了引用。
- 未被标记
新创建的对象或者更新的引用在当前标记周期内未被标记。
- 被认为不可达
由于这些对象未被标记,它们被认为是不可达的,但实际上它们可能在应用程序中被引用。
处理浮动垃圾
- 并发标记:
尽量减少浮动垃圾的数量,但完全避免是不可能的。
- 多标策略
通过多次标记来确保尽可能多的对象被正确标记。
- 增量回收
分阶段进行回收,允许在应用程序运行时进行垃圾收集,减少停顿时间。
总结
多标-浮动垃圾是现代垃圾收集器在提高效率和减少停顿时间时必须应对的挑战。通过分阶段的标记和并发执行,垃圾收集器能够在不显著影响应用程序性能的情况下,尽量回收内存并管理浮动垃圾。
漏标-读写屏障
漏标(Missing Mark)和读写屏障(Read/Write Barrier)是现代垃圾收集器用来解决并发和增量垃圾收集过程中挑战的重要技术概念。
漏标
漏标(Missing Mark)是指在并发标记过程中,由于应用程序继续运行,某些对象或引用没有被正确标记为可达的情况。这会导致垃圾收集器错误地认为这些对象是垃圾,从而可能回收仍在使用的对象。这种情况尤其在并发或增量垃圾收集器中容易发生,因为垃圾收集和应用程序逻辑是同时进行的。
读写屏障
为了应对漏标问题,垃圾收集器引入了读写屏障(Read/Write Barrier)机制。这些屏障是插入到对象引用读写操作中的特殊代码,确保在并发标记过程中正确跟踪对象引用的变化。读写屏障帮助垃圾收集器维护对象图的准确性,防止漏标和浮动垃圾问题。
读屏障
读屏障(Read Barrier)是在对象引用发生读操作时执行的代码。读屏障的使用比写屏障少见,但在某些垃圾收集器中(如Shenandoah和ZGC)非常重要。
- 并发标记和压缩
读屏障可以帮助处理并发标记和压缩过程中对象的引用更新,确保对象在被访问时处于正确的状态。
- 转发指针(Forwarding Pointer)
在对象被移动时,读屏障可以检查并跟随转发指针,以访问对象的新位置。
写屏障
写屏障(Write Barrier)是在对象引用发生写操作时执行的代码。
它主要用于:
- 卡表(Card Table)维护
写屏障可以将包含新引用的内存区域标记为脏,以便垃圾收集器在下一次回收时检查这些区域。
- 记忆集(Remembered Set)更新
当一个引用被更新时,写屏障可以将该引用加入到记忆集中,以确保在增量或并发标记阶段不会漏掉这些引用。
- 跨代引用处理
在分代垃圾收集中,写屏障可以用于跟踪从老年代到新生代的引用,确保这些引用在垃圾回收过程中被正确处理。
应用实例
- Java G1垃圾收集器
G1(Garbage-First)垃圾收集器使用写屏障来维护记忆集和卡表,以支持其增量和并发回收机制。在G1中,写屏障确保更新后的引用被正确标记和处理,从而避免漏标。
- Shenandoah和ZGC
Shenandoah和ZGC是Java虚拟机中的低延迟垃圾收集器,它们利用读屏障和写屏障来支持并发标记和压缩。读屏障在这些垃圾收集器中尤为重要,因为它们需要处理对象在并发移动过程中被访问的情况。
总结
漏标和读写屏障是并发和增量垃圾收集器中关键的技术挑战和解决方案。通过使用读写屏障,垃圾收集器能够准确地跟踪对象引用的变化,确保在并发环境中正确标记和回收对象,避免漏标和浮动垃圾问题。这些技术的应用使得现代垃圾收集器能够在减少停顿时间的同时,保持高效的内存管理。
记忆集与卡表
记忆集(Remembered Set)和卡表(Card Table)是现代垃圾收集器中用于优化和管理内存的两个重要数据结构。它们主要用于分代垃圾收集(Generational Garbage Collection)策略中,以有效地跟踪跨代引用和改进垃圾收集性能。
记忆集
记忆集是一个数据结构,用于跟踪跨代引用,即从老年代(Old Generation)指向新生代(Young Generation)的引用。在分代垃圾收集器中,新生代垃圾收集(Minor GC)通常比老年代垃圾收集(Major GC)更频繁且更快。为了避免每次新生代垃圾收集都扫描整个老年代,记忆集记录了老年代中的哪些对象包含对新生代对象的引用。
记忆集的作用
- 减少扫描范围
在新生代垃圾收集时,只需扫描记忆集中记录的老年代对象,而不是整个老年代,从而提高效率。
- 跨代引用跟踪
确保所有从老年代到新生代的引用在垃圾收集过程中都能被正确处理,防止误回收仍在使用的对象。
记忆集的实现
1.对象粒度
记录具体哪些对象包含跨代引用。常用于较细粒度的跟踪,但可能增加开销。
2.卡表粒度
将内存分成固定大小的卡片(Card),记录哪些卡片包含跨代引用。这种方法比较粗粒度,但开销较小。
卡表
卡表是一个位图或数组,用于分代垃圾收集器中标记哪些内存区域(卡片)可能包含对新生代对象的引用。卡片是一段固定大小的内存块,卡表的每一位对应一张卡片。
卡表的作用
- 跨代引用跟踪
在对象引用发生变化时,通过写屏障更新卡表,标记那些可能包含跨代引用的卡片。
- 加速扫描
在新生代垃圾收集时,垃圾收集器只需扫描卡表中标记为脏的卡片,而不是整个老年代,从而提高垃圾收集的速度和效率。
卡表的实现
- 卡片大小
通常卡片大小为512字节到1KB,具体大小取决于实现和优化目标。
- 写屏障
在对象引用发生写操作时,写屏障会将相关的卡片标记为脏,以便在垃圾收集时进行检查。
应用实例
- Java G1垃圾收集器
G1垃圾收集器使用记忆集和卡表来优化其分代垃圾收集机制。记忆集用于跟踪老年代中的跨代引用,而卡表用于快速标记哪些卡片可能包含跨代引用。在新生代垃圾收集时,G1只需扫描记忆集和卡表,从而减少了老年代的扫描开销。
- HotSpot JVM
HotSpot JVM中的分代垃圾收集器(如Parallel GC、CMS)也广泛使用记忆集和卡表。写屏障用于在对象引用发生变化时更新卡表,确保跨代引用被正确标记和处理。
总结
记忆集和卡表是分代垃圾收集器中用于优化内存管理和垃圾收集性能的重要数据结构。记忆集通过记录跨代引用,减少了新生代垃圾收集时的扫描范围。卡表通过标记可能包含跨代引用的内存区域,加速了垃圾收集的过程。它们共同作用,使现代垃圾收集器能够高效地管理内存,同时保持较低的停顿时间。