掘金 后端 ( ) • 2024-03-28 13:46

前言

JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了ZGC、ZGC核心技术、ZGC的内存划分、ZGC的执行流程、分代ZGC的设计等内容。


一、ZGC介绍

ZGC(Z Garbage Collector)是一种高效且可扩展的低延迟垃圾回收器。在垃圾回收过程中,ZGC通过优化算法和硬件支持,将Stop-The-World(STW)时间控制在一毫秒以内,使其成为追求低延迟应用的理想选择。此外,ZGC支持灵活的堆大小配置,从几百兆到16TB的堆大小均可轻松应对,且堆大小对STW时间的影响微乎其微。

相比之下,G1垃圾回收器在STW时间方面有所不同。G1的STW时间主要来源于其转移阶段,具体包括以下几个步骤:

  1. 初始标记(Initial Mark):STW阶段,采用三色标记法快速标记从GC Root直接可达的对象。此阶段的STW时间通常非常短暂。
  2. 并发标记(Concurrent Mark):并发执行阶段,对存活对象进行标记。这个阶段允许用户线程和GC线程同时工作,从而减少了STW时间。
  3. 最终标记(Final Mark):STW阶段,处理SATB(Snapshot-At-The-Beginning)相关的对象标记。此阶段的STW时间同样较短。
  4. 清理(Cleanup):STW阶段,如果某个区域中没有存活对象,则直接清理该区域。此阶段的STW时间也较短。
  5. 转移(Copy):将存活对象复制到其他区域。这是G1垃圾回收器中STW时间较长的阶段,因为它涉及对象的复制和引用更新。

G1转移时需要停顿的主要原因是确保对象引用的正确性。在转移过程中,如果允许用户线程和GC线程同时工作,可能会出现以下问题:假设对象A引用了对象C,在转移过程中,C对象被复制到新的区域。如果此时用户线程修改了A对象的某个属性(如A.c.count = 2),但实际上这个修改是针对转移前的C对象。当转移完成后,A对象的引用被更新为指向新的C对象,但此时获取A.c.count的值仍然是1,因为修改是在转移前进行的。这种不一致性会导致数据错误。

为了解决这个问题,G1垃圾回收器在转移过程中需要暂停用户线程,确保转移操作的原子性和正确性。然而,ZGC和Shenandoah等新型垃圾回收器解决了这一问题,使得转移过程也能够并发执行,从而进一步提高了性能。

二、ZGC核心技术

1.读屏障(Load Barrier)

在ZGC中,采用了读屏障(Load Barrier)技术来处理对象引用的获取。当尝试获取一个对象引用时,读屏障会检查该引用是否指向了转移后的对象。如果不是,用户线程会将引用更新为指向转移后的对象。这种机制确保了用户线程总是看到最新、最正确的对象状态,从而避免了上述的不一致性问题。这种设计使得ZGC在保持低延迟的同时,也实现了高效的垃圾回收。

2.着色指针(Colored Pointers)

着色指针(Colored Pointers)是一种内存管理优化技术,用于在64位虚拟机中更有效地利用内存地址空间。在64位系统中,指针通常占用8个字节,这足以表示接近无限的内存空间。然而,大多数应用程序使用的内存远小于这个范围,因此指针的高位通常是未使用的。着色指针技术正是利用了这些未使用的位来存储关于对象状态的信息。

着色指针的设计将原本8字节的地址指针巧妙地拆分成了三个部分:

  • 低44位地址:用于直接表示对象的内存地址,这个范围足够覆盖大多数应用程序所使用的内存空间,最多可以表示16TB的内存。

  • 中间4位颜色位:这四位用于存储关于对象状态的信息。每一位只能存储0或1,并且同一时间只有一位可以是1。不同的位代表不同的状态:

    • 终结位:当设置时,表示该对象只能通过终结器(Finalizer)进行访问。
    • 重映射位(Remap) :表示在垃圾收集转移过程后,对象的引用关系已经更新。
    • Marked0和Marked1:用于标记对象是否可达,是垃圾收集过程中的重要标识。
  • 高16位未使用:这些位在当前实现中未被使用,但为未来扩展提供了空间。

应用程序通常使用8个字节的地址来访问对象,但在ZGC中,仅使用其中的44位来表示对象的内存地址。这看似是一个缩减,但实际上并不会引发问题。这是因为应用程序所使用的对象地址主要是虚拟内存地址,这些地址在最终执行时会被操作系统映射到物理内存上。ZGC通过调整操作系统的内存管理逻辑,确保了即使着色指针中的颜色位发生变化,指针依然能够准确地指向目标对象。这样的设计让着色指针技术在保留原有寻址功能的同时,还额外提供了关于对象状态的信息,从而显著提高了垃圾收集的性能和效率。

3.ZGC核心技术总结

1. 着色指针(Colored Pointers)

  • 指针拆分:ZGC的着色指针技术将传统的8字节地址指针创新性地拆分为三部分,不仅保留了指向对象地址的功能,还额外承载了对象当前所属状态的信息。
  • 状态标识:通过指针中的颜色位,ZGC能够迅速识别对象的存活状态,从而优化垃圾回收过程。
  • 系统兼容性:着色指针技术目前不支持32位系统,并且不兼容指针压缩技术。

2. 读屏障(Load Barrier)

  • 状态检查:在用户线程访问对象引用时,读屏障机制会介入检查该对象的当前状态。如果对象的所属状态与当前GC阶段所期望的颜色状态不一致,读屏障会触发相应的处理逻辑。
  • 用户线程参与:在这种情况下,读屏障会指导用户线程完成本阶段所需的转移或更新工作,确保对象引用的正确性。
  • 性能影响:虽然读屏障机制确保了垃圾回收的正确性,但它也会带来一定的性能开销。根据实际应用场景和负载情况,性能损失通常在5%~10%之间。

三、ZGC的内存划分

ZGC(Z Garbage Collector)的内存布局策略是其高效和低延迟特性的关键之一。类似于G1垃圾回收器,ZGC也将堆内存划分为多个独立的区域,这些区域被称为Zpage。然而,与G1不同的是,ZGC对Zpage的划分更加精细,旨在实现更精确的内存管理和控制,从而进一步减少垃圾回收过程中的停顿时间。ZGC的Zpage可以分为三类,分别是小区域、中区域和大区域,它们各自具有不同的容量和用途:

  • 小区域(Small Zpage) :每个小区域的大小为2MB,专门用于存放小型对象,即大小不超过256KB的对象。这种划分方式有助于减少内存碎片,并提高内存利用率。
  • 中区域(Medium Zpage) :每个中区域的大小为32MB,用于存放中等大小的对象,即大小在256KB到4MB之间的对象。中区域的引入进一步扩展了ZGC的适用范围,使其能够处理更大规模的内存需求。
  • 大区域(Large Zpage) :大区域的大小则根据实际需求动态分配,每个大区域只保存一个大于4MB的大型对象。这种设计允许ZGC在必要时为大型对象提供足够的连续内存空间。

通过这种精细的内存划分策略,ZGC能够在垃圾回收过程中实现更精确的内存管理,从而有效控制停顿时间,提高整体性能。

四、ZGC的执行流程

  • 初始标记阶段:在此阶段,ZGC会标记所有由GC Roots直接引用的对象作为存活对象。由于这些存活对象的数量通常不多,因此此阶段的停顿时间非常短暂,对应用程序的影响几乎可以忽略不计。

  • 并发标记阶段:在这一阶段,ZGC会遍历堆中的所有对象,并使用读屏障和写屏障来检查每个对象是否可达。用户线程在此过程中会协助进行标记工作,如果它们发现某个对象尚未完成标记,它们会主动参与标记过程,确保没有遗漏。

  • 并发处理阶段:在这个阶段,ZGC会选择需要转移的Zpage,并创建转移表。这个转移表用于记录每个对象在转移前后的地址映射关系,确保在后续的转移过程中能够准确地找到对象的新位置。

  • 转移开始阶段:ZGC首先会转移与GC Root直接关联的对象。对于不需要转移的对象,它们的remapped值会被设置为1,以避免在后续过程中重复进行转移判断。完成转移后,ZGC会将新旧对象的地址对记录到转移映射表中。

  • 并发转移阶段:在并发转移阶段,ZGC会将剩余的对象转移到新的ZPage中,并在转移后将新旧对象的地址对记录到转移映射表中。转移完成后,原来的Zpage就可以被安全地清空了,但转移表需要保留下来以供后续使用。此时,如果用户线程尝试访问一个对象的引用(例如,对象4引用对象5),它会通过读屏障来检查这个引用。如果发现引用的对象已经完成了转移(例如,对象5已经转移到了新的位置),它会更新这个引用,使其指向对象5的新位置,并将remap标记为1,表示已经完成了重新映射。并发转移阶段结束后,这一轮的垃圾回收就完成了。然而,需要注意的是,此时并没有完成所有指针的重映射工作,这个工作会被推迟到下一阶段,与下一轮的标记阶段一起完成(因为这两个阶段都需要遍历整个对象图)。

第二次垃圾回收

  • 初始标记阶段:在第二次垃圾回收的初始标记阶段,ZGC会再次沿着GC Root标记对象。这是为了确保在上一轮垃圾回收后新增的存活对象也能被正确标记。

  • 并发标记阶段:在并发标记阶段,ZGC会检查每个对象的Marked值。如果Marked为1,表示上一轮的重映射还没有完成,ZGC会首先完成重映射工作,从转移表中找到老对象转移后的新位置,然后再进行标记。如果对象的Remap值为1,则只需要进行标记操作。

  • 并发处理阶段:在并发处理阶段,ZGC会删除转移映射表并释放相应的内存空间。这是因为经过上一轮的转移和标记后,这些映射信息已经不再需要了。

并发转移阶段的并发问题

在ZGC的并发转移阶段,处理并发问题的机制是高度精细化的。这个阶段涉及用户线程和GC线程之间的协作,以确保在对象转移过程中的线程安全和效率。当用户线程尝试转移一个对象时,它会在转移映射表中记录该对象的新地址。然而,如果这个操作与GC线程的计划发生冲突,即GC线程也打算移动这个对象,那么GC线程会采取主动让步的策略。

GC线程在检测到潜在的冲突时,会放弃自己的写入操作,因为它知道用户线程已经先一步进行了转移工作。这种放弃不是简单的停止操作,而是GC线程的一种智能决策,它基于ZGC的并发转移策略和对系统状态的理解。通过这种方式,ZGC确保了并发转移阶段的正确性和效率,避免了线程间的竞争和潜在的冲突。

这种协同工作的方式不仅减少了线程间的交互成本,还提高了系统的整体性能。它充分利用了多核处理器的并行处理能力,使得用户线程和GC线程能够高效协同工作,共同完成对象的转移任务。因此,在ZGC的并发转移阶段,通过用户线程和GC线程之间的智能协作,确保了垃圾收集过程的顺利进行和系统的高效运行。

五、分代ZGC的设计

在JDK 21及之后的版本中,ZGC引入了分代设计,明确区分了年轻代和老年代。这种设计的主要目的是优化垃圾回收的性能和效率。通过将大部分对象的生命周期限制在年轻代,ZGC能够减少对老年代的扫描次数,从而降低了垃圾回收的开销。同时,由于年轻代和老年代的垃圾回收可以并行执行,进一步提高了系统的吞吐量和响应速度。

在分代设计的基础上,ZGC对着色指针进行了相应的调整。原本用于保存8字节地址的指针被拆分成了三个部分,以更好地适应分代垃圾回收的需求:

  • 46位对象地址:这是指针的主要部分,用于直接表示对象的内存地址。46位的宽度可以支持最多64TB的地址空间,为大型应用提供了充足的内存管理空间。
  • 12位颜色位:这些位用于标识对象的颜色状态,是ZGC垃圾回收过程中的关键信息。颜色位可以帮助ZGC快速判断对象是否存活,从而进行有效的垃圾回收。
  • 未使用的4位和2位:指针的最低4位和最高2位在当前设计中并未被使用,为未来的扩展和优化留下了空间。这种设计灵活性使得ZGC能够适应不断变化的应用需求和硬件环境。

通过分代设计和着色指针的调整,ZGC在保持高效垃圾回收的同时,进一步提升了系统的性能和可扩展性。这些改进措施使得ZGC成为大型应用和高性能场景下的理想选择。


总结

JVM是Java程序的运行环境,负责字节码解释、内存管理、安全保障、多线程支持、性能监控和跨平台运行。本文主要介绍了ZGC、ZGC核心技术、ZGC的内存划分、ZGC的执行流程、分代ZGC的设计等内容,希望对大家有所帮助。