年轻代中的GC
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象 经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就晋升到年老代中()。
因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
什么时候会触发Minor GC?
1 Eden区域满了
2 新创建的对象大小 > Eden所剩空间
3 超过设置的直接晋升大小 PretenureSizeThreshold
4 对象年龄超过阈值 15
CMS设置了CMSScavengeBeforeRemark参数,这样在CMS的Remark之前会先做一次Minor GC来清理新生代,加速之后的Remark的速度。这样整体的stop-the world时间反而短, Full GC的时候会先触发Minor GC
什么时候会触发Full GC?
1种是之前每次晋升的对象的平均大小 > 老年代剩余空间
1种是Minor GC后存活的对象超过了老年代剩余空间。这两种情况都是因为老年代会为新生代对象的晋升提供担保,而每次晋升的对象的大小是无法预测的,所以只能基于统计,1个是基于历史平均水平,一个是基于下一次可能要晋升的最大水平。
这两种情况都是属于promotion failure
promotion failed
concurrent mode failed
并说明什么情况下会发生,可以如何避免?
promotion failed
在进行Minor GC时,survivor space放不下、对象只能放入旧生代,而此时旧生代也放不下造成的;
concurrent mode failed
concurrent mode failure是在执行CMS GC的过程中同时有对象要放入旧生代,而此时旧生代空间不足造成的。
参数设置不当,一个是在年老代被用完之前不能完成对无引用对象的回收;一个是当新空间分配请求在年老代的剩余空间中得不到满足
方法一是降低触发CMS的阀值,即参数-XX:CMSInitiatingOccupancyFraction的值,默认值是68,所以这里调低到50,让CMS GC尽早执行,以保证有足够的空间
并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾。
重新标记(Remark) 的作用在于:
之前在并发标记时,因为是 GC 和用户程序是并发执行的,可能导致一部分已经标记为 从 GC Roots 不可达 的对象,因为用户程序的(并发)运行,又可达 了,Remark 的作用就是将这部分对象又标记为 可达对象。
至于 “浮动垃圾”,因为 CMS 在 并发标记 时是并发的,GC 线程和用户线程并发执行,这个过程当然可能会因为线程的交替执行而导致新产生的垃圾(即浮动垃圾)没有被标记到;而 重新标记 的作用只是修改之前 并发标记 所获得的不可达对象,所以是没有办法处理 “浮动垃圾” 的。
问题解决:中间调整过几次,先搞了几台机器做了验证,后来逐步推广的。
1、调大heap区,由原来的4g,调整到5g,young区的大小不变,还是2g,这时候old区就由2g变为3g了(这样保证old区有足够的空间);
2、设置-XX:UseCMSInitiatingOccupancyOnly,其实这个不关这个问题,只是发现半夜CMS进行的有点频繁,就禁止掉了悲观策略;
3、设置CMS区回收的比例,从80%调整到75%,让old区尽早的进行,有足够的空间剩余;
Heap什么时候会发生OOM?
当花在GC的时间超过了GCTimeLimit,这个值默认是98%
当GC后的剩余空间小于GCHeapFreeLimit,这个值默认是2%
7. 什么是剩余空间不够?
剩余空间不够不是说整体的空间不够分配某个对象,而是说连续的空间不够分配给某个对象。所以一旦内存碎片大多就可能发生剩余空间不够的问题,所以CMS这种收集器,需要在标记-清除几次之后进行压缩,进行优化。CMSFullGCsBeforeCompaction可以设置进行几次清除之后进行压缩
8. Full GC的次数说的是stop the world的次数,所以一次CMS至少会让Full GC的次数+2,因为CMS Initial mark和remark都会stop the world,记做2次。而CMS可能失败再引发一次Full GC
9. JMI默认会一个小时调用一次System.gc()清理缓存,所以可以DisableExplicitGC,也可以设置sun.rmi.dgc.client.gcInterval和sun.rmi.dgc.server.gcInterval参数来规定JMI清理的时间
10. 对于性能调优来说,应该理解对于给定的硬件,给定的算法(垃圾收集器),单个/多个线程单位时间内能够回收的空间是接近一个常量的。如果想要缩短GC的时候,就要考虑是否要相应调小空间
11. CMS收集器会了减少stop the world的时间,让GC线程和业务线程并发,这样也就相对拉长了CMS收集器单次GC的时间
12. 尽可能地让对象停留在新生代,因为新生代采用了复制算法,相对收回得更快,而且Minor GC的次数肯定比Full GC多,那么对象在新生代被清除的更能性会更高。而对象一旦进入到老年代,那么只有Full GC时才会回收,对象在整个系统停留的时间就会很长,很可能创建的它的线程早就死了,而它还活着
13. 为了尽可能让对象停留在新生代,就要注意设置Survivor区域的大小,因为它直接和对象是否进入老年代相关。之前就遇到过这种情况,明明新生代还有很大的空间,但是每次Minor GC后总是有对象进入到了老年代。后来发现由于Survivor太小,导致Tenuring Threshold为1,意思是年龄为1的对象大小超过了Survivor / 2(可通过TargetSurvivorRatio来调节,默认是50,即1/2),年龄只要超过1的对象这时候就要直接进入老年代了。而进入老年代,对象就只有在Full GC的时候才会被清除。而如果调大了Survivor空间,让对象对象尽量接近Max Tenuring Threshold时才进入到老年代,这时候会大大减少老年代的对象大小,并且让对象在新生代停留时间变长,提高了它们被快速清理出系统的概率。
网友评论