写在前面
前面学习了JVM的垃圾识别方法、垃圾回收算法等理论知识,真正要进行垃圾回收的是是实现这些算法的垃圾回收器。本篇我们介绍的垃圾回收器还是以 OracleJDK 中自带的 HotSpot 虚拟机为主。HotSpot 中使用的垃圾收集器主要包括 7 个:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS 和 G1(Garbage First)收集器。如下图:
由上图可看出,其中年轻代收集器共三种(Serial,ParNew,Parallel Scavenge)、老年代收集器三种(Serial Old,Parallel Old,CMS),横跨年轻代与老年代的收集器一种(G1(Garbage First))。JDK1.8 默认垃圾收集器Parallel Scavenge(新生代)+Serial Old(老年代)。
下面我们就一起来看下各种垃圾收集器的特点及用法。
Serial 收集器属于最早期的垃圾收集器,也是 JDK 1.3 版本之前唯一的垃圾收集器。它是单线程运行的垃圾收集器,其单线程是指在进行垃圾回收时所有的工作线程必须暂停,直到垃圾回收结束为止。
优势: Serial 收集器的特点是简单和高效,并且本身的运行对内存要求不高,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开。
ParNew 收集器实际上是 Serial 收集器的多线程并行版本,运行示意图如下图所示:
Parallel Scavenge 收集器和 ParNew 收集器类似,它也是一个并行运行的垃圾回收器;不同的是,该收集器关注的侧重点是实现一个可以控制的吞吐量。而这个吞吐量计算的也很奇怪,它的计算公式是:用户运行代码的时间 / (用户运行代码的时间 + 垃圾回收执行的时间)。比如用户运行的时间是 8 分钟,垃圾回收运行的时间是 2 分钟,那么吞吐量就是 80%。Parallel Scavenge 收集器追求的目标就是将这个吞吐量的值,控制在一定的范围内。
Parallel Scavenge 收集器有两个重要的参数:
-XX:MaxGCPauseMillis 参数:它是用来控制垃圾回收的最大停顿时间;
-XX:GCTimeRatio 参数:它是用来直接设置吞吐量的值的。
Serial Old收集器是Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下虚拟机使用。
如果在Server模式下,它主要还有两大用途:
1.与Parallel Scavenge收集器搭配使用
2.作为CMS收集器的后备预案,在并发收集发生Conurrent Mode Failure使用。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器.
CMS(Concurrent Mark Sweep)收集器与以吞吐量为目标的 Parallel Scavenge 收集器不同,它强调的是提供最短的停顿时间,因此可能会牺牲一定的吞吐量。它主要应用在 Java Web 项目中,它满足了系统需要短时间停顿的要求,以此来提高用户的交互体验。
Garbage First(简称 G1)收集器是历史发展的产物,也是一款更先进的垃圾收集器,主要面向服务端应用的垃圾收集器。它将内存划分为多个 Region 分区,回收时则以分区为单位进行回收,这样它就可以用相对较少的时间优先回收包含垃圾最多区块。从 JDK 9 之后也成了官方默认的垃圾收集器,官方也推荐使用 G1 来代替选择 CMS 收集器。
下面是对垃圾收集器的一些总结:
收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,将来替换CMS |
下面我们就来学习下垃圾收集器的一个重要理论,即“分代收集理论”,因为目前商用虚拟机的垃圾收集器都是基于分代收集的理论进行设计的,它是指将不同“年龄”的数据分配到不同的内存区域中进行存储,所谓的“年龄”指的是经历过垃圾收集的次数。这样我们就可以把那些朝生暮死的对象集中分配到一起,把不容易消亡的对象分配到一起,对于不容易死亡的对象我们就可以设置较短的垃圾收集频率,这样就能消耗更少的资源来实现更理想的功能了。
通常情况下分代收集算法会分为两个区域:新生代(Young Generation)和老年代(OldGeneration),其中新生代用于存储刚刚创建的对象,这个区域内的对象存活率不高,而对于经过了一定次数的 CG 之后还存活下来的对象,就可以成功晋级到老生代了。
前面我们说了CMS是java8中使用的垃圾回收器,那下面我怕们就来看下CMS的执行流程:
它的整个过程分为四个阶段:
初始标记(CMS initial mark)阶段,这步的执行时间很短,它只是标记一下 CG Roots 的关联对象;
并发标记(CMS concurrent mark)阶段,这步是从 GC Roots 关联的对象进行遍历判断并标识死亡对象,这个过程比较慢,但不需要停止用户线程,用户的线程可以和垃圾收集线程并发执行;
重新标记(CMS remark) 阶段,这步是为了判断并标记,刚刚并发阶段用户继续运行的那一部分对象,所以此阶段的执行时间也比较短;
并发清除(CMS concurrent sweep) 阶段,这步就是清除上面标记的死亡对象,由于 CMS 使用的是标记-清除算法,而非标记-整理算法,因此无须移动存活的对象,这个阶段垃圾收集线程也可以和用户线程并发执行。
它的流程图如下:
CMS的垃圾回收过程会产生很多的空间碎片,它提供了几个参数来处理这些空间碎片。
-XX:+UseCMS-CompactAtFullCollection ,(默认是开启的,此参数从 JDK9 开始废弃) 用于在 CMS 收集器进行 Full GC 时开启内存碎片的合并和整理。
-XX:CMSFullGCsBefore-Compaction, 用于规定多少次(根据此参数的值决定)之后再进行一次碎片整理。
知识拓展:
JAVA11中引入了很强悍的垃圾回收器ZGC 收集器,它支持 TB 级别的堆内存管理,而且 ZGC 收集器也非常高效,可以做到 10ms 以内完成垃圾收集。
在 ZGC 收集器中没有新生代和老生代的概念,它只有一代。ZGC 收集器采用的着色指针技术,利用指针中多余的信息位来实现着色标记,并且 ZGC 使用了读屏障来解决 GC 线程和应用线程可能存在的并发(修改对象状态的)问题,从而避免了Stop The World(全局停顿),因此使得 GC 的性能大幅提升。
ZGC 的执行流程和 CMS 比较相似,首先是进行 GC Roots 标记,然后再通过指针进行并发着色标记,之后便是对标记为死亡的对象进行回收(被标记为橘色的对象),最后是重定位,将 GC 之后存活的对象进行移动,以解决内存碎片的问题。