1. JVM ParNew + CMS垃圾回收器
1.1. ParNew
1.1.1. ParNew垃圾回收器简介
ParNew是用在新生代的垃圾回收器。即使有G1回收器,还有很多线上系统在用的ParNew回收器。
该回收器使用的是复制算法,采用多线程来进行回收,提高了回收效率。Serial垃圾回收器回收算法和ParNew一样,但是是单线程的。
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
状态。
虽然要Stop the World
暂停一切工作线程,但影响不大,因为速度很快,仅仅标记GC Roots直接引用的对象即可。(方法的局部变量和类的静态变量 是GC Roots。但是 类的实例变量不是GC Roots。)
并发标记
第二阶段,并发标记阶段,会让系统可以创建各种新对象,也可能部分存活对象失去引用,变成垃圾对象。
就是对老年代所有对象进行GC Roots追踪,是最耗时的。该阶段需要追踪所有对象是否从根源上被GC Roots引用(查看引用来源时候还有效),但这个最耗时的阶段是跟系统程序并发运行的,所以这个阶段不会对系统运行造成性能体验上的影响。
重新标记
第三个阶段,重新标记阶段,在第二阶段中会创建新的对象及老对象可能会变成垃圾对象,在该阶段会再次进入Stop the World
状态,对这部分对象进行重新标记。
重新标记阶段,速度会很快,第二阶段中变动过的对象会比较少,所以运行速度很快。
并发清理
第四阶段,并发清理阶段,该阶段就是让系统程序随意运行,然后同时清理掉标记为垃圾的对象即可。
该阶段很耗时,因为需要进行对象的清理,因为是跟系统程序并发运行的,所以不影响系统程序的执行。
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(已废弃,不建议新项目使用)。