一文带你彻底弄懂ZGC

1 推荐的文章

1.1 必看

干掉1ms以内的Java垃圾收集器ZGC到底是个什么东西?

1.2 选看

ZGC有什么缺点?

2 疑问【皆来自上面两篇文章】

2.1 什么使得用户线程工作的同时,让垃圾收集器可以回收垃圾-读写屏障

ZGC (Z Garbage Collector) 和读写屏障:

ZGC确实使用了读写屏障。读写屏障是一种在对象读取和写入操作时自动执行的额外操作,用于维护垃圾收集器的某些属性。

在ZGC中,读写屏障主要用于两个目的:

  • 并发可重定位: ZGC可以在应用线程运行时移动对象,这要求在每次对象访问时都进行检查,以确保访问的是对象的最新副本。读写屏障用于在对象访问时执行这些检查和必要的调整。

  • 标记: 和其他垃圾收集器一样,ZGC需要标记活对象。它使用读写屏障在对象访问时进行标记。

通过使用读写屏障,ZGC能够在应用线程运行时执行垃圾收集的大部分工作,从而实现极低的停顿时间。

2.2 为什么当GC信息存储在对象头上时,无法立即重用对应的内存空间?【接2.1的问题】

摘自:

如图,这个对象在第二次GC周期开始的时候,地址视图还是M0。如果第二次GC的标记阶段还切到M0视图的话,就不能区分出对象是活跃的,还是上一次垃圾回收标记过的。这个时候,第二次GC周期的标记阶段切到M1视图的话就可以区分了,此时这3个地址视图代表的含义是:

M1:本次垃圾回收中识别的活跃对象。
M0:前一次垃圾回收的标记阶段被标记过的活跃对象,对象在转移阶段未被转移,但是在本次垃圾回收中被识别为不活跃对象。
Remapped:前一次垃圾回收的转移阶段发生转移的对象或者是被应用程序线程访问的对象,但是在本次垃圾回收中被识别为不活跃对象。
现在,我们可以回答“使用地址视图和染色指针有什么好处”这个问题了

使用地址视图和染色指针可以加快标记和转移的速度。以前的垃圾回收器通过修改对象头的标记位来标记GC信息,这是有内存存取访问的,而ZGC通过地址视图和染色指针技术,无需任何对象访问,只需要设置地址中对应的标志位即可。这就是ZGC在标记和转移阶段速度更快的原因。

当GC信息不再存储在对象头上时而存在引用指针上时,当确定一个对象已经无用的时候,可以立即重用对应的内存空间,这是把GC信息放到对象头所做不到的。

我的理解:参考传统的cms或者g1收集器,这种对象头存储了gc信息,也就意味着在垃圾收集阶段,必须产生STW,让用户线程停止工作,直到垃圾收集阶段结束后,被收集的空闲空间才能被用户线程重用。在zgc阶段,垃圾收集和用户线程可以并发的操作一个对象,那么就必须让gc信息独立于对象头存储,这样即使gc线程占用了这个垃圾对象,用户线程还可以通过独立存储的染色指针了解到这个对象的垃圾情况,一旦知道被收集或者发生了转移,用户线程通过读写屏障机制可以立马感知到。

当垃圾收集器将GC信息存储在对象头上时,每次访问对象都需要检查这些信息,以确定对象是否处于活动状态、是否已被标记为垃圾等。这种设计有几个缺点,其中之一就是在对象变成垃圾并且确定可以回收其占用的内存空间之后,不能立即重用这些内存空间。

这是因为即使一个对象已经确定为垃圾,其他线程可能仍然持有到这个对象的引用,并可能尝试访问它。如果垃圾收集器立即回收并重用这块内存空间,这些访问可能会导致未定义行为,因为内存可能已经被重新分配并用于其他目的。

为了安全地回收内存,垃圾收集器需要确保没有任何线程可以再访问到被判定为垃圾的对象。这通常意味着需要等到所有可能访问到对象的线程都到达了一个安全点(safe point),在这个点上它们不会再访问到垃圾对象。这个过程可能会引入额外的停顿时间,尤其是在多线程环境中。

另一方面,ZGC通过使用地址视图和染色指针技术,将GC信息存储在引用指针上,而不是对象头上。这样,即使一个对象变成了垃圾,其他线程持有的引用仍然可以通过地址视图和染色指针的转换来访问到这个对象,直到垃圾收集器确定可以安全地回收和重用这块内存空间。这种设计允许ZGC在应用线程运行时执行大部分垃圾收集工作,从而减少停顿时间,并且能够更快地回收和重用内存空间。

2.3 压缩指针技术

2.3.1 什么是压缩指针技术

这是一种内存优化技术,通过减小对象引用的大小来减少内存占用和提高性能。在启用压缩指针的情况下,对象引用不再直接存储对象的实际内存地址,而是存储一个相对于某个基地址的偏移量。这允许使用更小的数据类型来存储引用,从而减小其大小。

2.3.2 染色指针和压缩指针的区别,染色指针有无用到压缩指针技术

Z Garbage Collector (ZGC) 是一种低延迟的垃圾收集器,自从在JDK 11中首次引入以来,它确实经历了大量的发展和改进。

染色指针和压缩指针是两种不同的技术,但是染色指针中存储的地址信息可以使用压缩指针技术:

  • 染色指针: ZGC使用染色指针来存储关于对象的元数据(如标记信息、转发指针等)。染色指针通过在指针的低位或高位添加额外的位来实现,这些位用于存储垃圾收集器需要的信息。这种技术允许ZGC在不访问对象本身的情况下快速检索这些信息,从而减少延迟。

  • 压缩指针: 这是一种内存优化技术,通过减小对象引用的大小来减少内存占用和提高性能。在启用压缩指针的情况下,对象引用不再直接存储对象的实际内存地址,而是存储一个相对于某个基地址的偏移量。这允许使用更小的数据类型来存储引用,从而减小其大小。

虽然染色指针和压缩指针都涉及对指针的操作,但它们解决的问题和实现的方式是不同的。ZGC确实使用了染色指针技术,但这和压缩指针技术是分开的。ZGC在JDK 15中增加了对压缩类指针的支持,这意味着即使在使用ZGC的情况下,也可以利用压缩指针技术来减小内存占用和提高性能。

2.3.3 jdk15中,压缩指针被用在了哪个地方,染色指针有没有用到这种压缩技术

JDK 15中的压缩指针和染色指针:

  • 压缩指针: 在JDK 15中,ZGC增加了对压缩类指针(Compressed Class Pointers)的支持。这意味着即使在使用ZGC时,也可以利用压缩指针技术来减小Java堆对象的内存占用。压缩类指针主要用于优化对象头中的类元数据指针的存储。通过将这些指针存储为相对于Java堆的某个基地址的偏移量,而不是完整的64位地址,可以减小对象头的大小,从而减小整体的内存占用。

  • 染色指针: ZGC使用染色指针来存储关于对象的元数据,如标记信息和转发指针。染色指针通过在指针的一部分位上存储这些额外的信息来实现。这并不是压缩指针技术,但它是一种利用指针额外位的技术。染色指针主要用于加速垃圾收集过程,而不是为了减小内存占用。

总的来说,虽然压缩指针和染色指针都涉及对指针的操作,但它们解决的问题和实现的方式是不同的。压缩指针用于减小内存占用,而染色指针用于加速垃圾收集过程。

2.3.4 染色指针中存储的是对象的位置,这个位置信息有可能过时是吗?

是的,染色指针中存储的位置信息可能会过时。当垃圾收集器决定移动一个对象时,它会更新对象的位置,并在原始位置留下一个转发指针,指向对象的新位置。如果染色指针中的位置信息过时,加载屏障将介入,使用转发指针来找到对象的当前位置,并更新染色指针以反映最新状态。

2.4 ZGC通过在并发阶段执行大部分工作来实现极低的停顿时间,如何实现的,具体讲讲

Z Garbage Collector (ZGC) 是一种低延迟的垃圾收集器,它通过在并发阶段执行大部分的垃圾收集工作来实现极低的停顿时间。下面是ZGC如何实现这一目标的具体细节:

2.4.1 并发执行

ZGC的大多数阶段都是并发执行的,意味着这些阶段可以与应用线程同时运行,而不需要停止应用线程。这包括标记阶段、重分配阶段、重映射阶段和清理阶段。通过在并发阶段执行这些工作,ZGC能够显著减少停顿时间。

2.4.2 染色指针和加载屏障

ZGC使用染色指针技术来存储对象的元数据,如标记信息和转发指针。这意味着这些信息存储在指针本身中,而不是在对象头中。为了支持这种技术,ZGC使用了加载屏障(Load Barrier),这是一种在读取对象引用时自动执行的操作。

当应用线程尝试通过染色指针访问对象时,加载屏障会介入并执行必要的操作,如解析转发指针以找到对象的当前位置,或者更新染色指针以反映对象的最新状态。这确保了即使在垃圾收集过程中对象被移动,应用线程也能够无缝地访问对象。

2.4.3 分代收集和区域化内存

虽然ZGC不是一个传统的分代垃圾收集器,但它采用了一种类似分代收集的方法,将内存划分为多个区域,并根据对象的生命周期将其分配到不同的区域中。这允许ZGC有效地管理内存,并减少需要在每次垃圾收集周期中处理的对象数量。

2.4.4 并发可重定位

ZGC能够在应用线程运行时移动对象,这是通过并发重分配阶段实现的。在这个阶段,ZGC会识别出需要移动的对象,并将它们复制到新的内存位置,同时更新所有指向这些对象的引用。

这种并发可重定位的能力是ZGC实现低停顿时间的关键。它允许ZGC在应用线程运行时执行大量的内存压缩和碎片整理工作,从而减少了需要在停顿阶段执行的工作量。

2.5 zgc加载屏障

2.5.1 什么是加载屏障?

2.5.2 加载屏障的添加时机?

加载屏障是在读取对象引用时自动添加的。在ZGC中,每次应用线程尝试通过染色指针访问对象时,加载屏障都会介入。加载屏障的目的是确保即使在垃圾收集过程中对象被移动,应用线程也能够无缝地访问对象。

2.5.3 转发指针的存储位置:

转发指针通常存储在对象被移动前的原始内存位置。当对象被移动到新的位置时,原始位置上的内存会被更新为一个转发指针,指向对象的新位置。

2.5.4 用户线程是否参与移动对象的操作:

用户线程不直接参与移动对象的操作。对象的移动是由垃圾收集器在并发重分配阶段执行的。然而,用户线程在通过染色指针访问对象时,可能会触发加载屏障,加载屏障可能会执行一些与对象移动相关的操作,如解析转发指针和更新染色指针。

2.5.5 转移的对象是否都是存活的对象:

是的,转移的对象通常都是存活的对象。垃圾收集器在标记阶段确定哪些对象是存活的,然后在重分配阶段只移动这些存活的对象。目标是将存活的对象集中到连续的内存区域中,从而减少内存碎片并提高内存使用效率。不存活的对象将被垃圾收集器回收,它们占用的内存空间将被释放。

2.6 zgc中的区域也是逻辑划分的吗,像g1一样分成多个大小均等的子区域?

ZGC的内存管理与G1有所不同。ZGC不是将堆划分为多个大小均等的子区域,而是使用一种称为“Colored Pointers and Load Barriers”的技术来管理内存。ZGC的内存是动态划分的,它使用大的内存页来管理堆空间,并在需要时动态地分配和释放这些内存页。这种设计使得ZGC能够更灵活地管理内存,减少内存碎片,并提高内存利用率。

2.7 zgc的生命周期

一文带你彻底弄懂ZGC_第1张图片

2.7.1 ZGC的垃圾收集阶段

  • 初始标记(Initial Mark):

    • 用户线程:继续运行,但是会受到一些停顿的影响,因为ZGC需要标记所有从GC根直接可达的对象。
    • GC线程:标记所有从GC根直接可达的对象,并为并发标记阶段做一些准备工作。
  • 并发标记/对象重定位(Concurrent Mark/Relocation):

    • 用户线程:继续运行,并且在访问对象时可能会触发加载屏障,以帮助标记和重定位对象。
    • GC线程:并发地标记堆中所有可达的对象,并开始重定位一些对象到新的内存位置。
  • 再标记(Remark):

    • 用户线程:可能会受到停顿的影响,因为ZGC需要完成标记过程,并处理一些遗留的工作。
    • GC线程:完成标记过程,处理并发阶段中遗留的一些工作。
  • 并发转移准备(Concurrent Relocation Preparation):

    • 用户线程:继续运行,并且在访问对象时可能会触发加载屏障。
    • GC线程:准备进行对象的并发转移,包括确定哪些对象需要被移动。
  • 初始转移(Initial Relocation):

    • 用户线程:继续运行,但是会受到一些停顿的影响,因为ZGC需要开始移动对象。
    • GC线程:开始移动一些对象到新的内存位置。
  • 并发转移(Concurrent Relocation):

    • 用户线程:继续运行,并且在访问对象时可能会触发加载屏障,以帮助重定位对象。
    • GC线程:并发地移动剩余的对象到新的内存位置。

这个描述提供了一个大致的概览,但是需要注意的是,ZGC的具体实现和行为可能会随着不同版本的JDK而变化。

2.7.2 再标记阶段的遗留工作

在再标记(Remark)阶段,ZGC完成了并发标记阶段可能遗留下来的一些工作。这些工作可能包括:

  • 处理并发标记阶段中遗留的标记任务。
  • 处理并发阶段中新产生的引用关系。
  • 更新和处理内部数据结构,为后续的转移阶段做准备。

2.7.3 为什么再标记和初始转移也涉及到找gc roots动作

  • 初始标记(Initial Mark): 这个阶段确实涉及到扫描所有的GC Roots,以标记所有从GC Roots直接可达的对象。这个阶段的耗时与GC Roots的数量成正比。

  • 再标记(Remark): 这个阶段可能需要再次处理GC Roots,以确保在并发标记阶段新产生的引用关系得到正确处理。此外,再标记阶段还需要完成一些其他的清理和准备工作,以确保转移阶段能够正确执行。

  • 初始转移(Initial Relocation): 在初始转移(Initial Relocation)阶段,ZGC开始移动对象到新的内存位置。处理GC Roots的原因并不是为了防止转移后的空间被占用,而是为了更新指向即将被移动对象的引用。GC Roots是程序运行中的一组引用,它们直接指向堆中的对象。当对象被移动时,这些直接指向对象的引用也必须被更新,以确保它们指向对象的新位置。如果不更新这些引用,程序可能会尝试访问已经不再有效的内存位置,导致错误。

ZGC的设计目标是尽量减少STW阶段的影响,以实现低延迟。虽然初始标记和初始转移阶段的确需要处理GC Roots,但这些阶段通常耗时非常短。再标记阶段虽然可能涉及到更多的工作,但ZGC通过优化和并行处理尽量减少了这个阶段的停顿时间。

2.7.4 每一个阶段有固定的时间段吗?

在初始转移(Initial Relocation)阶段,ZGC开始移动对象到新的内存位置。处理GC Roots的原因并不是为了防止转移后的空间被占用,而是为了更新指向即将被移动对象的引用。GC Roots是程序运行中的一组引用,它们直接指向堆中的对象。当对象被移动时,这些直接指向对象的引用也必须被更新,以确保它们指向对象的新位置。如果不更新这些引用,程序可能会尝试访问已经不再有效的内存位置,导致错误。

2.7.5 垃圾是在转移阶段被回收的吗还是默认被新的对象覆盖就行了?

在ZGC中,垃圾(不再被引用的对象)是在转移阶段间接回收的。具体来说,ZGC通过标记-重定位的方式工作。在标记阶段,它标记出所有存活的对象。在转移阶段,它只移动这些存活的对象到新的内存位置,并更新所有指向这些对象的引用。没有被标记的对象(即垃圾)不会被移动,它们占用的内存空间在转移后变得空闲,可以在后续的内存分配中被重用。因此,垃圾对象的内存是在新对象分配时被覆盖的。

2.8 zgc的缺点

2.8.1 ZGC时Java进程占用三倍内存问题:由于ZGC着色指针把内存空间映射了3个虚拟地址,使得TOP/PS等命令查看占用内存时看到Java进程占用内存过大。此问题不影响操作系统,但是会影响到监控运维工具,需要注意。分析一下是这样的吗

Z Garbage Collector (ZGC) 使用了一种称为“着色指针”(Colored Pointers)的技术来管理堆内存和实现快速的垃圾收集。这种技术涉及到将每个内存页映射到多个虚拟地址空间中,通常是三个。这三个地址空间分别用于不同的目的,如标记活对象、重定位对象等。

这种内存映射方式确实会导致操作系统报告的Java进程内存占用量增加,因为同一块物理内存被映射到了多个虚拟地址空间。然而,这并不意味着Java进程实际上消耗了更多的物理内存。物理内存的实际使用量并没有增加,只是因为多次映射导致在虚拟地址空间中的占用看起来更大。

这种情况可能会对一些监控和运维工具造成影响,因为这些工具可能会根据操作系统报告的内存占用量来生成警告或者报告。如果这些工具没有针对ZGC的内存管理方式进行优化,它们可能会误报Java进程的内存占用过高。

为了准确监控和管理使用ZGC的Java应用,运维团队可能需要更新或配置他们的监控工具,以正确解释ZGC的内存使用情况。这可能包括使用Java自带的工具(如jstat、jmap等)或者其他了解ZGC工作原理的第三方工具来获取更准确的内存使用信息。

2.8.2

你可能感兴趣的:(java,zgc,java)