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 Old 收集器

Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程的标记整理算法。
在注重吞吐量以及 CPU 资源敏感的场合,优先考虑 Parallel Scavenge 和 Parallel Old 的组合进行收集。

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。主要应用在互联网或 BS 系统的服务器上,这类应用尤其重视服务器的响应速度,希望停顿时间最短,以给用户最好的体验。
CMS 时基于标记清除算法实现的,主要分为 4 个步骤:

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,还需要知道的几个概念:

G1 收集过程

G1 收集器将 java 堆从一个整体收集变成了一个个 Region 进行收集,收集的过程中,采用垃圾优先,也就是会计算每个 Region 的垃圾回收情况,回收最有利的 Region。
回收的模式分为:Young GC,Mixed GC 和 FullGC。

Young GC

回收的 CSet 就是所有年轻代里面的 Region。
过程为:

Mixed GC

Mixed GC: CSet 是所有年轻代里的 Region 加上在全局并发标记阶段标记出来的收益高的老年代 Region;
1、全局并发标记(global concurrent marking)
全局并发标记包括 5 个步骤:

FullGC

需要非常注意的是,G1 的 FullGC 将是采用 Serial 收集器进行。
这将会导致 STW 发生,这个时间直到收集完成为止。
G1 的 GC 过程会在 Young GC 和 Mixed GC 之间不断地切换运行,同时定期地做全局并发标记,在实在赶不上对象创建速度的情况下使用 Full GC。
因此,这也是我们调优的时候需要重点关注的,G1 的退化情况。调优的目的是尽量保证退化的情况不出现。

总结

本文对 JVM 中的各种 GC 回收器进行了总结,在配置 GC 回收策略的时候,我们需要结合我们的业务场景来进行: