本文的重点是来详细介绍下上篇文章中提到的其中垃圾回收器

1. Serial GC为单核与串行而生

之前也说过,这是一款最古老的垃圾回收器,因为那时候都是单核的CPU,一个线程只能跑用户线程或者是垃圾回收线程,而不能同时进行,过程如下图所示。

JVM之垃圾回收器下篇_jvm

所以这款GC采用的是独占的方式,对于新生代统一称为SerialGC,使用的是复制算法,这样可以提高回收效率,老年代称为Serial Old GC,采用的是标记压缩算法,这是为了清除内存碎片考虑。

它的优点在于,适用于内存占用不大的应用,单核的服务器的唯一选择。

缺点也很明显,吞吐量不高,且在GC回收的时候会触发STW,造成程序卡顿。

使用:我们可以在JVM参数设置如下参数,使得新老年代都是用SerialGC。

-XX:+UseSerialGC
2. 并行的ParNew GC

他的底层几乎是与Serial GC是一样的,唯一的不同在于它可以并行执行垃圾回收,如下图所示。

JVM之垃圾回收器下篇_算法_02

这里说的并行,并不是说不会引发STW,从而造成用户线程卡顿了,这是说多个线程同时执行垃圾回收,使得效率提高了许多。

特点:它只能适用于新生代,对于老年代,需要配合其他GC一起使用,再拿组合图看一下吧。

JVM之垃圾回收器下篇_算法_03

可以看到,与Serial Old GC 组合可以在新生代提高效率,在老年代节省线程资源,只不过总觉得哪里怪怪的,所以在jdk9就去掉了他们的连接,而jdk14又去掉了与CMS的连接,所以是一个很尴尬的垃圾回收器。

使用:我们可以在JVM参数设置如下参数,使得新老年代都是用SerialGC。

-XX:+UseParNewGC
3. 吞吐量优先的并行Parallel GC

该回收器也分为两种,新生代使用的Parallel Scavenge GC,采用复制算法。老年代使用Parallel Old GC,采用标记压缩算法,两者都是并行的,运行过程如下图所示。

JVM之垃圾回收器下篇_算法_04

我认为前面的的ParNewGC是不完美的回收器,所以新老年代一个用并行,一个用串行,感觉就是在Serial上重复造轮子而开发的并行回收器,所以新一代的并行回收器底层和之前是不一样的,更加的优秀,JDK8也是采用这两个作为默认的回收器。

优点:对于暂停时间,吞吐量之类的控制更加灵活,可以用参数加以调节,还有自适应调节策略。

使用:我们可以在JVM参数设置如下参数,使得新老年代都是用ParallelGC,激活一个,另一也会被激活。

-XX:+UseParallelGC
-XX:+UseParallelOldGC

可设置年轻代垃圾回收线程的数量默认情况下当CPU数小于8,线程数与核数相等。

当CPU数大于8,根据如下公式选择 3+[(5*cpu_count)/8],如cpu数为12个,线程数为10个。

设置如下,其实默认即可。

-XX:ParallelGCThreads=xx

自适应调节策略,由于这个策略,导致我们实际查看Eden区与幸存区的比例从8:1:1变为了6:1:1,它的作用是用来调整内存比例以达到平衡点,一般默认即可。

-XX:+UseAdaptiveSizePolicy
4. 真正意义的并发CMS GC

CMS全称为Concurrent Mark Sweep,即并发标记清除,与之前不同,由于要清理内存碎片,所以前面都是采用标记压缩算法的,而该GC没有用到,而是用到了标记清除算法,因为它关注的是更短的stw,即程序停顿时间,所以整体采用的都是并发的方式,也就是用户线程与垃圾回收线程交替执行,执行过程如下。

JVM之垃圾回收器下篇_人工智能_05

工作流程

1.初始标记,从GCRoots标记可达对象,虽然有停顿,但是时间很短。

2.并发标记,与GC线程与用户线程交替执行,遍历标记内存中的所有可达对象,虽然耗时长,但是是没有停顿的。

3.重新标记,修正在并发标记阶段因引用改变的对象,即重新从GCRoots出发,标记关联对象,而失去引用的对象不会在这次被清除,这个停顿时间会比初始标记稍长一些。

4.并发清除,清除掉已经是垃圾的对象,释放内存空间。

注意点

1.因为垃圾回收线程几乎一直在回收,所以在内存空间不够前需要提前进行一次回收,即设置一个阈值,等满了再回收就来不及了,不仅会报concurrent mode failure的错误,还会启动后备方案,Serial来进行老年代的垃圾回收,这样停顿时间就长了。

2.因为采用的是标记清除算法,所以存在内存碎片的问题,需要维护一个空闲列表记录来合理选择内存分配。

3.不使用标记压缩算法是有原因的,因为在压缩的时候会改变对象的内存地址,而对象可能正在被使用,所以这是不得已的办法,当然,也可以设置在多少次后执行一次标记整理,减少碎片。

总结

优点:停顿时间短,有效的利用的线程资源。
缺点:
1.会产生内存碎片问题,可能造成大对象无法放入,而内存空间还充足,即隐式的内存泄漏。
2.降低了吞吐量,原先是所有线程同时跑用户程序的,现在需要拿出一条回收垃圾。
3.产生浮动垃圾,回看工作流程的第2条,在并发标记阶段如果某些对象失去了引用,将不会在这次GC中被回收,而要等到下次GC被清理。

5.未来的主流G1

先提出两个问题,抛砖引玉一下。

为什么要有G1?

我们知道,内存和CPU其实是在不断扩大的,这样一来我们就要尽量提高吞吐量,降低程序的停顿时间,所以G1的目标是在延迟可控的情况下尽可能的提高吞吐量的全功能的垃圾回收器,新老年代都由它一个来完成。

其实说白了,之前的GC回收器还是不够优秀,要拓展新思路。

为什么要叫G1?

它的特点其实就是采用新的思路,分区算法,将内存分为一个个的小块,优先收集垃圾最大的区域,所以叫Garbage First,它天生就是针对大容量的多核服务器而准备的,容量小反而体现不出它的优越性。

G1的优势

1.并发与并行兼具,在一定需要停顿的地方采用并行的方式,提高效率,多数情况采用并发的方式,使得程序不需要停顿。
2.仍然是个分代收集器,新老年代都是分为一个个的小块,活全都让它一个人干了。
3.采用复制算法将可用对象复制到其他区域,但由于分块的原因,不会产生内存碎片。
4.根据所设置的时间,每次只回收价值最大的一些区域,保证了它最大的回收效率。

region区域

如下图所示,这些一个个的小块,大小都是相同的,可能是1M,2M,当然还有个例外,就是大对象,只有大小超过1.5个region的空间后,才会被放入H区,大对象放在这里是有原因的,如果是短暂使用的大对象,直接放入老年代,其实相当于内存泄漏了,因为后面就不在使用了,白白浪费了空间。

在垃圾回收的时候,某些区域被回收了,会变成图中的空白区域,这些区域由空白列表记录,下次有对象来的就放入其中。

JVM之垃圾回收器下篇_算法_06

垃圾回收过程

回收的过程如下所示,这里简单讲讲。
1.年轻代GC:当Eden区满了,触发youngGC,开始并行式的独占式回收,同时对象在S区,O区移动。
2.年轻代+并发标记:当堆空间占用达默认阈值45%时,触发并发标记,标记那些仍需要使用的对象。
3.混合回收:因为是分区域的,被标记为垃圾的区域都会被回收,只不过一般老年代回收的较少。

JVM之垃圾回收器下篇_python_07

6.七种经典GC的总结

JVM之垃圾回收器下篇_jvm_08

其实都在图里面了,简单说几点。

单核,内存小,使用串行GC,如Serial系列。
多核,需要提高吞吐量,使用并行GC,使用Parallel系列。
多核,需要低延迟时间,使用并发GC,如CMS。
互联网,一般都采用G1。

7.未来展望:shenandoahGC与ZGC

在openJDK12中,出现了新一代的shenandoahGC,这个是当时是不被官方承认的,号称是停顿时间在10ms以内,但只不过是号称,但实验证明在停顿时间上比其他GC都要优秀,但是吞吐量会降低一些,也就是在缩短的停顿时间的基础上,垃圾回收运行的时间会长一些。

虽然jdk14也加入了shenandoahGC,但是相比跨时代的ZGC,任何垃圾回收器都要黯然失色,它才是真正停顿时间控制在10ms以内,且我看了下jdk17已经能控制在1ms以内的,吞吐量也与其他的GC相差无几,未来,必定是ZGC的天下。

最后说点题外话,之前看到一款AliGC,是阿里搞出来的,性能方面比G1略优秀一些,已经是不错了,但是被ZGC完爆,这也引起了我的反思,在国内,阿里的技术人员是顶尖的,但是放到国外呢?我们终究是在走别人的老路,AliGC就是改动了G1的底层,根据他们的场景进行了优化,但总是跟在别人屁股后面,所以我们的眼光要放的更远一些,不要在国内称雄,而要跟国外更优秀的人去比,这样才能够有更大的进步,更能够意识到自己的不足,想想马斯克吧,他的眼光早已不在这个世界了,而是宇宙啊!

ps:暂时JVM的知识告一段落了,之后再更新字节码相关。