一、JVM堆内存结构

jvm01
堆内存主要分为两个区域,分别是新生代和老年代,其中新生代又划分了Eden区和Survivor区,用复制算法对该区域进行垃圾回收;老年代由于存活对象占比大,不适合采用复制算法,采用的是标记整理算法进行垃圾回收。

二、垃圾回收器概览

jvm02
对于新生代和老年代有不同的垃圾收集器,其中新生代的垃圾收集器有:Serial、ParNew、Parallel Scavenge、G1;老年代的垃圾收集器有:Serial Old、CMS、Parallel Old、G1.

收集器之间的连线代表可以配合使用,比如比较常用的ParNew+CMS+Serial Old.

三、Serial

jvm03
Serial收集器是最古老的垃圾收集器,适用于新生代,采用复制算法,用单线程的方式进行垃圾回收,GC时需要STW(Stop The World),是client模式默认的垃圾收集器,但是由于Java通常用于服务端,因此Serial收集器实际使用比较少。

使用-XX:+UseSerialGC参数设置新生代使用Serial收集器。

四、ParNew

jvm04
ParNew收集器是Serial的多线程版本,由于现在绝大部分服务器都是多CPU的,Serial收集器仅用一个线程执行垃圾回收,因此效率比较低,ParNew在Serial的基础上进行优化,采用多线程的方式执行垃圾回收。

使用-XX:+UseParNewGC参数设置新生代使用ParNew收集器。
使用-XX:ParallelGCThreads参数设置GC线程数,默认情况下与CPU核数相同。

五、Parallel Scavenge

jvm05
Parallel Scavenge收集器跟ParNew收集器非常类似,也是采用复制算法用于新生代垃圾回收,只是两者的关注点不同,ParNew收集器是通过多线程的方式缩短GC时间,而Parallel Scavenge关注的是吞吐量,吞吐量=用户代码运行时间 / (用户代码运行时间 + 垃圾收集时间),比如用户代码运行时间为100分钟,垃圾收集时间为1分钟,则吞吐量=99 / (99 + 1) * 100% = 99%.

Parallel Scavenge提供了两个参数来控制吞吐量,通过-XX:MaxGCPauseMillis来控制最大GC停顿时间,通过-XX:GCTimeRatio来控制吞吐量大小,范围在0-100,默认为99。

自适应调节策略参数-XX:+UseAdaptiveSizePolicy:打开之后,就不需要设置新生代大小(-Xmn),Eden,survivor比例及(-XX:SurvivorRatio)晋升老年代年龄(-XX:PretenureSizeThreshold),虚拟机根据系统运行状况,调整停顿时间,吞吐量, GC自适应调节策略。

使用 -XX:+UseParallelGC参数设置新生代使用Parallel Scavenge收集器。

六、Serial Old

jvm06
Serial Old是Serial老年代版本,适用于老年代,采用标记整理算法,通常于CMS垃圾收集器搭配使用,当CMS执行垃圾回收,出现“Concurrent Mode Failure”时,切换到Serial Old,重新执行Full GC。

使用-XX:+UseSerialGC参数设置老年代使用Serial Old收集器。

七、Parallel Old

jvm07
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用标记整理算法,用于老年代的垃圾回收,同样关注吞吐量。

使用 -XX:+UseParallelOldGC参数设置老年代使用Parallel Old收集器。

八、CMS

jvm08
无论是Serial Old还是Parallel Old,执行垃圾回收时,都需要Stop The World,而且GC线程和用户线程不能并发运行。在老年代,存活对象占比要比新生代大得多,所以老年代在标记和清理的时间都比较耗时,CMS针对于标记和清理做了优化,其中标记阶段主要分为初始标记、并发标记和重新标记,那么CMS的主要设计思想是什么?

我们先看下调用量:GC Roots -> A -> B -> C

如果是Serial Old和Parallel Old,都是一口气把整条调用量都标记出来,而CMS分阶段标记,在初始阶段(初始标记),则只是标记GC Roots可直达的对象,例如调用链GC Roots -> A -> B -> C的 A,这个阶段需要STW,但由于只是标记GC Roots可直达的对象,速度比较快。

在标记的第二阶段(并发标记)则不需要从GC Roots出发了,可以从A出发来追踪标记整个调用链条,由于这个时候A是确定的,因此可以与用户线程并发执行。

在标记的第三阶段(重新标记)由于在并发标记阶段与用户线程并发运行,因此标记的对象可能会有变化,并且产生新的垃圾,因此这个阶段需要STW,对原来的标记进行修正,这个过程耗时较小。

标记完成之后,就确定了垃圾对象,因此在清理阶段,可以与用户线程同时进行。正是由于这个阶段与用户线程同时进行,如果这个时间内产生垃圾比较多,可能会触及“Concurrent Mode Failure”失败,此时将切换到Serial Old收集器,重新执行垃圾回收,将导致整个过程非常耗时。

使用 -XX:+UseConcMarkSweepGC参数设置老年代使用CMS收集器。

九、G1

jvm09
jvm10
在G1算法中,采用了另外一种完全不同的方式组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存。

-XX:G1HeapRegionSize参数指定Region大小,区间只能是1M、2M、4M、8M、16M和32M。
默认把堆内存按照2048份均分,最后得到一个合理的大小。

G1收集器应用于多CPU以及大内存的环境下,极大减少垃圾收集的停顿时间。
jvm11

初始标记:标记一下GC Roots能直接关联到的对象,需要停顿线程,但耗时很短。
并发标记:是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但 可与用户程序并发执行。
最终标记:修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

使用 -XX:+UseG1GC参数设置新生代和老年代使用G1收集器。

十、最佳实践

在G1收集器发布之前,用的比较多的垃圾收集器为:ParNew + CMS + Serial Old,也就是JVM参数设置+XX:UseConcMarkSweepGC。
在G1收集器发布之后,用的标记多的垃圾收集器为:G1,也就是JVM参数设置+XX:UseG1GC

jdk 默认垃圾收集器如下:
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1

可以通过java -XX:+PrintCommandLineFlags -version 查看默认垃圾收集器。
jvm12

打赏
支付宝 微信
上一篇 下一篇