Java 中常见的垃圾收集器有哪些?
Java 中常见的垃圾收集器有哪些?
回答重点
分为新生代收集器和老年代收集器来看,常见的垃圾收集器包括:
新生代垃圾收集器:
1)Serial 收集器:
- 单线程收集器,适合小型应用和单处理器环境。
- 触发 Stop-The-World(STW)操作,所有应用线程在 GC 时暂停。
- 适用场景:适用于单线程应用和客户端模式。
2)ParNew 收集器:
- 是 Serial 收集器的多线程版本,能够并行进行垃圾收集。
- 与 CMS 收集器配合使用时,通常会选择 ParNew 收集器作为新生代收集器。
- 适用场景:适用于多处理器环境,通常配合 CMS 收集器使用。
3)Parallel Scavenge 收集器(吞吐量优先):
- 也称为 “吞吐量收集器”,追求最大化 CPU 时间的利用率。
- 并行处理新生代垃圾回收,适合大规模后台任务处理,注重吞吐量而非延迟。
- 适用场景:适用于大规模运算密集型后台任务,适合对吞吐量要求较高的场景。
老年代垃圾收集器:
1)Serial Old 收集器:
- Serial 收集器的老年代版本,使用标记-整理(Mark-Compact)算法进行垃圾回收。
- 适用场景:适合单线程环境和低内存使用场景,通常配合 Serial 收集器一起使用。
2)Parallel Old 收集器:
- Parallel Scavenge 收集器的老年代版本,使用多线程并行标记-整理算法。
- 适用场景:适合大规模并行计算的场景,适用于高吞吐量要求的任务。
3)CMS(Concurrent Mark-Sweep)收集器:
- 并发标记-清除收集器,追求低延迟,减少 GC 停顿时间。
- 使用并发标记和清除算法,适合对响应时间有较高要求的应用。
- 缺点:可能会产生内存碎片,并且在并发阶段可能会发生 Concurrent Mode Failure,导致 Full GC。
- 适用场景:适用于对响应时间要求高的应用,如 Web 服务和电商平台。
4)G1(Garbage First)收集器:
- 设计用于取代 CMS 的低延迟垃圾收集器,能够提供可预测的停顿时间。
- 通过分区来管理内存,并在垃圾收集时优先处理最有价值的区域,避免了 CMS 的内存碎片问题。
- 适用场景:适合大内存、多 CPU 服务器应用,尤其在延迟和响应时间敏感的场景中表现出色。
5)ZGC(Z Garbage Collector)收集器:
- 低停顿、高吞吐量的垃圾收集器,停顿时间一般不会超过 10 毫秒。
- 适用场景:适用于需要管理大堆内存且对低延迟要求极高的应用。
它们之间的关系(连线代表可以搭配使用):
扩展知识
垃圾收集算法
- 标记-清除算法:主要用于 CMS,标记存活对象后,清除不可达对象,但容易产生内存碎片。
- 标记-整理算法:用于 G1 和 Parallel Old,标记存活对象后进行整理,避免内存碎片。
JVM 垃圾回收调优思路
- 吞吐量调优:主要关注降低垃圾回收的总时间,通过 Parallel Scavenge 和 Parallel Old 提高 CPU 使用效率。
- 延迟调优:关注最大停顿时间,通过 CMS、G1、ZGC 等收集器降低 STW 停顿时间。
- 堆大小调优:通过合理的堆内存分配和分代比例调优,避免频繁的 Minor GC 和 Full GC。
扩展多个垃圾收集器
1. Serial 收集器
Serial 收集器是最基础、历史最悠久的收集器,它是一个单线程收集器,在进行垃圾回收时,必须暂停其他所有的工作线程,直到收集结束,这是其主要缺点。
它的优点在于单线程避免了多线程复杂的上下文切换,因此在单线程环境下收集效率非常高,由于这个优点,迄今为止,其仍然是 HotSpot 虚拟机在客户端模式下默认的新生代收集器:
2. ParNew 收集器
它是 Serial 收集器的多线程版本,可以使用多条线程进行垃圾回收:
3. Parallel Scavenge 收集器
Parallel Scavenge 也是新生代收集器,基于 标记-复制 算法进行实现,它的目标是达到一个可控的吞吐量。这里的吞吐量指的是处理器运行用户代码的时间与处理器总消耗时间的比值:
1 | 吞吐量 = 运行用户代码时间 \ (运行用户代码时间 + 运行垃圾收集时间) |
Parallel Scavenge 收集器提供两个参数用于精确控制吞吐量:
1)-XX:MaxGCPauseMillis:控制最大垃圾收集时间,假设需要回收的垃圾总量不变,那么降低垃圾收集的时间就会导致收集频率变高,所以需要将其设置为合适的值,不能一味减小。
2)-XX:MaxGCTimeRatio:直接用于设置吞吐量大小,它是一个大于 0 小于 100 的整数。假设把它设置为 19,表示此时允许的最大垃圾收集时间占总时间的 5%(即 1/(1+19) );默认值为 99 ,即允许最大 1%( 1/(1+99) )的垃圾收集时间。
4. Serial Old 收集器
从名字也能看出来,它是 Serial 收集器的老年代版本,同样是一个单线程收集器,采用 标记-整理 算法,主要用于给客户端模式下的 HotSpot 使用:
5. Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,采用 标记-整理 算法实现:
6. CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于 标记-清除 算法实现,整个收集过程分为以下四个阶段:
- 初始标记 (inital mark) :标记 GC Roots 能直接关联到的对象,耗时短但需要暂停用户线程;
- 并发标记 (concurrent mark) :从 GC Roots 能直接关联到的对象开始遍历整个对象图,耗时长但不需要暂停用户线程;
- 重新标记 (remark) :采用增量更新算法,对并发标记阶段因为用户线程运行而产生变动的那部分对象进行重新标记,耗时比初始标记稍长且需要暂停用户线程;
- 并发清除 (inital sweep) :并发清除掉已经死亡的对象,耗时长但不需要暂停用户线程。
其优点在于耗时长的 并发标记 和 并发清除 阶段都不需要暂停用户线程,因此其停顿时间较短,其主要缺点如下:
- 由于涉及并发操作,因此对处理器资源比较敏感。
- 由于是基于 标记-清除 算法实现的,因此会产生大量空间碎片。
- 无法处理浮动垃圾(Floating Garbage):由于并发清除时用户线程还是在继续,所以此时仍然会产生垃圾,这些垃圾就被称为浮动垃圾,只能等到下一次垃圾收集时再进行清理。
7. G1 收集器
G1 (Garbage-Frist) 收集器是一种面向服务器的垃圾收集器,主要应用在多核 CPU 和 大内存的服务器环境中。
G1 虽然也遵循分代收集理论,但不再以固定大小和固定数量来划分分代区域,而是把连续的 Java 堆划分为多个大小相等的独立区域(Region)。每一个 Region 都可以根据不同的需求来扮演新生代的 Eden 空间、Survivor 空间或者老年代空间,收集器会根据其扮演角色的不同而采用不同的收集策略。
上面还有一些 Region 使用 H 进行标注,它代表 Humongous,表示这些 Region 用于存储大对象(humongous object,H-obj),即大小大于等于 region 一半的对象。
G1 收集器的运行大致可以分为以下四个步骤:
1)初始标记 (Inital Marking) :标记 GC Roots 能直接关联到的对象,并且修改 TAMS(Top at Mark Start)指针的值,让下一阶段用户线程并发运行时,能够正确的在 Reigin 中分配新对象。
G1 为每一个 Reigin 都设计了两个名为 TAMS 的指针,新分配的对象必须位于这两个指针位置以上,位于这两个指针位置以上的对象默认被隐式标记为存活的,不会纳入回收范围;
2)并发标记 (Concurrent Marking) :从 GC Roots 能直接关联到的对象开始遍历整个对象图。遍历完成后,还需要处理 SATB 记录中变动的对象。
SATB(snapshot-at-the-beginning,开始阶段快照)能够有效的解决并发标记阶段因为用户线程运行而导致的对象变动,其效率比 CMS 重新标记阶段所使用的增量更新算法效率更高;
3)最终标记 (Final Marking) :对用户线程做一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的少量的 STAB 记录。虽然并发标记阶段会处理 SATB 记录,但由于处理时用户线程依然是运行中的,因此依然会有少量的变动,所以需要最终标记来处理;
4)筛选回收 (Live Data Counting and Evacuation) :负责更新 Region 统计数据,按照各个 Region 的回收价值和成本进行排序,在根据用户期望的停顿时间进行来指定回收计划,可以选择任意多个 Region 构成回收集。
然后将回收集中 Region 的存活对象复制到空的 Region 中,再清理掉整个旧的 Region 。此时因为涉及到存活对象的移动,所以需要暂停用户线程,并由多个收集线程并行执行。