1. 回顾CMS的 STW
为了避免长时间STW,CMS采用了4个阶段来垃圾回收,其中初始标记和重新标记,耗时很短,虽然会导致 “Stop the World” ,但是影响不大。
然后并发标记和并发清理,两个阶段耗时最长,但是可以跟系统的工作线程并发运行,所以对系统没有太大影响。
这就是CMS的基本工作原理。
2.并发回收垃圾导致CPU资源紧张
如图,由于在垃圾回收的并发标记和并发清理两个阶段,同时让系统同时工作,会导致有限的CPU资源被垃圾回收线程占用了一部分。
并发标记的时候, 需要对GC Roots进行深度追踪,看所有对象里面到的有多少存活的。但是因为老年代里存活对象是比较多的,这个过程会追踪大量的对象,所以耗时较高。
并发清理,又需要把垃圾对象从各种随机的内存位置清理掉,也是比较耗时的。
所以在这两个阶段,CMS的垃圾回收线程是比较耗费CPU资源的。CMS默认启动的垃圾回收线程的数量是 (CPU核数 + 3)/ 4。
举例,用最普通的2核4G机器和4核8G机器来计算,前者因为CPU资源有限,此时CMS 还会有个 “(2 + 3) / 4” = 1 个垃圾回收线程,去占用宝贵的一个CPU。
所以CMS这个并发垃圾回收的机制,第一个问题就是会消耗CPU资源。
3. Concurrent Mode Failure问题
Concurrent Mode Failure问题,也就是并发模式失败的问题。
在并发清理阶段,CMS只不过是回收之前标记好的垃圾对象。
这一阶段的系统在运行时,可能会有一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是 “浮动垃圾” 。
如上图,标记为“新的” 的对象就是在并发清理期间,系统程序可能先把某些对象分配在新生代,然后可能触发了Minor GC,一些对象进入老年代,然后短时间内又没人引用这些对象了。
这种对象,就是老年代的**“浮动垃圾”** 。它虽然成了垃圾,但CMS只能回收之前标记出来的垃圾对象,不会回收他们,需要等到下一次GC的时候才会回收他们。
所以,为了保证在CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。
CMS垃圾回收的触发时机,其中有一个就是当老年代内存占用达到一定比例了,就自动执行GC。
使用“-XX:CMSInitiatingOccupancyFaction” 参数来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6 里面默认的值是 92%。
意思是,老年代占用了 92%的空间了,就自动进行CMS垃圾回收,预留8%的空间个并发回收期间,系统程序把一些新对象放入老年代中。
当CMS垃圾回收期间,系统程序要放入老年代的对象大于可用内存空间,就会发生 Concurrent Mode Failure,就是说并发垃圾回收失败了,一边回收,一边把对象放入老年代,内存都不够了。
此时就会自动用 “Serial Old” 垃圾回收器替代CMS,也就是直接强行把系统程序 STW ,重新进行长时间的GC Roots追踪,标记出来全部垃圾对象,不允许新的对象产生。
然后一次性把垃圾对象都回收掉,完事儿了再恢复系统线程。
在实际生产中,这个自动触发CMS垃圾回收的比例需要合理优化,避免 “Concurrent Mode Failure” 问题。
4. 内存碎片问题
老年代的CMS采用 “标记-清理” 算法,每次标记出垃圾对象,然后一次性回收掉,都会导致大量的内存碎片产生。
如果内存碎片太多,会导致后续进入老年代的对象找不到可用的连续内存空间,然后触发Full GC。
CMS中的参数 “-XX:+UseCMSCompactAtFullCollection” ,默认打开了,它的意思是在Full GC之后要再次进行 STW,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片。
另一个参数 “-XX:CMSFullGCsBeforeCompaction” ,意思是执行多少次Full GC之后再执行一次内存碎片整理的工作,默认是0,即每次Full GC之后都会进行一次内存整理。
在内存碎片整理完之后,存活对象都放在一起,然后就能空出来大片连续内存空间可供使用。
5. 思考题
有哪几个触发老年代GC的时机?
答:
第一,是老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开;
第二,老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;
第三,新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时若老年代内存不足,就会进行Full GC。
新增第四点,当配置了 "-XX:CMSInitiatingOccupancyFaction" 参数,老年代使用率达到临界点,触发Full GC。