1. JVM ParNew + CMS垃圾回收器


1.1. ParNew

1.1.1. ParNew垃圾回收器简介

ParNew是用在新生代的垃圾回收器。即使有G1回收器,还有很多线上系统在用的ParNew回收器。

该回收器使用的是复制算法,采用多线程来进行回收,提高了回收效率。Serial垃圾回收器回收算法和ParNew一样,但是是单线程的。

jvm_gc_parnew_01

1.1.2. 如何设置指定ParNew垃圾回收器

对于设置JVM参数,在Eclipse/IntelliJ IDEA中可以设置Debug JVM Arguments,使用“java -jar”命令启动时直接在后面跟上JVM参数即可。

部署到Tomcat时,可以在Tomcat的catalina.sh中设置Tomcat的JVM参数,使用Spring Boot也可以在启动时指定JVM参数。

启动时指定使用ParNew垃圾回收器参数,使用-XX:+UseParNewGC,加入这个选项,JVM启动之后对新生代进行垃圾回收时会使用ParNew垃圾回收器

1.1.3. ParNew垃圾回收器线程数量

部署系统的服务器都是多核CPU的,为了在垃圾回收的时候充分利用多核CPU的资源,指定使用ParNew垃圾回收器之后,会默认设置垃圾回收线程的数量,跟CPU的核数是一样的。

这个参数一般不用手动去调节,跟CPU核数一样的线程数量,是可以充分进行并行处理的。如果一定要自己调节ParNew的垃圾回收线程数量,使用-XX:ParallelGCThreads参数即可,通过该参数来设置线程的数量。建议不要随意动这个参数。


1.2. CMS

1.2.1. CMS 垃圾回收的基本原理

老年代的垃圾回收器一般是CMS,采用的是标记清理算法,就是先用标记方法去标记出垃圾对象,然后把这些垃圾对象清理掉。

标记-清理算法,先通过追踪GC Roots的方法,看对象是否被引用,如果有引用就是存活对象,否则就是垃圾对象。先将垃圾对象都标记出来,然后一次性把垃圾对象都回收掉。这种方法最大的问题是会造成很多内存碎片

如果 CMS垃圾回收 时,一直Stop the World,会导致系统长时间不能使用,无法响应请求,体验会很差。所以CMS垃圾回收器尽量同时执行垃圾回收线程和系统工作线程,来进行垃圾回收,提升效率

1.2.2. CMS 垃圾回收的各个阶段介绍

为了提高使用体验,CMS 垃圾回收分为 4 个阶段,部分阶段可以不用Stop the World,尽量减少系统暂停时间。详细介绍如下。

初始标记

CMS进行垃圾回收时,第一阶段,会先执行初始标记阶段,这个阶段会让系统的工作线程全部停止,进入Stop the World状态。

jvm_gc_cms_01

虽然要Stop the World暂停一切工作线程,但影响不大,因为速度很快,仅仅标记GC Roots直接引用的对象即可。(方法的局部变量和类的静态变量 是GC Roots。但是 类的实例变量不是GC Roots。)

并发标记

第二阶段,并发标记阶段,会让系统可以创建各种新对象,也可能部分存活对象失去引用,变成垃圾对象。

jvm_gc_cms_02

就是对老年代所有对象进行GC Roots追踪,是最耗时的。该阶段需要追踪所有对象是否从根源上被GC Roots引用(查看引用来源时候还有效),但这个最耗时的阶段是跟系统程序并发运行的,所以这个阶段不会对系统运行造成性能体验上的影响。

重新标记

第三个阶段,重新标记阶段,在第二阶段中会创建新的对象及老对象可能会变成垃圾对象,在该阶段会再次进入Stop the World状态,对这部分对象进行重新标记。

jvm_gc_cms_03

重新标记阶段,速度会很快,第二阶段中变动过的对象会比较少,所以运行速度很快。

并发清理

第四阶段,并发清理阶段,该阶段就是让系统程序随意运行,然后同时清理掉标记为垃圾的对象即可。

jvm_gc_cms_04

该阶段很耗时,因为需要进行对象的清理,因为是跟系统程序并发运行的,所以不影响系统程序的执行。

1.2.3. CMS 垃圾回收机制性能分析及相关参数

了解 CMS 垃圾回收机制之后,发现已经尽可能的进行性能优化了。

最耗时的其实就是对老年代全部对相关进行GC Roots追踪(标记出来到底哪些可以回收),然后对各种垃圾对象从内存里清理掉。

即第二阶段和第四阶段,这两个阶段都是和系统程序并发执行的,所以两个最耗时的阶段对性能影响不大。

只有 第一个阶段和第三个阶段是需要Stop the World,这两个阶段都是简单的标记,速度非常的快,所以对系统运行响应也不大。

回收线程数量

CMS的垃圾回收线程是比较耗费CPU资源的。默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4。(eg:2核CPU,(2 + 3) / 4 = 1个垃圾回收线程)

Concurrent Mode Failure

为了保证在CMS垃圾回收期间,让对象可以进入老年代来存放,会预留一些空间。所以CMS垃圾回收的触发时机,其中有一个条件就是老年代内存占用达到一定比例,就自动执行GC。

通过参数-XX:CMSInitiatingOccupancyFaction来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是92%。 (eg:老年代占用92%空间了,就自动进行CMS垃圾回收,预留8%的空间给并发回收期间一些新对象放入老年代中。)

如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间。此时会发生Concurrent Mode Failure,代表并发垃圾回收失败,就是一边回收一边把对象放入老年代,导致内存不够了。

这种情况会自动用Serial Old垃圾回收器替代CMS,系统程序进入Stop the World状态,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生,然后一次性把垃圾对象都回收掉,再恢复系统线程。

内存碎片

老年代的CMS采用“标记-清理”算法,每次都是标记出来垃圾对象,然后一次性回收掉,这样会导致大量的内存碎片产生。内存碎片太多,会导致后续对象进入老年代找不到可用的连续内存空间,然后频繁触发Full GC

CMS有一个参数是-XX:+UseCMSCompactAtFullCollection,默认打开,该参数是在Full GC之后要再次进行Stop the World,然后进行碎片整理,把存活对象挪到一起,使内存空间连续,避免内存碎片。

还有一个参数是-XX:CMSFullGCsBeforeCompaction,该参数是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,就是每次Full GC之后都会进行一次内存整理。


1.3. 总结

  • JDK 8 及之前:
    • 低延迟 → ParNew + CMS(已废弃,不建议新项目使用)。

results matching ""

    No results matching ""