下面我们再来看下JVM的一些内存分配与回收策略:
(A) 对象分配规则
1.对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
2.大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝。通过参数-XX:PretenureSizeThreshold=3145728控制。
3.长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,对象每熬过了1次Minor GC对象的年龄加1,达到阀值对象进入老年区。通过参数-XX:MaxTenuringThreshold=15(默认)控制。
4.动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold要求的年龄数。
5.空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。 (-XX:-HandlePromotionFailure)
(B) 术语说明
Young Generation(新生代):分为:Eden区和Survivor区,Survivor区有分为大小相等的From Space和To Space。
Old Generation(老年代): 当 OLD 区空间不够时, JVM 会在 OLD 区进行 major collection。
Minor GC:新生代GC,指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
Major GC:发生老年代的GC,对整个堆进行GC。出现Major GC,经常会伴随至少一次Minor GC(非绝对)。MajorGC的速度一般比minor GC慢10倍以上。
Full GC:整个虚拟机,包括永久区、新生区和老年区的回收。
(C) 分配规则实例
1> 对象优先在Eden分配
虚拟机提供了-XX:+PrintGCDetails 参数打印收集器日志,并且在进程退出时输出当前内存各区域的分配情况。
通过 -Xms20M -Xmx20M -Xmn10M这3个参数限制java堆大小为20MB,且不可扩展。其中10MB分配给新生代,剩下的10MB分配给老年代。
-XX:SurvivorRatio=8 决定了新生代中Eden区与一个Survivor区的比例为8:1,即Eden区=8MB,一个Survivor=1MB,另一个Survivor也为1MB。
/**
* eden 对象通过分配担保机制提前转移到老年代去
*
* vm 参数 -verbose:gc -Xms20M -Xmx20M -Xmn10M
-XX:SurvivorRatio=8 -XX:+PrintGCDetails
*/
public class MinorGC {
private static final int _1MB=1024*1024;
public static void testMinorGC(){
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[4 * _1MB]; //出现一次minor GC
}
public static void main(String[] args) {
testMinorGC();
}
}
Heap
PSYoungGen total 9216K, used 6892K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 84% used [0x00000000ff600000,0x00000000ffcbb238,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 80% used [0x00000000fec00000,0x00000000ff4000a8,0x00000000ff600000)
Metaspace used 2660K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 284K, capacity 386K, committed 512K, reserved 1048576K
从运行结果我们可以很清晰的看到,eden有8MB的存储控件(通过参数配置),前6MB的数据优先分配到eden区域,当下一个2MB存放时,因空间已满,触发一次GC,但是这部分数据因为没有回收(引用还在,当赋值为null后则不会转移),数据会被复制到s0区域,但是s0区域不够存储,因此直接放入老生代区域,新的2MB数据存放在eden区域
2> 大对象直接进入老生代
/**
* -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -verbose:gc
* @author 高国藩
* @date 2018年8月20日 下午2:03:15
*/
@SuppressWarnings("unused")
public class BigObjIntoOld {
private final static int ONE_MB = 1024*1024;
public static void main(String[] args) {
byte[] testCase1,testCase2,testCase3,testCase4;
testCase1 = new byte[8*ONE_MB];
}
}
Heap
PSYoungGen total 9216K, used 1147K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 14% used [0x00000000ff600000,0x00000000ff71ef58,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400010,0x00000000ff600000)
Metaspace used 2634K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
可以从日志中看出,该对象直接在老年代上分配
3> 年长者(长期存活对象)进入老生代
package org.fanmi.JavaDemo;
/**
* @Described:当年龄大于一定值的时候进入老生代 默认值15岁
* VM params : -Xms20M -Xmx20M -Xmn10M -XX:MaxTenuringThreshold=1 -XX:+PrintGCDetails -verbose:gc
*/
public class BigObjIntoOld {
private final static int ONE_MB = 1024 * 1024;
public static void main(String[] args) {
@SuppressWarnings("unused")
byte[] testCase1, testCase2, testCase3, testCase4;
testCase1 = new byte[1 * ONE_MB / 4]; // 0.25MB
testCase2 = new byte[7 * ONE_MB + 3 * ONE_MB / 4]; // 7.75MB
testCase2 = null;
testCase3 = new byte[7 * ONE_MB + 3 * ONE_MB / 4];
testCase3 = null;
testCase4 = new byte[ONE_MB];
}
}
[GC (Allocation Failure) [PSYoungGen: 1239K->856K(9216K)] 9175K->8800K(19456K), 0.0008654 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 8792K->0K(9216K)] 16736K->8732K(19456K), 0.0007237 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 0K->0K(9216K)] [ParOldGen: 8732K->769K(10240K)] 8732K->769K(19456K), [Metaspace: 2627K->2627K(1056768K)], 0.0043113 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 9216K, used 1106K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 13% used [0x00000000ff600000,0x00000000ff714930,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 769K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 7% used [0x00000000fec00000,0x00000000fecc05e8,0x00000000ff600000)
Metaspace used 2634K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
从代码中我们可以看到,当testCase1划分为0.25MB数据,进行多次大对象创建之后,testCase1应该在GC执行之后被复制到s0区域(s0足以容纳testCase1),但是我们设置了对象的年龄为1,即超过1岁便进入老生代,因此GC执行2次后testCase1直接被复制到了老生代,而默认进入老生代的年龄为15。我们通过profilter的监控工具可以很清楚的看到对象的年龄,如图所示:
4> 群体效应(大批中年对象进入老生代)
package org.fanmi.JavaDemo;
/**
* @Described:s0占用空间到达50%直接进入老生代
* VM params : -Xms20M -Xmx20M -Xmn10M -XX:MaxTenuringThreshold=15 -XX:+PrintGCDetails -verbose:gc
* Edon s0 s1 old age
* 8 1 1 10 15
* 0.5 0 0 7.5
* 7.5 0.5 0 7.5
* 7.5 0 0 8
* @author YHJ create at 2012-1-3 下午05:50:40
* @FileNmae com.yhj.jvm.gc.dynamicMoreAVG_intoOld.MoreAVG_intoOld.java
*/
public class BigObjIntoOld {
private final static int ONE_MB = 1024 * 1024;
public static void main(String[] args) {
@SuppressWarnings("unused")
byte[] testCase1, testCase2, testCase3, testCase4;
testCase1 = new byte[7 * ONE_MB + ONE_MB / 2];
testCase2 = new byte[ONE_MB / 2];
testCase3 = new byte[7 * ONE_MB + ONE_MB / 2];
testCase3 = null;
testCase4 = new byte[7 * ONE_MB + ONE_MB / 2];
}
}
[GC (Allocation Failure) [PSYoungGen: 1495K->992K(9216K)] 9175K->8760K(19456K), 0.0009134 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 8672K->992K(9216K)] 16440K->8776K(19456K), 0.0008838 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 9216K, used 8754K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 94% used [0x00000000ff600000,0x00000000ffd94930,0x00000000ffe00000)
from space 1024K, 96% used [0x00000000fff00000,0x00000000ffff8030,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 7784K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 76% used [0x00000000fec00000,0x00000000ff39a020,0x00000000ff600000)
Metaspace used 2634K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 281K, capacity 386K, committed 512K, reserved 1048576K
新生代中特定年龄中的内存和大于了s0的一半,该对象将提前全部进入老年代中。
5> 担保GC(担保minorGC)
在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否是否大于老年代剩余空间的大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,则只进行Minor GC,如果不允许,则进行一次Full GC。
老年代进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来,在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年达对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多的空间。
如某次Minor GC存活后的对象突增,远高于平均值的话,会导致担保失败。如果出现了HandlePromotionFailure,那就只好在失败后重新发起一次Full GC。
但是该配置现在在高级版本的Java中已经失效了,JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。