Java8 的 GC 方式
Java8 的 GC 方式
PS : STW 即 stop the world 算法,即停止所有线程。
我们重点看 ParNew 和 ConcMarkSweep(CMS)算法和 G1 算法
jvm 中垃圾回收的算法
标记 - 清除算法
标记 - 清除算法分为标记和清除两个阶段:首先标记出需要回收的对象,在标记完成后统一回收所有被标记的对象。
存在的问题: 一是效率低,标记和清除两个过程效率都不高。二是空间问题,标记清除后会产生大量的不连续的内存碎片。空间碎片太多会导致程序在运行过程中需要分配较大对象时无法找到连续内存而不得不提前触发 GC。
复制算法
为了解决效率问题,复制算法应运而生。它将可用内存分为大小相等的两块,每次只使用其中一块,当其中一块内存耗尽,触发 GC 时就将还存在的对象复制到另外一块内存上面,然后再把已使用过的内存空间一次性清除。这样实现了对整个半区的 GC,内存分配时完全不用考虑碎片的情况。缺点在于这种算法将内存的可用大小缩小了一半。
标记 - 整理算法
复制算法当对象存活率较高的情况时,照样会出现效率低下的问题,另外内存要浪费 50%。为了避免上述问题,出现了 标记 - 整理算法。(mark-compact) 其标记过程与标记 - 清除算法一样,但后续步骤不直接清除,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
这个思想有点像双指针:指针范围以内是有效值,指针范围以外都是可以随意覆盖的值。
分代收集法
根据对象的存活周期将内存分为几块,如当前 hotsport 就分为新生代和老年代,然后在各个年代采用不同的收集算法。新生代采用复制算法,老年代采用标记清除或者标记整理算法。
垃圾收集器
jvm1.8 中支持的垃圾收集器见下图:
上图表示不同的 GC 收集器的组合,根据业务场景的不同,结合各垃圾收集器的特点,在年轻代和老年代我们可以使用不同的垃圾收集器。
Serial 收集器
Serial 收集器是一个单线程收集器,只会使用一条线程去收集,同时需要暂停其他所有工作线程,直至收集结束。
优点:
简单高效,在单 CPU 环境中没有线程开销,可以获得最大的效率。
适用于运行在 Client 模式下的虚拟机。
ParNew 收集器
ParNew 收集器是 Serial 收集器的多线程版本,除了多线程收集之外,其余包括控制参数、收集算法、对象分配规则、回收策略等都与 Serial 收集器一样。
ParNew 收集器是 jvmServer 模式下的首选新生代收集器,除 Serial 收集器外,只有 ParNew 收集器能与 CMS 收集器配合工作。默认开启的收集线程数与 CPU 的数量相同。可以通过 -XX:parallelGCThreads 参数来限制垃圾收集的线程数。
Parallel Scavenge 收集器
Parallel Scavenge 收集器是一个新生代收集器,也采用复制算法,并行多线程收集。特点在于达到一个可控目标吞吐量(Throughput)。
吞吐量 = 运行用户代码的时间/(运行用户代码的时间 +GC 耗时)。
-XX:MaxGCPauseMillis 设置停顿时间。
-XX:GCTimeratio 设置吞吐量。
Parallel Scavenge 收集器 能够根据上述两个参数进行自适应调节。
需要注意的是,该收集器只能用于年轻代,只能与 Serial Old 或者 Parallel Old 搭配使用。
Serial Old 收集器
Serial Old 收集器是 Serial 收集器的老年代版本,同样式一个单线程收集器,使用标记整理算法。收集器的主要意义也是提供给 Client 模式下使用,在 Server 模式下,其主要作用有:
- 搭配 Parallel Scavenge 收集器使用。
- 做为 CMS 收集器的后备方案,当 CMS 出现 Concurrent Mode Failure 时使用。
- 做为 G1 的 fullGC 收集器。这也是我们在使用 G1 的时候需要注意的,G1 应该尽量杜绝 FullGC。否则就会退化为串行 GC。
Parallel Old 收集器
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程的标记整理算法。
在注重吞吐量以及 CPU 资源敏感的场合,优先考虑 Parallel Scavenge 和 Parallel Old 的组合进行收集。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。主要应用在互联网或 BS 系统的服务器上,这类应用尤其重视服务器的响应速度,希望停顿时间最短,以给用户最好的体验。
CMS 时基于标记清除算法实现的,主要分为 4 个步骤:
- 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象,速度很快。
- 并发标记(CMS concurrent mark):进行 roots tracing 过程。
- 重新标记(CMS remark):修正并发标记阶段因用户程序继续运作而导致标记产生变动的哪一部分对象的标记记录,这个极端停顿时间比初始标记长。但远比并发标记短。
- 并发清除(CMS concurrent sweep): 回收资源。
上述步骤中,初始标记、重新标记这两个步骤需要停止所有线程。
CMS 收集器缺点:
- CMS 收集器对 CPU 资源非常敏感,在 CPU 资源很匮乏时,效率会非常低,造成停顿时间过长。
- CMS 收集器无法处理浮动垃圾,即在 CMS 收集器收集过程中新产生的垃圾,如果浮动垃圾较大,会导致 CMS 失败。当 CMS 失败后,会启动后背预案,临时启用 SerialOld 收集器来进行老年代收集。这样停顿时间就会比较长。
- CMS 收集器基于标记清除算法,会产生大量的内存碎片,需要额外开启内存整理。通过参数 -XX:CMSFullGCsBeforeCompation,设置执行多少次不压缩的 GC 后进行一次压缩。
G1 收集器
G1 收集器是在 CMS 收集器基础之上的一个升级。其主要的思路是,在 G1 之前的任何垃圾收集器中,分代的思想,都是讲内存分为地址连续的几部分来进行分代管理和 GC。
但是随着 jvm heap 内存的不断增加,我们知道,在不少应用中可能会用到诸如 32G 这样大的 heap 内存。这就对原有的分代收集带来了新的挑战,无论采取什么垃圾回收算法,到会导致由于 heap 内存的增大导致的一次 GC 耗时特别长。
heap 内存的回收耗时无法预计。为了解决这个问题,G1 增加了 region 的概念。将 heap 内存分为一个个大小相等的 Region,Region 的范围为 1M- 32M。可以根据需要自行配置。(-XX:G1HeapRegionSize=?)
通过上图可以发现,在 G1 中,首先将内存划分为大小相等的 Region,之后再在这些 Region 之上来进行分代划分。这样每个代都将是不连续的 Region 组成。
为什么要这么做呢,其最终目的都是为了实现可控的 StopTheWorld 的时间。
即,不管你提供了多大的内存,我都只用了这个么多,我只在我已经用了的内存上进行 GC。这样才能控制 GC 的时间
G1 的重要概念
此外,关于 G1,还需要知道的几个概念:
- CSet(收集集合):GC 过程记录的可被回收的 Region 的集合。在 CSet 中存活的数据会在 GC 过程中被移动到另一个可用分区,CSet 中的分区可以来自 eden 空间、survivor 空间、或者老年代。
- RSet(Remembered Set 记忆集合):记录了其他 Region 中的对象引用本 Region 中对象的关系,属于 points-into 结构 (谁引用了我的对象)。作用是不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描 RSet 即可。
- Humongous regions:用来存放大于标准的 Region 内存 50% 的大对象区域,如果有些对象大于整个 Region 就会去找连续的 Region 保存,如果没有就会触发 GC。
G1 收集过程
G1 收集器将 java 堆从一个整体收集变成了一个个 Region 进行收集,收集的过程中,采用垃圾优先,也就是会计算每个 Region 的垃圾回收情况,回收最有利的 Region。
回收的模式分为:Young GC,Mixed GC 和 FullGC。
Young GC
回收的 CSet 就是所有年轻代里面的 Region。
过程为:
- 阶段 1:根扫描,静态和本地对象被扫描;
- 阶段 2:更新 RS,处理 dirty card 队列更新 RS;
- 阶段 3:处理 RS,检测从年轻代指向老年代的对象;
- 阶段 4:对象拷贝,拷贝存活的对象到 survivorl/old 区域;
- 阶段 5:处理引用队列,软引用,弱引用,虚引用处理;
Mixed GC
Mixed GC: CSet 是所有年轻代里的 Region 加上在全局并发标记阶段标记出来的收益高的老年代 Region;
1、全局并发标记(global concurrent marking)
全局并发标记包括 5 个步骤:
- 步骤一:初始标记(initial mark,STW):标记了从 GCRoot 开始直接可达的对象。
- 步骤二:根区域扫描(root region scan):G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
- 步骤三:并发标记(Concurrent Marking):G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断。
- 步骤四:重新标记(Remark,STW):该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
- 步骤五:清除垃圾(Cleanup):在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
2、拷贝存活对象(evacuation)
FullGC
需要非常注意的是,G1 的 FullGC 将是采用 Serial 收集器进行。
这将会导致 STW 发生,这个时间直到收集完成为止。
G1 的 GC 过程会在 Young GC 和 Mixed GC 之间不断地切换运行,同时定期地做全局并发标记,在实在赶不上对象创建速度的情况下使用 Full GC。
因此,这也是我们调优的时候需要重点关注的,G1 的退化情况。调优的目的是尽量保证退化的情况不出现。
总结
本文对 JVM 中的各种 GC 回收器进行了总结,在配置 GC 回收策略的时候,我们需要结合我们的业务场景来进行:
- 并行 GC 是 jdk1.8 默认的 GC 回收策略,默认采用 ParNew 收集器 + Parallel Old 收集器来进行。这是一个注重高吞吐量的收集策略。缺点在于 FullGC 的时间可能会不可控。需要注意 STW 对业务的影响。
- 串行 GC 仅仅适用于 -client,其吞吐量和 GC 卡顿时间都比较差。大多数情况下不建议使用。
- CMS GC 可以有效的解决 GC 的 STW 时间不可控的问题,但是带来了吞吐量的降低。且在特定情况下也会退化为串行 GC。此外 STW 时间也不一定是可控的。
- **G1 GC 是 CMS 基础之上的升级,结合了 CMS 的优点,同时对 CMS 吞吐量的情况做了改善,尤其适合大堆内存的情况。**比如 heap 大于 6G,还同时要求延迟也可控,那么 G1 是一个不错的选择。不过需要特别关注的是 G1 退化的情况,如何避免 FullGC 的出现。