文章目录
垃圾回收机制
垃圾回收 (Garbage Collection,GC
),顾名思义就是释放垃圾占用的空间 ,当需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些"自动化"的技术实施必要的监控和调节。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java 垃圾收集器(Garbage Collector, GC)是 Java 虚拟机(JVM)的一部分,它自动管理内存,回收不再使用的对象所占用的内存空间。这有助于防止内存泄漏,并且使得开发人员可以更专注于业务逻辑的编写而不是内存管理。
没有万能的垃圾收集器,只有根据具体应用场景选择适合自己的垃圾收集器 。垃圾收集器是垃圾回收算法(如引用计数法、标记清除法、标记整理法、复制算法等)的具体实现。它的主要任务是识别并回收那些不再被程序使用的对象所占用的内存空间,从而避免内存泄漏和内存溢出的问题。
垃圾收集器分类
就目前来说,JVM 的垃圾收集器主要分为两大类:分代收集器 和分区收集器 ,分代收集器的代表是 CMS
,分区收集器的代表是 G1
和 ZGC
JVM(Java虚拟机)垃圾回收机制 ------ 垃圾收集器
JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version
命令查看):
- JDK 8:
Parallel Scavenge
(新生代)+Parallel Old
(老年代) - JDK 9 ~ JDK20:
G1
ZGC 收集器
ZGC
(Z Garbage Collector) 是一种低延迟、可伸缩性强 的垃圾回收器,是JVM 中的一项重要技术。ZGC的目标是 尽可能地减少垃圾回收对应用程序的停顿时间,并且可以处理大内存堆 。
它于Java 11版本中正式发布,适用于大内存低延迟服务的内存管理和回收,在 128G 的大堆下,最大停顿时间为 1.68 ms ,停顿时间远胜于 G1
和 CMS
。
通过下面的参数启动 ZGC:
java
$ java -XX:+UseZGC className
ZGC的设计原则是给予应用程序更多的时间来执行业务逻辑,以减少垃圾回收的停顿时间。它具有以下特点:
-
低停顿时间:ZGC 以毫秒为单位的短暂停顿时间作为目标,在控制在10ms以内。它通过并发的方式进行垃圾回收,减少对应用程序的影响。即使是大型堆内存,ZGC也能维持非常低的停顿时间。
-
可伸缩性:ZGC 的设计使其能够处理几个字节到数TB范围内的大型堆内存。它采用了柔性的并发策略,允许在并行、并发和单线程模式之间根据需要做出动态调整。
-
不需要设置-Xmx :与传统的垃圾回收器相比,ZGC不要求显式设置最大堆大小。ZGC可以自动按需调整堆的大小,并将内存释放给操作系统。
-
一致的性能:ZGC致力于提供一致的性能,无论是小型应用还是大型内存应用,都希望能够获得稳定的延迟和吞吐量。
ZGC 的性能优势
与 G1
和 CMS
类似,ZGC
也采用了复制算法
ZGC
在复制算法的基础上做了重大优化,ZGC 在标记、转移和重定位阶段几乎都是并发的 ,这是 ZGC
实现停顿时间小于 10ms 的关键所在。
-
ZGC
使用的是柔性并发(Colored Pointers) 的技术,通过将对象指针分成多个颜色(Colored)(指针染色技术),以实现并发的垃圾回收。 -
它在垃圾回收过程中会对引用关系进行处理,并且可以并发复制和重定向对象。同时,
ZGC
还采用了读屏障技术来保护并发操作中的数据一致性。
复制算法
复制算法主要包括以下 3 个阶段:
- 标记阶段:从 GC Roots 开始,分析对象可达性,标记出活跃对象。
-
对象转移阶段:把活跃对象复制到新的内存地址上。
-
重定位阶段:因为转移导致对象地址发生了变化,在重定位阶段,所有指向对象旧地址的引用都要调整到对象新的地址上。
标记阶段因为只标记 GC Roots
,耗时较短。但转移阶段和重定位阶段需要处理所有存活的对象,耗时较长,并且转移阶段是 STW
的,因此,G1 的性能瓶颈就主要在转移阶段。
指针染色
ZGC
在垃圾回收过程中会对引用关系进行处理,并且可以并发复制和重定向对象。同时,ZGC
还采用了读屏障技术来保护并发操作中的数据一致性。
在一个指针中,除了存储对象的实际地址外,还有额外的位被用来存储关于该对象的元数据信息。这些信息可能包括:
- 对象是否被移动了(即它是否在回收过程中被移动到了新的位置)。
- 对象的存活状态。
- 对象是否被锁定或有其他特殊状态。
通过在指针中嵌入这些信息,ZGC
在标记和转移阶段会更快,因为通过指针上的颜色就能区分出对象状态,不用额外做内存访问。
ZGC
仅支持64位系统,它把64位虚拟地址空间划分为多个子空间,如下图所示:
其中:
- 0-4TB 对应 Java 堆
- 4TB-8TB 被称为
M0 地址空间
- 8TB-12TB 被称为
M1 地址空间
- 12TB-16TB 预留未使用
- 16TB-20TB 被称为
Remapped 空间
。
当创建对象时,首先在堆空间申请一个虚拟地址,该虚拟地址并不会映射到真正的物理地址。同时,ZGC
会在 M0
、M1
、Remapped
空间中为该对象分别申请一个虚拟地址,且三个虚拟地址都映射到同一个物理地址。
下图是虚拟地址的空间划分:
三个空间在同一时间只有一个空间有效。ZGC
之所以设置这三个虚拟地址,是因为 ZGC
采用的是"空间换时间 "的思想,去降低 GC
的停顿时间。与上述地址空间划分相对应,ZGC
实际仅使用64位地址空间的第0-41位 ,而第42-45位存储元数据 ,第47-63位固定为0。
由于仅用了第 0~43 位存储对象地址, 2 44 2^{44} 244 = 16TB,所以 ZGC 最大支持 16TB 的堆。而对象的存活信息,则存储在42-45位中,这与传统的垃圾回收并将对象存活信息放在对象头中完全不同。
读屏障
当程序尝试读取一个对象时,读屏障会触发以下操作:
-
检查指针染色:读屏障首先检查指向对象的指针的颜色信息。
-
处理移动的对象:如果指针表示对象已经被移动(例如,在垃圾回收过程中),读屏障将确保返回对象的新位置。
-
确保一致性:通过这种方式,ZGC 能够在并发移动对象时保持内存访问的一致性,从而减少对应用程序停顿的需要。
读屏障可能被 GC 线程和业务线程触发,并且只会在访问堆内对象时触发,访问的对象位于 GC Roots
时不会触发,这也是扫描 GC Roots
时需要 Stop The World
的原因。
下面是一个简化的示例代码,展示了读屏障的触发时机。
java
Object o = obj.FieldA // 从堆中读取引用,需要加入屏障
<Load barrier>
Object p = o // 无需加入屏障,因为不是从堆中读取引用
o.dosomething() // 无需加入屏障,因为不是从堆中读取引用
int i = obj.FieldB //无需加入屏障,因为不是对象引用
ZGC 的工作过程
ZGC 周期由三个 STW(Stop The World) 暂停和四个并发阶段组成:标记/重新映射(M/R)
、并发引用处理(RP)
、并发转移准备(EC)
和 并发转移(RE)
。
Stop-The-World 暂停阶段
-
标记开始(Mark Start)STW 暂停:这是 ZGC 的开始,进行 GC Roots 的初始标记。在这个短暂的停顿期间,ZGC 标记所有从 GC Root 直接可达的对象。
-
重新映射开始(Relocation Start)STW 暂停:在并发阶段之后,这个 STW 暂停是为了准备对象的重定位。在这个阶段,ZGC 选择将要清理的内存区域,并建立必要的数据结构以进行对象移动。
-
暂停结束(Pause End)STW 暂停:ZGC 结束。在这个短暂的停顿中,完成所有与该 GC 周期相关的最终清理工作。
并发阶段
-
并发标记/重新映射 (M/R) :这个阶段包括并发标记和并发重新映射。在并发标记中,ZGC 遍历对象图,标记所有可达的对象。然后,在并发重新映射中,ZGC 更新指向移动对象的所有引用。
-
并发引用处理 (RP) :在这个阶段,ZGC 处理各种引用类型(如软引用、弱引用、虚引用和幽灵引用)。这些引用的处理通常需要特殊的考虑,因为它们与对象的可达性和生命周期密切相关。
-
并发转移准备 (EC) :这是为对象转移做准备的阶段。ZGC 确定哪些内存区域将被清理,并准备相关的数据结构。
-
并发转移 (RE) :在这个阶段,ZGC 将存活的对象从旧位置移动到新位置。由于这一过程是并发执行的,因此应用程序可以在大多数垃圾回收工作进行时继续运行。
ZGC 的两个关键技术:指针染色 和读屏障 ,不仅应用在并发转移阶段,还应用在并发标记阶段:将对象设置为已标记,传统的垃圾回收器需要进行一次内存访问,并将对象存活信息放在对象头中;而在 ZGC 中,只需要设置指针地址的第42-45位即可,并且因为是寄存器访问,所以速度比访问内存更快。
尽管ZGC在降低停顿时间方面表现出色,但与其他垃圾回收器相比,在某些特定场景或对于特定应用程序可能会有一些性能上的差异。因此,在选择垃圾回收器时需根据具体需求和应用程序特点进行评估和选择。