JVM垃圾回收器

168 阅读8分钟

1. 常用的JVM垃圾回收器

Java虚拟机(JVM)的垃圾收集器(GC)主要有以下几种:

  1. Serial收集器:这是最基本的垃圾收集器,它为单线程环境设计。它只使用一个线程进行垃圾回收,所以在进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集完毕。

  2. Parallel/Throughput收集器:这个收集器的目标是增加吞吐量。它在年轻代使用标记-复制(mark-copy)算法,老年代使用标记-清除-整理(mark-sweep-compact)算法。 Parallel GC对于运行在服务器模式的JVM是默认的垃圾回收器。

  3. Concurrent Mark Sweep (CMS) 收集器:这个收集器是为减少垃圾回收暂停时间而设计的。它允许垃圾回收线程和应用线程同时工作,但这种方法可能会导致更多的CPU资源消耗。

  4. G1收集器:G1是"Garbage-First"的缩写,是一款面向服务器的垃圾收集器。G1收集器将堆内存分割为独立的子区域,并并行、增量地进行垃圾回收。G1旨在满足实时性要求,可以为其设置某项工作(如垃圾收集)在合理的时间范围内完成。

  5. ZGC收集器:ZGC(Z Garbage Collector)是一个可扩展的低延迟垃圾收集器。ZGC的设计目标是处理TB级别的堆内存,而停顿时间不超过10ms,并且不牺牲太多的吞吐性能。

  6. Shenandoah收集器:Shenandoah GC是一个用于OpenJDK的低延迟垃圾收集器。Shenandoah试图以更高的垃圾收集线程并行度来最小化垃圾收集的停顿时间。

以上这些都是常见的JVM垃圾收集器,各自有各自的适用场景,应根据实际应用的需求和特性选择合适的垃圾收集器。

2. Concurrent Mark Sweep (CMS) 收集器

CMS,全名Concurrent Mark Sweep,是一种以获取最小回收停顿时间为目标的收集器。它非常适合运行在服务器环境和多核硬件环境下的应用。以下是CMS垃圾回收器的工作流程,它可以大致分为四个步骤:

  1. 初始标记(Initial Mark, STW):这个阶段的目标是标记所有的GC Roots能直接关联到的对象。需要注意的是,这个阶段是需要暂停所有的工作线程的,但这个阶段一般很快就能完成。

  2. 并发标记(Concurrent Mark):这个阶段就是进行GC Roots Tracing的过程。和初始标记不同,这个阶段是可以和用户线程一起并发执行的。

  3. 重新标记(Remark, STW):为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分标记记录,需要重新进行一次标记。这个阶段也需要暂停所有的工作线程。

  4. 并发清除(Concurrent Sweep):清除掉标记阶段判断出的不再使用的对象。这个阶段也是可以并发执行的。

需要注意的是,CMS收集器在进行老年代的并发清理阶段时,新生代的垃圾收集并不会被暂停,新生代的垃圾收集依然采用"Stop The World"方式进行。

虽然CMS收集器在减少停顿时间上做了很多努力,但是CMS也存在一些明显的缺点:

  1. CMS收集器对CPU资源非常敏感:在并发阶段,会同时启动多个线程进行垃圾回收,增加了对CPU资源的消耗。

  2. 无法处理浮动垃圾:在并发清理阶段,由于用户线程还在运行,所以会产生一些无法回收的垃圾对象。

  3. 产生大量碎片:由于并发清理阶段,CMS采用的是"标记-清除"算法,这会导致内存中产生大量的不连续内存碎片,碎片过多可能会在对象实例化时,导致无法找到足够大的连续内存而提前触发另一次垃圾收集。

总的来说,CMS是一个以获取最小回收停顿时间为目标的收集器,如果服务器对响应时间要求比较严格,且可以容忍高的CPU消耗,那么CMS就是一个比较好的选择。

3. G1收集器

G1收集器(Garbage-First)是一种面向服务器的垃圾收集器,主要面对的需求是:对系统停顿时间的精确控制(可以预测出停顿),以及能够处理大内存(堆大小可以高达数TB)。

G1与其他垃圾收集器相比最大的不同是,它把堆内存划分为多个独立的子区域(Region),同时还保持了分代思想。每个Region都可能是Eden区、Survivor区或者Old区。在逻辑上,所有的Eden区和Survivor区合起来就是新生代,所有的Old区就是老年代。

G1的工作过程可以分为以下几个步骤:

  1. 初始标记(Initial Marking, STW):标记所有从GC Roots开始直接可达的对象。

  2. 并发标记(Concurrent Marking):进行GC Roots Tracing的过程,在此阶段,G1会遍历对象图,标记整个堆中的活动对象。这个阶段可以与应用线程并发执行。

  3. 最终标记(Final Marking, STW):修正并发标记因用户程序继续执行而导致的标记状态变动。这个阶段会处理那些在并发标记阶段发生变化的对象。

  4. 筛选回收(Sorting the Collection Set):首先对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间,来制定回收计划。这意味着G1可以有选择地回收内存区域,首先收集价值最大(垃圾最多)的Region。

  5. 拷贝整理(Evacuation):在新生代区域,采用和其他收集器类似的复制算法,老年代区域如果被选定参与回收,也会采用复制算法,把存活对象复制到新的区域,然后回收老的区域。这一步是STW操作。

其中,初始标记、最终标记和拷贝整理阶段,会触发STW,停止用户线程。但这些阶段一般很快就能完成。

并发标记阶段的工作是在后台完成的,不影响用户线程。但是,这也会消耗一部分系统资源。

需要注意的是,由于G1的设计目标是:将STW停顿时间和分布,变成可预期且可配置的。因此,在默认情况下,G1 GC并不关注使用的CPU资源。但是,你可以通过适当的JVM选项来限制CPU的使用。

总的来说,G1是一种面向服务器的垃圾收集器,如果系统对Java堆的大小和垃圾收集的停顿时间有较高要求,那么G1收集器会是一个很好的选择。

4. ZGC收集器

ZGC (Z Garbage Collector) 是从 JDK 11 开始引入的低延迟垃圾收集器。ZGC 的主要设计目标是处理 TB 级别的堆内存,而停顿时间不超过 10 ms,并且不牺牲太多的吞吐性能。

ZGC 和其他的垃圾收集器主要的不同在于,它采用了一种被称为"颜色指针"的技术,以及使用了读屏障(Read Barrier)。

以下是 ZGC 的工作过程,它大致分为以下四个阶段:

  1. 标记阶段(Mark Start,STW):这个阶段会标记所有的 GC Roots 直接可达的对象,此阶段需要暂停用户线程,但这个阶段一般很快就能完成。

  2. 并发标记阶段(Concurrent Mark):这个阶段会从 GC Roots 开始,进行对象图的遍历。这个阶段的目标是找出所有存活的对象,此阶段可以与用户线程并发执行。

  3. 重分配/消除并发阶段(Relocate Start):ZGC 会选择一些已经填满的 Region 进行压缩。在这个阶段,存活的对象会被复制到新的 Region 中,然后释放旧的 Region,从而达到内存压缩的目的。

  4. 并发消除(Concurrent Relocate):并发地清理未被使用的对象,此阶段也是可以与用户线程并发执行。

在ZGC中,并发标记和并发消除阶段都是与用户线程并发执行的,而且暂停时间通常都在 10ms 以下。在标记阶段和消除阶段之间,ZGC 还可以处理内存碎片,将存活的对象集中存储,释放无用的内存空间,进一步提高内存利用率。

ZGC的设计目标是支持 TB 级别的堆内存,并且停顿时间都在 10ms 以下。如果你的应用程序需要管理大量的内存,并且对延迟有很高的要求,那么 ZGC 可能是一个很好的选择。