一、名词解释
JVM的内存分配及回收策略:
1.对象优先分配到Eden区中;
2.大对象直接进入到老年代;
3.长期存活的对象将进入老年代;
4.动态对象年龄判断;
5.空间分配担保机制
JVM的垃圾回收算法采用的分代回收算法,根据对象存活周期不同,将内存分为年轻代和老年代,这样可以因地制宜的选择相应的回收策略。年轻代采用的回收算法是复制算法,了解过该算法的童鞋肯定知道,该算法是把内存等分为二,但是年轻代并没有用50%的空间来做内存复制空间,因为根据IBM研究统计,98%的对象都是朝生夕死的,所以改进的分配策略是:一块比较大的Eden区,两块比较小的Survivor区,默认比例是8:1:1,可通过设置SurvivorRatio参数值调整,如SurvivorRatio=3,则Eden:S1:S2=3:1:1,对象优先在Eden区中分配,每次只使用Eden区和其中一个Survivor区,当Eden区没有足够的连续空间来分配给新对象时,会触发一次MinorGC(也有叫youngGC的,是一个意思),也就是年轻代的垃圾回收。未被回收掉的(GCRoots可达,还在使用),会被复制到From Survivor区,此时Eden区就可以被完全释放,并且,复制到From Survivor的对象会按顺序分配内存,这样便没有了碎片问题,同时,被复制的对象的age会加1, 当Eden区再次不够用了,会再次触发MinorGC,Eden区和Survivor区中还在使用的对象会被复制到To Survivor中,下一次minorGC,Eden和To Survivor区中还在使用的对象则会被复制到From Survivor区中,如此往复,经过若干次minorGC后,总有一些对象在From 和To区之间来回游荡,每熬过一次minorGC,对象的age就加一,直到对象的年龄达到一定值(默认为15,通过MaxTenuringThreshold设置),就会被移动到老年代,这就是长期存活的对象进入老年代,并且,为了能更好的适应不同场景下的内存情况,JVM并不总是要求对象一定要达到MaxTenuringThreshold才能晋升到老年代,当Survivor区中相同年龄的对象大小总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,这就是对象的动态年龄判断,当出现大量对象在MinorGC后仍然存活,就需要老年代进行分配担保,让Survivor无法容纳的对象进入到老年代,前提是老年代有足够的空间来存放这些对象,但是在实际完成内存回收之前JVM是无法知道到底有多少对象要被扔到老年代的,所以只好取之前每一次回收晋升到老年代的对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出空间,这就是空间分配担保机制。老年代中对象的存活率极高,且没有额外的空间进行分配担保,所以必须使用标记-清除或标记-整理算法进行回收。因为年轻代使用的是复制算法,如果有大对象分配在Eden区,回收时会出现对大对象的复制操作,比较耗时,且MinorGC的频率会提高,严重影响系统的吞吐量,因此,需要大量连续内存空间的对象,会被直接扔到老年代,最典型的就是那种很长的字符串、数组以及集合,这就是大对象直接进入老年代机制。
二、验证以上JVM的内存分配及回收策略。
1.对象优先分配到Eden区&&大对象直接进入老年代
/**
* 测试对象优先分配到Eden区&&大对象直接进入老年代
* -Xms200m -Xmx200m -Xmn100m -XX:+PrintGCDetails -XX:SurvivorRatio=3 -XX:PretenureSizeThreshold=31m -XX:+UseParNewGC
* 堆总大小200m,新生代总大小100m,Eden60m,s1及s2均为20m ,对象大于等于31m直接进入老年代
* 注意:网上查阅资料发现,描述的PretenureSizeThreshold参数的单位都是字节,但是实际测试过程中发现,配成m,会自动转换为字节,如此处配成31m
* 打印出来的 PretenureSizeThreshold = 32505856;此外PretenureSizeThreshold参数对Parallel Scavenge收集器是无效,此处配置的是ParNew收集器
* @author ljl
*/
public class TestEdenToAnther {
private static int _1MB = 1 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
//断点处
byte[] t1 = new byte[30 *_1MB];
byte[] t2 = new byte[10 *_1MB];
byte[] t3 = new byte[15 *_1MB];
byte[] t4 = new byte[45 *_1MB];
}
}
debug并结合jvisualvm:
初始状态,Eden区被占用了14.417M:
执行byte[] t1 = new byte[30*_1MB];后,t1被分配到Eden区,Eden空间此时被占用45.618M:
执行byte[] t2 = new byte[10*_1MB];后,t2被分配到Eden区,Eden空间此时被占用56.819M:
执行byte[] t3 = new byte[15*_1MB];Eden空间不足,触发MinorGC,t1(30M)大于Survivor区大小,被转移到老年代,t2(10M)小于Survivor区大小,被复制到Survivor1,Eden腾出空间,t3得以被分配到Eden:
执行byte[] t4 = new byte[45*_1MB];t4大于PretenureSizeThreshold设置的31m,被直接分配到老年代,至此,Eden存放了t3(15M),Survivor存放t2(10MB),老年代最终存放t1(30MB),t4(45MB),如下图所示:
2.长期存活的对象进入老年代
/**
* 测试长期存活的对象进入老年代
* -Xms200m -Xmx200m -Xmn100m -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseParNewGC
* 堆总大小200m,新生代总大小100m,Eden80m,s1及s2均为10m ,MaxTenuringThreshold=1对象年龄大于1则进入老年代
* @author ljl
*/
public class TestEdenToAnther {
private static int _1MB = 1 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
byte[] a = new byte[4 * _1MB];
byte[] b = new byte[40 * _1MB];
byte[] c = new byte[41 * _1MB];
c = null;
c = new byte[41 * _1MB];
}
}
初始状态,Eden区被占用17.601M:
执行byte[] a = new byte[4 * _1MB]; byte[] b = new byte[40 * _1MB];a,b被分配到Eden:
执行byte[] c = new byte[41 * _1MB];触发MinorGC,a被复制到Survivor,且a对象的age=1,b到老年代,c得以分配到Eden:
执行c = null; c = new byte[41 * _1MB];触发MinorGC,c原引用对象被完全回收,新引用对象(41M)分配到Eden,a对象age=2>1,被转移到老年代,至此,Eden存放了c(41M)对象,Survivor被清空,Old Gen存放了a(4M),b(40M)对象:
3.对象动态年龄判断
/**
* 测试对象动态年龄判断
* -Xms200m -Xmx200m -Xmn100m -XX:+PrintGCDetails -XX:MaxTenuringThreshold=14 -XX:+PrintTenuringDistribution -XX:+UseParNewGC
* 堆总大小200m,新生代总大小100m,Eden:S1:S2用的默认比例,8:1:1,Eden区80m,S1,S2各10m,配置对象年龄大于14则进入老年代
*
* @author ljl
*/
public class TestEdenToAnther {
private static int _1MB = 1 * 1024 * 1024;
public static void main(String[] args) throws InterruptedException {
byte[] a = new byte[2 * _1MB];
byte[] a_temp01 = new byte[3 * _1MB];
byte[] a_temp02 = new byte[1 * _1MB];
byte[] b = new byte[40 * _1MB];
byte[] c = new byte[41 * _1MB];
c = null;
c = new byte[41 * _1MB];
}
}
初始化状态,Eden区被占用17.601M:
执行 byte[] a = new byte[2 * _1MB]; byte[] a_temp01 = new byte[3 * _1MB];byte[] a_temp02 = new byte[1 * _1MB];byte[] b = new byte[40 * _1MB];
执行byte[] c = new byte[41 * _1MB];触发MinorGC,a,a_temp01,a_temp02被复制到Survivor,且三个对象的age均为1,b到Old Gen,c到Eden:
执行c = null; c = new byte[41 * _1MB];触发MinorGC,a,a_temp01,a_temp02三个对象的age均加1,变为2,且三个对象大小之和等于6M,大于Survivor的一半,遂被从Survivor1转移到Old Gen,c新对象分配到Eden,至此,Eden存放了c(41M)对象,Survivor被清空,Old Gen存放了a,a_temp01,a_temp02,b四个对象:
4.担保机制
这里再复习担保的具体做法:JVM在MinorGC前会先检查新生代对象总大小是否小于老年代剩余空间,小于则只MinorGC,大于则检查是否允许担保失败(HandlePromotionFailure是否为true),如果允许,则继续检查老年代剩余空间是否大于之前每次晋升到老年代的对象平均大小,大于则只进行MinorGC,小于则进行Full GC;如果不允许,则直接进行Full GC.另外,参数HandlePromotionFailure在6.0_24 及其之后的版本被移除掉了,也就是说,后面的版本参数HandlePromotionFailure默认为true,且不接受配置。笔者的JDK版本为1.7.0_79,后面也会测试下,是否可修改HandlePromotionFailure为false.
担保情景一:晋升到老年代的对象平均大小大于老年代剩余空间
/**
* 测试动态年龄判断
* -Xms100m -Xmx100m -Xmn50m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=30m -XX:+UseParNewGC -XX:+PrintFlagsFinal
* 堆总大小100m,新生代总大小50m,Eden:S1:S2用的默认比例,8:1:1,Eden区40m,S1,S2各5m,避免大对象直接进老年代的干扰,把PretenureSizeThreshold配成30m
* @author ljl
*/
public class TestEdenToAnther {
private static int _1MB = 1*1024*1024;
public static void main(String[] args) throws InterruptedException {
byte[] a = new byte[10 * _1MB];
byte[] b = new byte[10 * _1MB];
byte[] c = new byte[15 * _1MB];
byte[] d = new byte[10 * _1MB];
byte[] e = new byte[20 * _1MB];
byte[] f = new byte[10 * _1MB];
e = null;
f = null;
byte[] g = new byte[20 * _1MB];
}
}
执行 byte[] a = new byte[10 * _1MB]; byte[] b = new byte[10 * _1MB];byte[] c = new byte[15 * _1MB];创建c对象的时候触发MinorGC,a,b对象到老年代,共20M,c(15M)到Eden:
执行 byte[] d = new byte[10 * _1MB]; byte[] e = new byte[20 * _1MB];创建e对象的时候再次触发MinorGC,c,d到老年代,共25M,e(20M)到Eden:
执行 byte[] f = new byte[10 * _1MB];e = null; f = null; byte[] g = new byte[20 * _1MB];创建g对象的时候,当前新生代中对象有e和f,共30M,大于老年代剩余空间5M,进而检查是否允许担保失败,默认为允许,继续检查之前2次晋升到老年代的对象平均值为(20M+25M)/2=22.5M>5M ,将MinorGC改为进行一次Full GC,下图中可见Old Gen次数为1,进行了一次Full GC:
担保情景二:晋升到老年代的对象平均大小小于老年代剩余空间
/**
* 测试动态年龄判断
* -Xms100m -Xmx100m -Xmn50m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HandlePromotionFailure=false -XX:PretenureSizeThreshold=30m -XX:+UseParNewGC -XX:+PrintFlagsFinal
* 堆总大小100m,新生代总大小50m,Eden:S1:S2用的默认比例,8:1:1,Eden区40m,S1,S2各5m,避免大对象直接进老年代的干扰,把PretenureSizeThreshold配成30m
* @author ljl
*/
public class TestEdenToAnther {
private static int _1MB = 1*1024*1024;
public static void main(String[] args) throws InterruptedException {
byte[] a = new byte[5 * _1MB];
byte[] b = new byte[5 * _1MB];
byte[] a_Survivor = new byte[2 * _1MB];//这里要特别注意对象大小,不要下一次MinorGC被动态年龄判断给回收到老年代去了
byte[] c = new byte[25 * _1MB];
c = null;
byte[] d = new byte[10 * _1MB];
byte[] e = new byte[20 * _1MB];
byte[] f = new byte[9 * _1MB];
e = null;
f = null;
byte[] g = new byte[20 * _1MB];
}
}
debug并结合jvisualvm(此处不给出截图了):
初始化状态,Eden被占用11.202M:
执行 byte[] a = new byte[5 * _1MB]; byte[] b = new byte[5 * _1MB]; byte[] a_Survivor = new byte[3 * _1MB]; byte[] c = new byte[25 * _1MB];创建c对象的时候触发MinorGC ,
a,b到老年代,共10m,a_Survivor到s区,c(25m)存Eden:
执行 c = null; byte[] d = new byte[10 * _1MB];byte[] e = new byte[20 * _1MB];创建e对象的时候触发MinorGC,d到老年代,共10m,e(20M)存Eden:
执行 byte[] f = new byte[9 * _1MB]; e = null; f = null;byte[] g = new byte[20 * _1MB];创建g对象的时候,当前年轻代对象有a_Survivor,e,f,总大小31M,大于老年代剩余空间30M,进而检查是否允许担保失败,默认为允许,继续检查之前2次晋升到老年代的对象平均值为(10M+10M)/2=10M<30M ,遂只进行MinorGC:
在担保情景二下:晋升到老年代的对象平均大小小于老年代剩余空间,设置参数HandlePromotionFailure=false,上面有说到因该参数在6.0_24 版本后被移除掉了,不接受配置,笔者的JDK版本为1.7.0_79,理想测试结果为执行到为g分配空间时,仍然是触发MinorGC,而不是FullGC,代码就用情景二的代码,测试结果如下:
控制台会输出一下信息,提示你该参数配置是无效的:
打印的GC日志显示只进行了MinorGC,因此,该参数确实是不接受配置的:
以上,便是JVM的整个内存分配及回收策略,笔者只是把教科书上的描述加以实验,这样自己也能加深印象,另外,在测试大对象直接分配到老年代的时候,遇到一个比较奇怪的问题,我提了个问题帖:http://bbs.csdn.net/topics/392176004,尚没有解决,欢迎赐教或者讨论。