上篇文章我们介绍了如何判断对象是否为垃圾,有垃圾就要清理,清理就需要 垃圾收集器。
垃圾收集器(Garbage Collector)是垃圾收集GC的实现,根据是否分代收集可将垃圾收集器分为两种类型:分代收集和不分代收集。
分代收集
Serial垃圾收集器
Serial垃圾收集器是最简单的垃圾回收器的实现,同时它也是最古老的垃圾回收器,在jdk 1.3之前是HotSpot虚拟机新生代收集器的唯一选择。
Serial翻译成中文的意思是“串行的”,顾名思义Serial垃圾收集器就是一个单线程的垃圾回收器,它的这个串行指的是当它要进行垃圾回收时,其他所有的工作线程必须都要暂停,直到它回收结束,也就是我们常说的STW(Stop The World)。这很影响用户体验,如果每运行1个小时,暂停5分钟,用户就要等5分钟,别说5分钟了,哪怕是半分钟不响应,也会让人觉得这个网站真的很垃圾。在多线程应用程序中它不是一个好的选择,比如在服务器端使用,但是对于运行在客户端模式下的虚拟机来说是一个很好的选择,它也是HotSpot虚拟机运行在客户端模式下的默认新生代收集器。Serial垃圾收集器只适合几十兆到一两百兆的堆空间进行垃圾回收,在这种小内存中,它可以控制停顿时间在100ms以内,是可以被用户接受的。
Serial Old收集器用于老年代的回收,一般搭配着Serial收集器使用。Serial Old收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案。它和Serial收集器设计思想是一样的,只是回收的区域不同而已。
特点:
- 古老
- 简单高效(和其他单线程垃圾回收器比较)
- 单线程串行
- 适合在客户端模式下和单CPU环境中使用
ParNew垃圾收集器
ParNew是Parallel New的缩写,从名字可以看出,它是并行并且是收集新生代的垃圾回收器。ParNew垃圾收集器是Serial垃圾收集器的多线程并行版本,除了并行收集以外,其他的实现基本和Serial垃圾收集器无异。
在JDK 7以前ParNew垃圾收集器是新生代垃圾回收的首选,它主要是配合CMS垃圾收集器使用。不过JDK 9开始将ParNew垃圾收集器合并到了CMS垃圾收集器中。
Parallel 垃圾收集器
Parallel垃圾收集器是JDK 8默认的垃圾收集器,它也是一个并行多线程的垃圾收集器,跟CMS垃圾收集器类似,只不过CMS垃圾收集器侧重点是减少垃圾收集时用户线程停顿的时间,而Parallel垃圾收集器的侧重点是控制吞吐量以达到高效率地利用 CPU时间,尽快完成应用程序的运算任务。所以Parallel垃圾收集器有时也称为吞吐量优先的收集器(Throughput Collector)。吞吐量是根据进行垃圾收集所花费的时间与在垃圾收集之外所花费的时间(称为应用程序时间)进行计算的。
吞吐量=应用程序时间/(应用程序时间+垃圾收集时间)
Parallel垃圾收集器分为Parallel Scavenge和Parallel Old垃圾收集器。Parallel Scavenge垃圾收集器负责新生代的垃圾收集,Parallel Old垃圾收集器负责老年代的垃圾收集。
可以通过以下参数控制Parallel 垃圾收集器的行为:
-
-XX:MaxGCPauseMillis=<t>
:控制最大GC停顿时间,单位毫秒,默认情况下没有值。当设置停顿时间时,JVM会调整堆大小和其他跟垃圾回收相关的参数尽可能的小于设置的这个停顿时间。不过不要想着把MaxGCPauseMillis设置的越小越好,MaxGCPauseMillis设置小了,频率就变快了。比如,堆内存是500M,垃圾收集时间是10s,停顿时间是100ms,如果把MaxGCPauseMillis设置为70ms,为了达到70ms,堆内存就减小变成了300M,垃圾收集时间变成了5s,单次的停顿时间是变短了,整体的效率变低了,之前每隔10s停顿100ms,现在每隔10s停顿140ms,牺牲了吞吐量。而且MaxGCPauseMillis设置了之后不一定就能达到预期,并且如果对垃圾收集器运作原理不了解的话,设不准很容易出现问题。 -
-XX:GCTimeRatio=<N>
:设置吞吐量大小。GCTimeRati就是设置垃圾收集时间与应用程序时间之比的,公式为1 / (1 + <N>)
。比如:-XX:GCTimeRatio=19
,这意味着垃圾回收时间占总时间的1 / (1 + 19)= 1/20
,也就是5%
,GCTimeRatio默认值为99,即允许最大的垃圾回收时间为1%。 -
-Xmx<N>
:设置堆内存大小。收集器的隐式目标是在满足其他参数的情况下最小化堆的大小。
垃圾收集器会先满足参数-XX:MaxGCPauseMillis
,然后再满足参数-XX:GCTimeRatio
,在这两个参数都没满足的情况下参数才会被考虑-Xmx<N>
。
-XX:GCTimeRatio=<N>
和-XX:GCTimeRatio=<N>
参数尤其不好控制,所以在Parallel 垃圾收集器中提供了另一个参数-XX:+UseAdaptiveSizePolicy
,它是默认开启的,开启之后虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
CMS垃圾收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它负责老年代的垃圾回收,新生代只能选择 ParNew或者Seria收集器中的一个。CMS收集器在JDK 5发布,在JDk 9中被标记过时,在JDK 14中已经被删除,不过这并不影响它是一款具有划时代意义的垃圾收集器,它是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次实现了让垃圾收集线程与用户线程(基本上)同时工作。CMS 收集器也被称为低延迟垃圾收集器。
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,也是唯一一款基于标记-清除算法实现的老年代收集器,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:
- 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。
- 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记(CMS remark):为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
- 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
CMS垃圾收集器有以下缺陷:
- CPU 敏感: CMS对处理器资源敏感,毕竟采用了并发的收集、当处理核心数不足4个时,CMS对用户的影响较大。
- 浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾” 。
由于浮动垃圾的存在,因此需要预留出一部分内存,意味着CMS收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现Concurrent Mode Failure,这时虚拟机将临时启用Serial Old收集器来替代CMS收集器,这样停顿的时间就更长了。 - 会产生空间碎片:由于CMS是一款基于“标记-清除”算法实现的收集器,所以肯定会产生不连续的空间碎片。
不分代收集
Garbage First收集器
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
G1也遵循分代收集理论,但是它不像上面分代收集垃圾收集器那样物理分代,而是采用Region的概念逻辑分代,物理分区:每个Region可以扮演新生代的Eden空间、Survivor空间,或者老年代空间收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、 熬过多次收集的旧对象都能获取很好的收集效果。
G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值, 然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis
指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。
G1运行过程包括4个步骤:初始标记、并发标记、最终标记和筛选回收,除了并发标记外,其余阶段也是要完全暂停用户线程的。
G1垃圾收集器是JDK 9开始默认的垃圾收集器,它有如下特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短STW的时间。
- 不会产生内存碎片:G1是基于“标记—整理”算法实现的收集器
- 可以指定停顿时间:
-XX:MaxGCPauseMillis
参数指定目标的最大停顿时间,G1尝试调整新生代和老年代的比例,堆大小,晋升策略来达到这个目标时间。 - 局部收集:使用Region划分内存空间,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
ZGC
ZGC(Z Garbage Collector)是一款在JDK 11中新加入的具有实验性质的可扩展的低延迟垃圾收集器 ,在JDK 15中已经可以正式在生产中使用了。ZGC的目标是在任意堆内存大小下都可以把垃圾收集的停顿时间限制在10毫秒以内的低延迟。
ZGC也采用基于Region的堆内存布局,ZGC的Region具有动态性——动态创建和销毁,以及动态的区域容量大小。在x64硬件平台下,ZGC的Region可以具有大、中、小三类容量:
- 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
- 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对象。
- 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。
ZGC有如下特点:
- 低延迟、停顿时间短(10ms以内)
- 支持8M~16T内存的GC
- 多线程并发GC
- 采用染色指针技术实现对象引用
- 不再使用分代收集
- 支持不同容量的Region分区
垃圾收集器搭配及启动
新生代 | 老年代 | JVM参数 |
---|---|---|
Serial | SerialOld | -XX:+UseSerialGC |
Parallel Scavenge | SerialOld | -XX:+UseParallelGC |
Parallel Scavenge | ParallelOld | -XX:+UseParallelOldGC |
ParNew | SerialOld | -XX:+UseParNewGC |
ParNew | CMS + SerailOld | -XX:+UseConcMarkSweepGC |
G1 | G1 | -XX:+UseG1GC |
ZGC | ZGC | -XX:+UseZGC |
总结
本文主要简单介绍了常见的垃圾收集器。其中ZGC了解学习一下就可以了,ZGC虽然在JDK 15中可以使用,但是具体表现如何不好说,也没有被官方指定为默认垃圾收集器,对比之前的垃圾收集器可以发现,能留下来的都是曾经被官方指定为默认垃圾收集器,不被指定的在后续版本中就被移除了,比如CMS垃圾收集器。