深入理解JVM学习笔记之:年轻代和老年代垃圾回收算法分析
-
经过15次GC的对象会进入老年代
-
动态年龄判断
-
大对象直接进入老年代
-
Minor GC后 的对想太多,无法放入Survivor区怎么办?
-
老年代空间内存分配担保
-
老年代垃圾回收算法
首先我们来看下面的图,我们写好的代码在运行时,就会不断的创建各种各样的对象,这些对象都会优先放到新生代的Eden区和survivor1区域
接着假如新生代的Eden区和Survivor1区都快满了,此时就会触发Minor GC,把存活对象转移到Survivor2区
如图:
然后接着就会使用Eden区和Survivor1区继续为对象分配内存空间
1.躲过15次GC之后进入老年代
默认的设置下,当对象的年龄达到15岁时,也就是躲过15次GC的时候,他就会转移到老年代去
这个具体是多少岁转移到老年代,可以通过JVM参数 "-XX:MAX TenuringThreshold" 来设置,默认是15
2.动态年龄判断
这里有另外一个规则可以让对象进入老年代,不用等到默认15次GC后才可以。
他们的规则是,假如说当前放对象的Survivor区域里,一批对象的总大小大于zhe块区域的内存大小的50%,
那么大于等于这批对象年龄的对象,就可以直接进入老年代
如图:
假如这个图里Survivor区有两个对象,这两个对象的年龄都是一样的 都是2岁
然后两对象加起来超过50MB,超过了Survivor2区的内存大小的一半,这个时候,Survivor区里的大于等于2岁的对象,就要全部进入老年代里去了
这就是所谓的动态年龄判断的规则,这条规则也会让一些新生代的对象进入老年代
另外一个概念,其实这个规则运行是按照如下逻辑:年龄1+年龄2+年龄的多个对象内存占用总和超过了Survivor区域的50%,此时此时就会把年龄n以上的对象放入老年代
无论是15次GC后进入老年代还是动态对象年龄判断,都是希望那些可能长期存活的对象,转移到老年代
3.大对象直接进入老年代
有一个JVM参数,就是"-XX:PretenureSizeThreshold",可以把它设置为字节数,如“1048576”字节,就是1MB
它的意思是,如果要创建一个大于这个内存大小的对象,比如一个超大的数组,或者是别的啥东西,此时就直接把这个对象放入到老年代,就不会进入新生代
之所以这么做,就是要避免新生代里出现那种大对象,然后屡次躲过GC,还得把他在两个Survivor区域来回复制多次之后才能进入老年代
那么大的对象在内存里 来回复制,是很耗时的过程
所以说这也是一个对象进入老年代的规则
4.Minor GC 后的对象太多无法放入Survivor区怎么办?
现在有一个比较大的问题,就是如果在Minor GC之后发现剩余的存活对象太多,没办法都放入另一块Survivor区怎么办?
比如上图,假如在发生GC的时候,发现Eden区超过150MB的存活对象,此时没办法放入Survivor区中,此时该怎么办呢?
这个时候就必须把这些对象直接转移到老年代
5.老年代的空间内存分配担保
首先,在执行任何一次MinorGC 之前,JVM都会检查老年代可用的可用内存空间,是否大于新生代所有的对象的大小。
因为极端情况下,可能新生代Minor GC过后,所有对象都存活下来了,那岂不是新生代所有对象全部要进入老年代?
如果说发现老年代的内存大小是大于新生代所有对象的内存大小,那么可以放心大胆的进行MinorGC.
但是假如执行了Minor GC之前,发现老年代的可用内存已经小于新生代的全部对象大小
那么这个时候是不是有可能Minor GC 之后 存活下来的对象,全部需要转移到老年代,而老年代的空间又不够呢?
理论上,是有可能的
所以假如Minor GC之前,发现老年代的可用内存已经小于了新生代的全部对象大小, 就会看一个"-XX:-HandlePromotionFailure"的参数是否设置了
如果有这个参数,那么就会继续尝试进行下一-步判断。
下一步判断,就是看看老年代的内存大小,是否大于之前每一-次Minor GC后进入老年代的对象的平均大小。
举个例子,之前每次Minor GC后,平均都有10MB左右的对象会进入老年代,那么此时老年代可用内存大于10MB。
这就说明,很可能这次Minor GC过后也是差不多10MB左右的对象会进入老年代,此时老年代空间是够的,看下图。
如果上面那个步骤判断失败了,或者是“ -XX:-HandlePromotionFailure"参数没设置,此时就会直接触发-次"FullGC" , 就是对老年代进行垃圾回收,尽量腾出来-些内存空间,然后再执行Minor GC.
如果上面两个步骤都判断成功了。那么就是说可以冒点风险尝试一下Minor GC。此时进行Minor GC有几种可能。 第一种可能,Minor GC过后。剩余的存活对象的大小。是小于Sunivor区的大小的, 那么此时存活对象进入Survivor区域即可。
第二种可能, Minor GC过后, 剩余的存活对象的大小,是大于Survivor区域的大小,但是是小于老年代可用内存大小的,此时就直接进入老年代即可。
第三种可能,很不幸, Minor GC过后, 剩余的存活对象的大小。大于了Survivor区域的大小,也大于了老年代可用内存的大小。此时老年代都放不下这些存活对象了,就会发生 "Handle Promotion Failure”的情况,这个时候就会触发一次“FullGC"。
Full GC就是对老年代进行垃圾回收,同时也一般会对新生代进行垃圾回收。
因为这个时候必须得把老年代里的没人引用的对象给回收掉。然后才可能让Minor GC过后剩余的存活对象进入老年代里面。
如果要是Full GC过后, 老年代还是没有足够的空间存放Minor GC过后的剩余存活对象,那么此时就会导致所谓的"OOM"内存溢出了
因为内存实在是不够了,你还是要不停的往里面放对象,当然就崩溃了。
这段规则有点烧脑,但是我觉得如果大家仔细对这段文字多看两遍。然后结合我们的图,脑子里想一想,基本都能看懂这个规则。
6.老年代垃圾回收算法
其实把上面的内容都看懂之后,大家现在基本就知道了Minor GC的触发时机,然后就是Minor GC之前要对老年代空间大小做的检查
包括检查失败的时候要提前触发Full GC给老年代腾一些空间出来,或者是Minor GC过后剩余对象太多放入老年代内存都不够,也要触发Full GC。包括这套规则,还有触发老年代垃圾回收的Full GC时机。都给大家讲清楚了。
简单来说,-句话总结,对老年代触发垃圾回收的时机,-般就是两个:
要不然是在Minor GC之前, -通检查发现很可能Minor GC之后要进入老年代的对象太多了,老年代放不下,此时需要提前触发Full GC然后再带着进行Minor GC ;
要不然是在Minor GC之后,发现剩余对象太多放入老年代都放不下了。
那么对老年代进行垃圾回收采用的是什么算法呢?
简单来说,老年代采取的是标记整理算法,这个过程说起来比较简单
大家看下图,首先标记出来老年代当前存活的对象, 这些对象可能是东一个西一 个的。
大家-定要注意一点,这个老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。
如果系统频繁出现老年代的Full GC垃圾回收,会导致系统性能被严重影响,出现频繁卡顿的情况。