垃圾回收器简介

Serial和Serial Old垃圾回收器:分别用来回收新生代和老年代的垃圾对象 工作原理就是单线程运行,垃圾回收的时候会停止我们自己写的系统的其他工作线程,让我们系统直接卡死不动,然 后让他们垃圾回收,这个现在一般写后台Java系统几乎不用。

ParNew和CMS垃圾回收器:ParNew现在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,他 们都是多线程并发的机制,性能更好,现在一般是线上生产系统的标配组合。

G1垃圾回收器:统一收集新生代 和老年代,采用了更加优秀的算法和设计机制,是下下周的重点,一周都会来分析 G1垃圾回收器的工作原理和优缺点。

最常用的新生代垃圾回收器:ParNew

​ 通常大家线上系统都是ParNew垃圾回收器作为新生 代的垃圾回收器 当然现在即使有了G1,其实很多线上系统还是用的ParNew。新生代的ParNew垃圾回收器主打的就是多线程垃圾回收机制,另外一种Serial垃圾回收器主打的是单线程垃 圾回收,他们俩都是回收新生代的,唯一的区别就是单线程和多线程的区别,但是垃圾回收算法是完全一样的。使用“-XX:+UseParNewGC”选项,只要加入这个选项,JVM启动之后对新生代进行垃圾回收的,就是 ParNew垃圾回收器了。一旦指定了使用ParNew垃圾回收器之后,他默认给自己设置的垃圾回收线程的数量就是跟CPU的核数是一样的,但是如果你一定要自己调节ParNew的垃圾回收线程数量,也是可以的,使用**-XX:ParallelGCThreads**参数即可, 通过他可以设置线程的数量,但是建议一般不要随意动这个参数

思考:

  • 到底是用单线程垃圾回收好,还是多线程垃圾回收好? 到底是Serial垃圾回收器好还是ParNew垃圾回收器好?

​ 启动系统的时候是可以区分服务器模式和客户端模式的,如果你启动系统的时候加入“-server”就是服务器模式,如果加入“-cilent”就是客户端模式。一般以服务器启动使用多核cpu时使用ParNew更好,因为多线程并行垃圾回收,充分利用多核CPU资源,可以提升性能。如果运行在windows上如果是单核的使用Serial好,此时你如果要是还是用ParNew来进行垃圾回收,就会导致一个CPU运行多个线程,反而加重了性能开销,可能效率还不如单线程好

CMS垃圾回收器

​ 一般老年代我们选择的垃圾回收器是CMS,他采用的是标记清理算法

​ CMS垃圾回收器采取的是垃圾回收线程和系统工作线程尽量同时执行的模式来处理的

​ CMS执行一次垃圾回收的过程分为4个阶段:

  • 初始标记

  • 并发标记

  • 重新标记

  • 并发清理

方法的局部变量和类的静态变量是GC Roots。但是类的实例变量不是GC Roots

初始标记:标记出所有GC Roots直接引用的对象,此时会造成STW暂停一切工作线程,但是影响不大,因为他的速度特别快,仅仅只是标记GC Roots直接引用的一些对象

并发标记:就是对老年代所有对象进行GC Roots追踪,是最耗时的阶段,在这个阶段会让系统线程可以随意创建各种新对象继续运行,在这个阶段可能会创建各种新的对象,也可能会让部分存活对象失去引用变成垃圾对象,在这个过程中垃圾回收线程会尽可能对已有的对象进行GC Roots追踪

重新标记:在并发标记时,一边标记存活对象和垃圾对象,一边系统还在不停运行创建对象,让老对象变成垃圾,所以当并发标记结束时,会有很多存活对象和垃圾对象时并发标记没有标记出来的,此时进入重新标记,会再次进入STW,重新标记时对并发标记阶段中系统运行运行变动过的少数对象进行标记,所有速度会很快

并发清理:这个阶段就是让系统程序随意运行,然后他来清理掉之前标记为垃圾的对象即可,并发清理阶段其实是很耗时的,因为需要进行对象的清理,但是他也是跟系统程序并发运行的,所以其实也不影响系统程序的执行

碎片整理:整理内存碎片

并发回收垃圾导致CPU资源紧张

​ CMS垃圾回收器有一个最大的问题,虽然能在垃圾回收的同时让系统同时工作,但是大家发现没有,在并发标记和并发清理两个最耗时 的阶段,垃圾回收线程和系统工作线程同时工作,会导致有限的CPU资源被垃圾回收线程占用了一部分,并发标记的时候,需要对GC Roots进行深度追踪,看所有对象里面到底有多少人是存活的,但是因为老年代里存活对象是比较多的,这个过程会追踪大量的对象,所以耗时较高。并发清理,又需要把垃圾对象从各种随机的内存位置清理掉,也是比较耗时的。

​ 所以在这两个阶段,CMS的垃圾回收线程是比较耗费CPU资源的。CMS默认启动的垃圾回收线程的数量是(CPU核数 + 3)/ 4。我们用最普通的2核4G机器和4核8G机器来计算一下,假设是2核CPU,本来CPU资源就有限,结果此时CMS还会有个“(2 + 3) / 4” = 1个垃圾回收线程,去占用宝贵的一个CPU。

​ 所以其实CMS这个并发垃圾回收的机制,第一个问题就是会消耗CPU资源

Concurrent Mode Failure问题

​ 在并发清理阶段,CMS只不过是回收之前标记好的垃圾对象,但是这个阶段系统一直在运行,可能会随着系统运行让一些对象进入老年代,同时还变成垃圾对象,这种垃圾对象是“浮动垃圾”,但是CMS只能回收之前标记出来的垃圾对象,不会回收他们,需要等到下一次GC的时候才会回收他们。

​ 以为了保证在CMS垃圾回收期间,还有一定的内存空间让一些对象可以进入老年代,一般会预留一些空间。CMS垃圾回收的触发时机,其中有一个就是当老年代内存占用达到一定比例了,就自动执行GC。-XX:CMSInitiatingOccupancyFaction参数可以用来设置老年代占用多少比例的时候触发CMS垃圾回收,JDK 1.6里面默认的值是 92%。

​ 那么如果CMS垃圾回收期间,系统程序要放入老年代的对象大于了可用内存空间,这个时候,会发生Concurrent Mode Failure,就是说并发垃圾回收失败了,我一边回收,你一边把对象放入老年代,内存都不够了,此时就会自动用Serial Old垃圾回收器替代CMS,就是直接强行把系统程序“Stop the World”,重新进行长时间的GC Roots追 踪,标记出来全部垃圾对象,不允许新的对象产生,然后一次性把垃圾对象都回收掉,完事了再恢复系统线程

内存碎片问题

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

​ 所以CMS不是完全就仅仅用“标记-清理”算法的,因为太多的内存碎片实际上会导致更加频繁的Full GC

​ CMS有一个参数是“-XX:+UseCMSCompactAtFullCollection”,默认就打开了,意思是在Full GC之后要再次进行“Stop the World”,停止工作线程,然后进行碎片整理,就是把存活对象挪到一起,空出来大片连续内存空间,避免内存碎片。

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

8G内存下CMS常用的JVM配置

-Xms4096M -Xmx4096M -Xmn3072M -Xss1M -XX:PermSize=256M -XX:MaxPermSize=256M -XX:+UseParNewGC - XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection - XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark

思考

  1. ​ 为什么老年代的垃圾回收速度会比新生代的垃圾回收速度慢很多倍?到 底慢在哪里?

​ 新生代PerNew执行速度其实很快,因为直接从GC Roots出发就追踪哪些对象是活的就行了,新生代存活对象是很少的,这个速度是极快的, 不需要追踪多少对象。 然后直接把存活对象放入Survivor中,就一次性直接回收Eden和之前使用的Survivor了。

​ 老年代CMS在并发标记阶段,他需要去追踪所有存活对象,老年代存活对象很多,这个过程就会很慢; 其次并发清理阶段,他不是一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象,速度也很慢; 最后完事儿了,还得执行一次内存碎片整理,把大量的存活对象给挪在一起,空出来连续内存空间,这个过程还得“Stop the World”,那就更慢了。 万一并发清理期间,剩余内存空间不足以存放要进入老年代的对象了,引发了“Concurrent Mode Failure”问题,那更是麻烦,还得 立马用“Serial Old”垃圾回收器,“Stop the World”之后慢慢重新来一遍回收的过程,这更是耗时了。

  1. 还记得说过几个触发老年代GC的时机吗?

    第一是老年代可用内存小于新生代全部对象的大小,如果没开启空间担保参数,会直接触发Full GC,所以一般空间担保参数都会打开;

    第二是老年代可用内存小于历次新生代GC后进入老年代的平均对象大小,此时会提前Full GC;

    第三是新生代Minor GC后的存活对象大于Survivor,那么就会进入老年代,此时老年代内存不足。

    第四就是“-XX:CMSInitiatingOccupancyFaction”参数,如果老年代可用内存大于历次新生代GC后进入老年代的对象平均大小,但是老年代已经使用的 内存空间超过了这个参数指定的比例,也会自动触发Full GC

    上述情况都会导致老年代Full GC。