之前已经介绍了,判定对象是否"死亡"的几种算法,如何收集"垃圾"对象的算法.现在我们就应该看看将其付诸实践的几款经典垃圾收集器了.
这里说明的是,垃圾收集器分为
而它们有自己的搭配组合,搭配组合图如下:
但其中的 Serial+CMS 和 ParNew+Serial Old的搭配已经在JDK9中废弃了.其中的CMS和G1是我们了解的重点.
后面我们还会听到"并发"和"并行"的收集器,这里的"并发"和"并行"在讨论垃圾收集器中的上下文语境中可以理解为:
在衡量一款GC的好坏中有三款指标:内存占用(Footprint),吞吐量(Throughput)和延迟(latency).这三者构成了一个不可能三角.三者的总体表现好随着技术的进步会越来越好.但是想要在这三个方面都达到近乎完美这是不可能的.一款优秀的GC通常也只能同时达到其中两项.
但是延迟的重要性却是越来越得到大家的认可.因为随着计算机硬件的提升与进步,我们越来越能忍受GC多占用一点空间.也能够接受牺牲一定的效率(吞吐量)来换取更好的用户体验.说句很直接的话,内存占用和吞吐量是能够靠砸钱堆硬件来解决的.而且,互联网一定程度上是一个服务行业,要讨取用户的欢心.用户并不关心你这个服务器效率高不高,它只关心这个网站带给他的用户体验.而延迟是一个非常重要的用户体验.
以下这几种Minor GC都是基于 标记-复制算法的.包括
serial GC是最基础也是最悠久的GC ,在JDK1.3之前是虚拟机新生代GC的唯一选择.Serial是一个单线程工作的收集器.这里的单线程有两个含义:
Serial/Serial Old GC运行示意图:
可以预想到,"Stop The World"对于用户来说是多么糟糕的体验.想象一下,在你观看一个视频的时候,突然视频播放器立马停止播放,你不能对计算机进行任何的操作,因为Serial在进行GC.真是因为给用户带来糟糕体验,从JDK1.3开始,到现在的JDK14,HotSpot虚拟机开发团队为了削减这种"Stop The World"做了很多的努力.
但是这并不意味着SerialGC就是一个最早出现,目前已毫无作用的GC了.事实上,它仍是HotSpot虚拟机在**客户端(但是Java主要领域是服务器端)**下的默认新生代GC.对于核心数较少计算机以及内存资源受限的环境下,由于没有线程交互的开销,专心做GC的Serial可以获得较高的效率.
ParNewGC实际上可以看成是SerialGC的多线程并行版本.除了同时使用多条线程来进行垃圾回收以外.ParNew和Serial没有太大的区别.虽然看起来它并没有什么亮点,但是在JDK7之前,它是很多服务器端的虚拟机新生代GC首选.但是滑稽的是,它成为很多人的选择不是因为自己,而是大佬(CMS)带飞.后面我们就会介绍CMS.
ParNewGC示意图:
在G1出来之前,CMS可以说是最适合服务器端的老年代GC了,而能和它搭配新生代GC只有Serial和ParNew. Parallel Scavenge因为某些原因无法和CMS搭配.但是之后随着G1的出现.渐渐地取代了CMS的搭配.
Parallel Scavenge也是一款新生代GC,Parallel Scavenge和ParNew GC在很多特性从表面上看是相似的,但是最关键的一点是,它们关注的层面不一样,ParNew和CMS等收集器主要是关注于缩短停顿时间,而Parallel Scavenge以及和它搭配的老年代GC–Parallel Old GC关注是提高吞吐量…Parallel Scave GC的示意图会和后面的Parallel Old GC一起给出
以下的Major GC中除了 CMS运用的是 标记-清除算法以外都是基于 标记-复制 算法.
Parallel Old GC是Parallel Scavenge的老年代版本,也是支持多线程并行收集.在注重吞吐量的场景,常常使用 Parallel Scavenge+ Parallel Old的组合来搭配使用.Parallel Old+ Parallel Scavenge 搭配运行示意图:
Serial Old 是Serial GC的老年代版本.它的作用主要在两个方面:
CMS GC是一种以获得最短回收停顿时间为目标的收集器.而目前的互联网网页会非常关注网页的响应速度以提高用户的交互体验.所以CMS非常完美的符合这类应用的需求.所以很长一段的时间它都是服务器端的老年代GC首选(直到G1的出现).
而且从它的名字就可以看出它是并发(concurrent)而且是基于标记清除(mark sweep)算法的.下面就让我们详细的了解一下CMS吧.
首先由示意图和名字可以看出 并发标记和并发清除阶段是不需要停止用户线程的.而在初始标记和重新标记的过程就要停止用户线程"Stop The World".
第一阶段(初始标记):初始阶段仅仅是标记一下GC Roots能直接关联的对象.所以这个阶段虽然会暂停用户线程,但是速度非常快.
第二阶段(并发标记):这个阶段是从GC Roots直接关联的对象开始遍历整个对象图的过程.这个阶段时间较长而且会占用一定的系统资源.但是用户线程还是会正常运行.
第三阶段(重新标记):由于并发标记是和用户线程并发执行的,所以这个过程会产生一些新的"垃圾"并且有些"垃圾"会不再是"垃圾".重新标记就是修正并发过程产生的变化.这个过程会比初始阶段稍长,但还是比较短.
第四阶段(并发清除):这个阶段就是将标记已经"死亡"的对象进行清除.由于标记清除算法不需要移动对象,所以这个过程是可以与用户线程并发执行的.
CMS是一款优秀的GC.它最主要的优点可以从它的名字上看出来.但是它还是有比较明显的缺点:
目前商用的Mixed GC只有G1(Garbage First)一种.而G1 GC是垃圾收集器发展历史上里程碑式的成果.G1开创了收集器面向局部收集的设计思路和基于Region的内存布局形式.
G1就是为了作为CMS和Parallel Old +Parallel Scavenge的替代者和继承者而产生的.官方给G1设定的目标就是**在延迟可控的情况下获得尽可能高的吞吐量.成为一款全功能收集器.**在JDK9以及更先进的版本中,G1成为了默认的GC.而CMS已经不被推荐使用.
既然G1是一款具有开创性的意义的GC,那么我们自然就要了解它开创性的地方,以及它和其他的经典GC有什么不同.
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不一定一定在这个时间之内,但是会尽力做到.
这其中的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之间)运用的又是标记复制算法.这样就不会有内存碎片的产生.
大家可能发现发现了在CMS和G1收集器中标记阶段都是并发,这与之前的GC有所不同,那么它为什么能在标记阶段做到并发呢?这段时间内用户线程也在同时运行,期间可能会产生引用关系的改变.这可能产生两种问题:
这就是一致性问题.下面我们就可以用三色标记来演示一下标记过程.三色标记按照"是否访问过"的条件将对象标记成三种颜色:
在扫描的过程通过三色标记演示的话,就相当于一个由黑色-灰色-白色的一个渐变过程.
研究人员发现当且仅当两个条件同时满足的时候就会产生"对象消失"问题.
所以我们要防止"对象消失"问题就只要破坏两个条件中的一个就可以了.由此产生了两种解决方案:
作为服务端GC的热门选择,G1难免会和之前的CMS放在一起进行比较.有的人会觉得G1已经必要和CMS进行比较了,因为CMS已经被官方放弃了.但是事实上,CMS还没有被G1完全的取代.在某些特定的场景下G1的性能是比不上CMS的.他们之间的差异如下:
总结来说:在内存较小,系统资源不是那么充足的环境之下,使用CMS效果可能会更好.而相反的情况下G1则是更好的选择.,通常来说这个临界点在(堆内存在6–8G之间).