深入探讨Java JVM垃圾收集器

  |   0 评论   |   0 浏览

Java GC收集器介绍

  目前在用的JDK版本基本是6、7和8,通过java -XX:+PrintCommandLineFlags -version命令,可以查看当前jvm默认的命令行,里面有默认的gc收集器,6、7、8默认的gc回收器都是
-XX:+UseParallelGC,这个参数代表是分代收集,young区是用的parallel scavenge,old区是用的serial old。注意:-XX:+UseParallelOldGC,才是开启parallel scavenge+parallel old的收集器组合。
  基于分代收集的原则,目前一共有7个可用的垃圾回收器,分别对应young区和old区,对应关系如下图所示:
1598261295789256704.png

  下面一一介绍这几种垃圾收集器。

Young区收集器

  Serial:最原始,最古老的收集器,单线程工作,有着“Stop The World”的“美誉”,也就是说这个垃圾收集器在进行垃圾回收操作的时候,会将所有用户线程全部暂停,这个就是最开始很多人不喜欢java的原因。不过这个垃圾回收器以及变成历史,几乎没有再用的地方(比较适合单核的服务器,现在去哪里找呢?)。其工作模式如下所示:
1598261400877543424.png
  ParNew:Serial收集器的多线程版本,工作原理跟Serial差不多,由单线程变成了多线程。如下图所示:
1598261485078196224.png

注意:目前只有Serial和ParNew两个新生代垃圾收集器能跟老年代垃圾收集器CMS组合使用,因此多线程版本的ParNew+CMS(+Serial Old)是一个常用的组合。

  Parallel Scavenge:多线程并行收集器,跟另一个多线程收集器ParNew的设计理念不用,Parallel Svavenge主要目的是控制JVM的吞吐量(吞吐量=用户代码执行时间/(用户代码执行时间+GC耗时)),可以通过配置-XX:MaxGCPauseMillis来控制最大的单次GC耗时,配置-XX:GCTimeRatio来直接设置吞吐量大小(GCTimeRatio默认值是99,即允许JVM运行时间的1%为垃圾回收的耗时,这个不是绝对,只是尽最大可能去保障这个值)。Parallel Scavenge有个JVM参数值得注意,-XX:+UseAdaptiveSizePolicy,可以让虚拟机参数**-Xmn新生代大小、EdenS0S1、晋升老年代年龄(-XX:PretenureSizeThreshold**)由系统自动根据运行情况来调节,也就是GC自适应的调节策略。Parallel Scavenge是JDK6、7、8默认的新生代垃圾收集器(JDK9默认G1)。

Old区收集器

  Serial Old:是新生代收集器Serial的老年代版本,工作原理跟Serial一样,目前是JDK6、7、8默认的老年代垃圾收集器。目前默认与新生代收集器Parallel Scavenge共同使用。因为Serial Old是采用标记-整理算法来回收内存,因此Serial Old还是CMS收集器的备用收集器。
  Parallel Old:Parallel Scavenge的老年代版本,基于多线程和标记-整理算法实现,原理跟Parallel Scavenge一样,也是以JVM吞吐量控制为主的垃圾收集器,可以结合Parallel Scavenge共同使用。Parallel Scavenge+Parallel Old组合适合注重JVM吞吐量、以及CPU资源敏感的场景。
  CMS:英文全名叫Concurrent Mark Sweep,中文翻译叫并发标记清除,设计这款垃圾回收器的最主要目的是让垃圾回收的暂停时间最短,比较适合对响应速度有要求的场景,所以这个是牺牲吞吐量,换取最小暂停时间的垃圾收集器。这款收集器有个弊端,就是基于标记-清除为垃圾收集算法来实现的,在执行多次GC过后,会产生大量不连续的内存碎片,有可能导致对象无法分配,这时候就需要基于标记-整理算法实现的Serial Old垃圾收集器再执行一次Full GC,将内存碎片整理一下。可以通过-XX:+UseCMSCompactAtFullCollection开关参数,让CMS在执行完Full GC后进行内存整理,-XX:CMSFullGCsBeforeCompaction参数来控制执行多少次full gc后,进行一次带内存压缩的gc。
  CMS工作过程目前一共分为四个阶段:
  初始标记:CMS initial mark,仅仅标识一下GC root能关联到的对象,速度非常快。
  并发标记:CMS concurrent mark,GC root tracing的一个过程,标记出哪些对象root不可达,即为需要回收的对象,非常耗时。
  重新标记:CMS remark,由于并发标记是跟用户线程同时执行,重新标记是为了确保用户线程执行的时候没有改变并发标记的那些对象的状态,这里只需要判断用户线程当时改动过的小部分对象即可,速度很快。
  并发清除:CMS concurrent sweep,清除标记的垃圾对象,过程非常耗时。
  重置状态:CMS concurrent reset,并发重置状态等待下次CMS的触发。
  由于非常耗时的两个阶段,并发标记和并发清除,都是跟用户线程同时执行的,因此整个gc过程中,暂停时间是非常短的,但是,这里牺牲了JVM的吞吐量换来的,对于CPU敏感的系统来说,CMS可能并不合适。CMS的工作原理图如下所示:
1598261572500074496.png

G1垃圾收集器

  G1垃圾收集器是JDK7的时候正式发布的,基于标记-整理算法实现的垃圾收集器,能够很精确的控制垃圾回收的暂停时间。这个垃圾收集器是分块收集,跟之前的分代收集有所不同,没有老年代、新生代之分,G1把堆内存分成N个大小固定的块,每次根据允许的垃圾回收时间,优先回收垃圾最多的块。JDK9以后,G1成为默认的垃圾收集器。
  之前做过一个POC,分析了一下GC相关的参数和性能,压测的数据可以跟大家分享一下。两次压测都是在JDK1.8 64bit的环境下压测的,堆内存都是为80G。

JVM默认垃圾收集器 parallel scavenge+serial old

1706494675221155840.png

1706494840040525824.png

1706495061982121984.png

G1垃圾收集器

1706495191107964928.png

1706495248213413888.png

1706495292442349568.png

  从上面的图中可以看到,分代收集和分块收集的不同之处。分代收集里面,old区的内存比较大,full gc耗时比较长,其中max full gc time达到了280+ms,可以想象一下一个高并发实时应用,突然暂停280ms是啥影响,但是分代收集的young gc(mirror gc,新生代gc)耗时很短,而且分代收集主要的gc还是消耗在young gc上面。G1是属于分块收集,可以看到gc分析结果中,并没有young gc和full gc之分,分块收集并不像分代收集划分得那么明确,每个块都差不多,不会有太大的波动(gc时间的方差比分代收集小多了),没有分代收集的full gc那么大的峰值,但是也没有young gc那么短的暂停,但是从数据中可以看到,两者的JVM吞吐量是相当的,G1并没有增加cpu对业务线程的执行压力。

JVM启动参数之垃圾回收相关参数

参数描述
-XX:+UseSerialGCJVM在client模式下的默认值,开启此开关后,JVM将使用Serial+Serial Old的组合进行垃圾回收
-XX:+UseParNewGC开启此开关后,采用ParNew+Serial Old的组合进行垃圾回收
-XX:SurvivorRatio分代收集中,新生代的Eden与survivor区域的比例,默认为8,即8:1:1
-XX:PretenureSizeThreshold直接晋升到老年代的对象大小,超过此大小的对象,直接分配到老年代,默认0,即不管多大都先分配在eden
-XX:MaxTenureThreshold晋升到老年代的对象年龄,如果一个对象在新生代,一次回收没有收掉,那么年龄+1,默认15(最大15),年龄超过这个数,则对象进入老年代

  Parallel GC相关参数

参数描述
-XX:+UseParallelGCJVM在Server模式下的默认值,将采用Parallel Scavenge+Serial Old组合进行垃圾回收
-XX:+UseParallelOldGC使用Parallel Scavenge+Parallel Old组合进行垃圾回收,适合对JVM吞吐量有要求,cpu敏感的场景
-XX:+UseAdaptiveSizePolicy动态调整各个JVM各个区域的大小,以及对象进入老年代的年龄
-XX:ParallelGCThreads并行GC时候的线程数
-XX:GCTimeRatioJVM吞吐量设置,默认99
-XX:MaxGCPauseMillisGC的最大暂停时间

  CMS相关参数

参数描述
-XX:+UseConcMarkSweepGC采用ParNew+CMS+Serial Old的组合进行垃圾回收,Serial Old作为CMS备用,在发生Concurrent Mode Failure失败后使用
-XX:CMSInitiatingOccupancyFraction设置CMS在老年代多少空间被使用后出发垃圾回收,默认是68%
-XX:+UseCMSCompactAtFullCollection在CMS垃圾回收后,是否进行一次碎片整理
-XX:CMSFullGCsBeforeCompaction在CMS垃圾回收若干次后,再进行一次碎片整理

这里有一片关于JVM GC的文章,写得非常好,还有一些实战经验,值得一看https://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html


标题:深入探讨Java JVM垃圾收集器
作者:michael
地址:https://blog.junxworks.cn/articles/2018/09/17/1537152072254.html