关于Java虚拟机的垃圾回收,前面分别提到
1. 哪些需要回收?什么时候回收? 如何进行回收?
2.四种不同的垃圾回收算法
3. 七种应用于不同场景下的垃圾回收器
本次着重关注于虚拟机的内存分配和回收策略
1. 内存分配回收策略
在 四种不同的垃圾回收算法 中,我们知道目前主流的商业虚拟机的垃圾回收器都采用分代收集算法,下图便是HotPot对于不同年代内存的划分:
对象的内存分配大体上可以分为三种情况:
- 在堆上分配(JIT编译优化后可能在栈上分配),主要在新生代的Eden区中分配;
- 如果启用了本地线程分配缓冲,将线程优先在TLAB上分配;
- 少数情况下,可能直接分配在老年代中。
内存分配的主要规则:
1. 对象优先在Eden中分配
前面曾介绍HotSpot虚拟机新生代内存布局及算法
- 将新生代内存分为一块较大的Eden空间和两块较小的Survivor空间;
- 每次使用Eden和其中一块Survivor;
- 当回收时,将Eden和使用中的Survivor中还存活的对象一次性复制到另外一块Survivor;
- 而后清理掉Eden和使用过的Survivor空间;
- 后面就使用Eden和复制到的那一块Survivor空间,重复步骤3;
默认Eden:Survivor=8:1,即每次可以使用90%的空间,只有一块Survivor的空间被浪费;
大多数情况下,对象在新生代Eden区中分配;
当Eden区没有足够空间进行分配时,JVM将发起一次Minor GC(新生代GC);
Minor GC时,如果发现存活的对象无法全部放入Survivor空间,只好通过分配担保机制提前转移到老年代。
2. 大对象直接进入老年代
- 大对象指需要大量连续内存空间的Java对象,如,很长的字符串、数组;
- 经常出现大对象容易导致内存还有不少空间就提前触发GC,以获取足够的连续空间来存放它们,所以应该尽量避免使用创建大对象;
- 可以设置这个阈值 “-XX:PretenureSizeThreshold”:,大于这个参数值的对象直接在老年代分配;
此参数默认为0(无效),且只对Serail和ParNew两款收集器有效。 如果需要使用该参数,可考虑ParNew+CMS组合。
3. 长期存活的对象进入老年代
JVM给每个对象定义一个对象年龄计数器,其计算流程如下:
-
在Eden中分配的对象,经Minor GC后还存活,就复制移动到Survivor区,年龄为1;
-
而后每经一次Minor GC后还存活,在Survivor区复制移动一次,年龄就增加1岁;
-
如果年龄达到一定程度,就晋升到老年代中;
“-XX:MaxTenuringThreshold”:设置新生代对象晋升老年代的年龄阈值,默认为15;
JVM为更好适应不同程序,不是永远要求等到MaxTenuringThreshold中设置的年龄;
如果在Survivor空间中相同年龄的所有对象大小总和大于Survivor空间的一半,大于或等于该年龄的对象就可以直接进入老年代;
4.空间分配担保
-
分配担保
当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion); -
流程:
- 在发生Minor GC前,JVM先检查老年代最大可用的连续空间是否大于新生所有对象空间;
- 如果大于,那可以确保Minor GC是安全的;
- 如果不大于,则JVM查看HandlePromotionFailure值是否允许担保失败;
- 如果允许,就继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小;
- 如果大于,将尝试进行一次Minor GC,但这是有风险的;
- 如果小于或HandlePromotionFailure值不允许冒险,那这些也要改为进行一次Full GC;
在上面的第五步中,尝试Minor GC会有担保失败风险,原因在于因为尝试Minor GC前面,无法知道存活的对象大小,所以使用历次晋升到老年代对象的平均大小作为经验值;假如尝试的Minor GC最终存活的对象远远高于经验值的话,会导致担保失败(Handle Promotion Failure);失败后只有重新发起一次Full GC,这绕了一个大圈,代价较高;
但一般还是要开启HandlePromotionFailure,避免Full GC过于频繁,而且担保失败概率还是比较低的;
JDK6-u24后,JVM代码中已经不再使用HandlePromotionFailure参数了;
规则变为:
只要老年代最大可用的连续空间大于新生所有对象空间或历次晋升到老年代对象的平均大小,就会进行Minor GC;否则进行Full GC;
即老年代最大可用的连续空间小于新生所有对象空间时,不再检查HandelPromotionFailure,而直接检查历次晋升到老年代对象的平均大小;