1.Introduction

之前已经介绍了,判定对象是否"死亡"的几种算法,如何收集"垃圾"对象的算法.现在我们就应该看看将其付诸实践的几款经典垃圾收集器了.

这里说明的是,垃圾收集器分为

  • Minor GC(针对新生代):Serial,ParNew,Parallel Scavenge
  • Major GC(针对老年代):CMS(Concurrent Mark Sweep) ,Serial Old,Parallel Old
  • Mixed GC(混合收集):G1(Garbage First)

而它们有自己的搭配组合,搭配组合图如下:

在这里插入图片描述

但其中的 Serial+CMS 和 ParNew+Serial Old的搭配已经在JDK9中废弃了.其中的CMS和G1是我们了解的重点.

后面我们还会听到"并发"和"并行"的收集器,这里的"并发"和"并行"在讨论垃圾收集器中的上下文语境中可以理解为:

  • 并行(Parallel):并行描述的是,多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时的用户线程是处于等待状态.这时JVM中只有垃圾收集器在工作,于是对于用户来说意味着"Stop The World".
  • 并发(Concurrent):这里的并发描述的是垃圾收集器和用户线程之间的关系.说明同一时间用户线程和收集器线程在同时工作.由于用户线程还在运行,所以程序仍能够响应服务请求.但是垃圾收集器线程占有了一定的系统资源,所以应用程序的吞吐量就会受到一定的影响.

衡量GC的指标

在衡量一款GC的好坏中有三款指标:内存占用(Footprint),吞吐量(Throughput)延迟(latency).这三者构成了一个不可能三角.三者的总体表现好随着技术的进步会越来越好.但是想要在这三个方面都达到近乎完美这是不可能的.一款优秀的GC通常也只能同时达到其中两项.

  • 内存占用:就是GC线程在运行的时候会占用的内存空间的大小
  • 吞吐量:处理器用于运行用户代码的时间所占处理器总消耗时间的比例.如果用公式来表达就是: 用户代码运行时间/(用户代码运行时间+运行垃圾收集时间)
  • 延迟:延迟则是因为在GC的过程一定会使用户线程暂停一段时间(Stop The World).而用户线程暂停的这段时间就是延迟.

但是延迟的重要性却是越来越得到大家的认可.因为随着计算机硬件的提升与进步,我们越来越能忍受GC多占用一点空间.也能够接受牺牲一定的效率(吞吐量)来换取更好的用户体验.说句很直接的话,内存占用和吞吐量是能够靠砸钱堆硬件来解决的.而且,互联网一定程度上是一个服务行业,要讨取用户的欢心.用户并不关心你这个服务器效率高不高,它只关心这个网站带给他的用户体验.而延迟是一个非常重要的用户体验.

2.Minor GC(新生代GC)

以下这几种Minor GC都是基于 标记-复制算法的.包括

  • Serial GC(最早也是最基础的 Minor GC)
  • ParNew GC(注重减少响应时间)
  • Parallel Scavenge(注重提高吞吐量)

1.Serial GC(Minor GC)

serial GC是最基础也是最悠久的GC ,在JDK1.3之前是虚拟机新生代GC的唯一选择.Serial是一个单线程工作的收集器.这里的单线程有两个含义:

  1. serial只会使用一个收集线程
  2. serial在进行垃圾收集的时候,其他的工作线程必须暂停所有手头上的工作,直到serial完成垃圾收集之后.也就是"Stop The World".

Serial/Serial Old GC运行示意图:

在这里插入图片描述

可以预想到,"Stop The World"对于用户来说是多么糟糕的体验.想象一下,在你观看一个视频的时候,突然视频播放器立马停止播放,你不能对计算机进行任何的操作,因为Serial在进行GC.真是因为给用户带来糟糕体验,从JDK1.3开始,到现在的JDK14,HotSpot虚拟机开发团队为了削减这种"Stop The World"做了很多的努力.

但是这并不意味着SerialGC就是一个最早出现,目前已毫无作用的GC了.事实上,它仍是HotSpot虚拟机在**客户端(但是Java主要领域是服务器端)**下的默认新生代GC.对于核心数较少计算机以及内存资源受限的环境下,由于没有线程交互的开销,专心做GC的Serial可以获得较高的效率.

2.ParNew GC(Minor GC)

ParNewGC实际上可以看成是SerialGC的多线程并行版本.除了同时使用多条线程来进行垃圾回收以外.ParNew和Serial没有太大的区别.虽然看起来它并没有什么亮点,但是在JDK7之前,它是很多服务器端的虚拟机新生代GC首选.但是滑稽的是,它成为很多人的选择不是因为自己,而是大佬(CMS)带飞.后面我们就会介绍CMS.

ParNewGC示意图:

在这里插入图片描述

在G1出来之前,CMS可以说是最适合服务器端的老年代GC了,而能和它搭配新生代GC只有Serial和ParNew. Parallel Scavenge因为某些原因无法和CMS搭配.但是之后随着G1的出现.渐渐地取代了CMS的搭配.

3.Parallel Scavenge(Minor GC)

Parallel Scavenge也是一款新生代GC,Parallel Scavenge和ParNew GC在很多特性从表面上看是相似的,但是最关键的一点是,它们关注的层面不一样,ParNew和CMS等收集器主要是关注于缩短停顿时间,而Parallel Scavenge以及和它搭配的老年代GC–Parallel Old GC关注是提高吞吐量…Parallel Scave GC的示意图会和后面的Parallel Old GC一起给出

3.Major GC(老年代GC)

以下的Major GC中除了 CMS运用的是 标记-清除算法以外都是基于 标记-复制 算法.

  • Parallel Old
  • Serial Old
  • CMS

1Parallel Old GC

Parallel Old GC是Parallel Scavenge的老年代版本,也是支持多线程并行收集.在注重吞吐量的场景,常常使用 Parallel Scavenge+ Parallel Old的组合来搭配使用.Parallel Old+ Parallel Scavenge 搭配运行示意图:

在这里插入图片描述

2.Serial Old

Serial Old 是Serial GC的老年代版本.它的作用主要在两个方面:

  • 在Parallel Old出现之前,作为 Parallel Scavenge的老年代搭配使用
  • 在CMS收集器出现 Concurrent Mode Failure的错误之后作为备选方案.

3.CMS(Concurrent Mark Sweep)重点!!!

CMS GC是一种以获得最短回收停顿时间为目标的收集器.而目前的互联网网页会非常关注网页的响应速度以提高用户的交互体验.所以CMS非常完美的符合这类应用的需求.所以很长一段的时间它都是服务器端的老年代GC首选(直到G1的出现).

而且从它的名字就可以看出它是并发(concurrent)而且是基于标记清除(mark sweep)算法的.下面就让我们详细的了解一下CMS吧.

3.1CMS的执行过程

在这里插入图片描述

  1. 初始标记(initial mark)
  2. 并发标记(concurrent mark)
  3. 重新标记(remark)
  4. 并发清除(concurrent sweep)

首先由示意图和名字可以看出 并发标记和并发清除阶段是不需要停止用户线程的.而在初始标记和重新标记的过程就要停止用户线程"Stop The World".

第一阶段(初始标记):初始阶段仅仅是标记一下GC Roots能直接关联的对象.所以这个阶段虽然会暂停用户线程,但是速度非常快.

第二阶段(并发标记):这个阶段是从GC Roots直接关联的对象开始遍历整个对象图的过程.这个阶段时间较长而且会占用一定的系统资源.但是用户线程还是会正常运行.

第三阶段(重新标记):由于并发标记是和用户线程并发执行的,所以这个过程会产生一些新的"垃圾"并且有些"垃圾"会不再是"垃圾".重新标记就是修正并发过程产生的变化.这个过程会比初始阶段稍长,但还是比较短.

第四阶段(并发清除):这个阶段就是将标记已经"死亡"的对象进行清除.由于标记清除算法不需要移动对象,所以这个过程是可以与用户线程并发执行的.

3.2 CMS的明显缺点

CMS是一款优秀的GC.它最主要的优点可以从它的名字上看出来.但是它还是有比较明显的缺点:

  1. CMS对处理器资源非常敏感,事实上,面向并发的程序都对处理器资源很敏感.在并发阶段,它虽然不会使用户线程暂停,却会因为占用了系统资源而使得用户线程执行缓慢,降低总吞吐量.但是说实话,在现代硬件水平突飞猛进的今天.不是那么重要
  2. CMS无法处理浮动垃圾(Floating Garbage),从而导致一次完全的"Stop The World"的Full GC.在并发清理阶段,用户线程还是在运行并且产生新的垃圾对象.而这个垃圾对象是无法在这一阶段清除的.这些垃圾对象就被称为浮动垃圾.这些浮动垃圾出现在并发清理阶段之后.只能等到下一次的GC再清除掉.
  3. CMS有产生"Concurrent Mode Failure"的风险.同样也是因为垃圾收集阶段用户线程还在同时运行,那么就需要预留空间给用户线程.但是如果是并行GC的话,在GC时只有GC在运行,所以就可以等到老年区几乎占满的时候再运行.但是并发的CMS就不可以.那么要预留多少空间给用户线程比较好呢?这是一个问题.要是预留的空间不够用户线程运行,就会出现"Concurrent Mode Failure".这时JVM就会不得不启动备用方案:冻结用户线程,并临时启用Serial Old来进行老年代的垃圾收集.这样就会造成停顿时间较长.
  4. 标记清除算法本身的缺点.这个我也在相应文章介绍过,那就是标记清除算法会造成大量的内存碎片,当内存碎片已经影响到了对象内存分配的时候,就会不得不提前触发一次Full GC.为了解决这个问题JVM可以设置两种参数. 第一种是:在不得不进行FullGC的时候,对内存碎片进行整理.第二种是:每执行若干次的GC后,在下一次GC之前会进行碎片整理

4.Mixed GC(混合GC)超级重点!!!

目前商用的Mixed GC只有G1(Garbage First)一种.而G1 GC是垃圾收集器发展历史上里程碑式的成果.G1开创了收集器面向局部收集的设计思路和基于Region的内存布局形式.

G1就是为了作为CMS和Parallel Old +Parallel Scavenge的替代者和继承者而产生的.官方给G1设定的目标就是**在延迟可控的情况下获得尽可能高的吞吐量.成为一款全功能收集器.**在JDK9以及更先进的版本中,G1成为了默认的GC.而CMS已经不被推荐使用.

既然G1是一款具有开创性的意义的GC,那么我们自然就要了解它开创性的地方,以及它和其他的经典GC有什么不同.

4.1 G1的开创性

1.基于Region的内存布局:大家都知道之前的GC中堆内存都是基于分代理论.实际上G1也是基于分代理论.但是它并不是将堆内存中的空间连续分配的.而是将堆内存分成很多的Region.每一个Region也许是老年代,也许是新生代中的Eden ,Survivor.而且如果一个对象的内存超过了Region的一半.那么这个对象就会被判定为大对象.会有一个专门的Humongous区来专门存放.如果一个超级大对象超过了整个Region,就会被存放在N个连续的Humongous区域中.Humongous通常会被当做老年代看待.

region内存分布与经典内存分布图对比如下:

在这里插入图片描述

在这里插入图片描述

2.可预测的停顿时间模型(面向局部收集)

与之前的GC要么是Minor GC 要么是Major GC不同**,G1不会对整个新生代或者老年代进行GC.而是以Region为单位进行GC**.G1对每个Region进行一个价值判断.价值的判断指数主要是两点:这次回收所需的时间和这次回收获得的空间块大小.并且在后台维护一个优先级列表,每次根据用户设定的允许的收集停顿时间优先处理回收价值最大的那些Region.这也是Garbage First名字的由来.这样面向局部的收集方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率.但是需要说明一点:用户会设置一个能允许的停顿时间,但是GC不一定一定在这个时间之内,但是会尽力做到.

4.2 G1的执行流程

  1. 初始标记(Initial Marking)
  2. 并发标记(Concurrent Marking)
  3. 最终标记(Final Marking)
  4. 筛选清除(Live Data Counting and Evacuation)

这其中的TAMS和SATB会在后面介绍.

初始标记阶段:这个阶段,GC仅仅标记GC Roots 能直接关联到的对象.并且修改TAMS(Top at Mark Start 后面会介绍).使得下一阶段并发标记的时候,能够在Region中正确的分配对象.这个阶段需要停顿用户线程"Stop The World".但是时间很短

并发标记阶段:这个阶段会从GC Roots对堆中的对象进行可达性分析,找出要回收的对象,耗时较长,但是可以并发执行.这和CMS很相似,但是会整理SATB(原始快照)记录下的并发时产生引用变动的对象

最终标记阶段:对用户线程进行一个短暂的暂停.用于处理在并发标记阶段结束后仍遗留下来的少量的SATB记录.

筛选清除阶段:负责更新Region的价值然后进行一个排序,根据用户所期望的停顿时间来制定时间计划.最后将回收的Region集中存活的对象复制到空的Region中,在清理掉整个旧Region中对象.这个阶段涉及到存活对象的移动,由多条线程并行执行.所以必须暂停用户线程.

可以看到,G1收集器除了并发标记阶段以外,其他阶段都需要暂停用户线程,这也是G1不仅仅追求响应时间来决定的.

从整体来看G1运用的是标记-整理算法,但是从局部来看(两个Region之间)运用的又是标记复制算法.这样就不会有内存碎片的产生.

5.并发的可达性分析的细节

大家可能发现发现了在CMS和G1收集器中标记阶段都是并发,这与之前的GC有所不同,那么它为什么能在标记阶段做到并发呢?这段时间内用户线程也在同时运行,期间可能会产生引用关系的改变.这可能产生两种问题:

  1. 原来应该消亡的对象被判定为存活,这就会产生浮动垃圾,但是并不是大事,可以接受.下次GC的时候再回收就好了
  2. 原来应该存活的对象被判定为消亡,这就是一个大问题,会导致程序的崩溃.我们一定要解决.

这就是一致性问题.下面我们就可以用三色标记来演示一下标记过程.三色标记按照"是否访问过"的条件将对象标记成三种颜色:

  1. 白色:表明对象尚未被GC访问过.在开始阶段所有对象都是白色的,如果在结束阶段还有对象是白色的,那么它就是不可达的.
  2. 灰色:表明该对象本身已经被访问过了,但是它对其他对象的引用还没有被完全扫描.
  3. 黑色:表明该对象已经被GC访问过了,并且它对其他对象的引用也被完全扫描了.

在扫描的过程通过三色标记演示的话,就相当于一个由黑色-灰色-白色的一个渐变过程.

在这里插入图片描述

研究人员发现当且仅当两个条件同时满足的时候就会产生"对象消失"问题.

  1. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用.
  2. 赋值器有插入了一条或多条从黑色对象到该白色对象的新引用.

所以我们要防止"对象消失"问题就只要破坏两个条件中的一个就可以了.由此产生了两种解决方案:

  1. 原始快照(Snapshot At The Beginning):G1运用了这种方案,原始快照破坏的是第一个条件:当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发标记结束之后,再将这些记录中引用关系的灰色对象为根,重新扫描一次.
  2. 增量更新(Incremental Update):CMS运用了这种方案,增量更新破坏的是第二个条件:当有黑色对象插入新的指向到白色对象的时候,就将这个记录下来,等到并发标记结束后,再将以记录中的黑色对象重新扫描一遍

6 G1和CMS的比较

作为服务端GC的热门选择,G1难免会和之前的CMS放在一起进行比较.有的人会觉得G1已经必要和CMS进行比较了,因为CMS已经被官方放弃了.但是事实上,CMS还没有被G1完全的取代.在某些特定的场景下G1的性能是比不上CMS的.他们之间的差异如下:

  1. G1在在尽量满足用户设定的响应时间的前提下,还能达到较高的吞吐量
  2. G1在执行过程中不会产生内存碎片,这其实也是因为CMS基于的是标记清除算法,而G1整体基于标记整理算法,局部基于标记复制算法.
  3. CMS在并发标记阶段为了解决一致性问题用的是增量更新算法(Incremental Update),而G1使用的是原始快照(Snapshot At The Beginning STAB).
  4. G1在执行过程的内存占用和执行负载都要比CMS要高.内存占用更高(大约多10%-20%)是因为G1的每一个Region的有一个Remembered Set(记忆集),执行负载更高是它们使用写屏障来维持Remembered Set,但是由于G1的RememberedSet更复杂,所以会产生更多的负载.
  5. G1使用的是最新的,开创性的算法和思路.所以还有很多的进步空间和红利.另一方面Oracle公司对于G1的支持也是不能忽视的.

总结来说:在内存较小,系统资源不是那么充足的环境之下,使用CMS效果可能会更好.而相反的情况下G1则是更好的选择.,通常来说这个临界点在(堆内存在6–8G之间).

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐