Java高级——内存分配机制
对象都是在堆上分配,但实际上也有可能经过即时编译后被拆散为标量类型并间接地在栈上分配分代设计下,新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代《Java虚拟机规范》并未规定新对象的创建和存储细节,取决于垃圾收集器及参数的设定以下如无特别说明,均使用HotSpot虚拟机的Serial+Serial Old,JDK8环境。
概述
对象都是在堆上分配,但实际上也有可能经过即时编译后被拆散为标量类型并间接地在栈上分配
分代设计下,新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代
《Java虚拟机规范》并未规定新对象的创建和存储细节,取决于垃圾收集器及参数的设定
以下如无特别说明,均使用HotSpot虚拟机的Serial+Serial Old,JDK8环境
对象优先在Eden分配
设置堆20M,新生代Eden 8M,2个Survivor 1M,老年代10M
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-XX:+PrintGCDetails -XX:+UseSerialGC
如下程序分配三个2M,一个3M对象
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation4 = new byte[3 * _1MB];
}
}
分配allocation4时,Eden已用了6MB,剩余空间不够分配allocation4的3MB发生一次GC
如上图,三个2MB对象无法放入Survivor(只有1M),只能将其放到老年代,并在eden分配allocation4的3MB
大对象直接进入老年代
大对象指需要大量连续内存空间的Java对象,如很长的字符串、元素数量多的数组
需避免大对象,因为会导致内存还有空间就提前触发gc,且复制时占用大量内存
-XX:PretenureSizeThreshold 可设置大于该值的对象直接在老年代分配,避免其在Eden和Survivor来回复制占用内存,其只能对Serial和ParNew有效
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
-XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC
如上设置为3MB,如下分配一个4MB数组
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation;
allocation = new byte[4 * _1MB];
}
}
对象会直接分配到老年代
长期存活的对象将进入老年代
虚拟机为每个对象定义了一个年龄计数器,存储在对象头
Eden的对象经过一次Minor GC后仍存活并能被Survivor容纳的话,就会被移动到Survivor
对象在Survivor中每熬过一次Minor GC,年龄就增加1岁,当它的年龄达到15(-XX:MaxTenuringThreshold),就会被晋升到老年代中
-XX:TargetSurvivorRatio设置超过多少Survivor比例,会重新计算Threshold,为避免其影响设置的高一点为80
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:+UseSerialGC -XX:TargetSurvivorRatio=80
如下allocation1需要256KB,Survivor可以容纳
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
allocation1 = new byte[_1MB / 4];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB]; //第一次GC
allocation3 = null;
allocation3 = new byte[4 * _1MB]; //第二次GC
}
}
当-XX:MaxTenuringThreshold=1时
- allocation2在第一次GC直接进入老年代(Survivor放不下),即5337K->786K
- allocation1先进入Survivor然后在第二次GC进入老年代,新生代被收集变为0
- 截图4178为allocation3+其他,4881为allocation1+allocation2+其他
当-XX:MaxTenuringThreshold=15时,可看到第二次GC后,allocation1仍留在新生代的Survivor
动态对象年龄判定
并非年龄达到-XX:MaxTenuringThreshold才能晋升老年代
若Survivor中相同年龄所有对象大小的总和大于Survivor的一半(-XX:TargetSurvivorRatio),年龄大于或等于该年龄的对象就可直接进入老年代
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-XX:+PrintGCDetails -XX:+PrintTenuringDistribution -XX:+UseSerialGC -XX:MaxTenuringThreshold=15
如下设置晋升年龄为15
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[_1MB / 4];
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB]; //第一次GC
allocation4 = null;
allocation4 = new byte[4 * _1MB]; //第二次GC
}
}
可看到第一次GC后还有1023K,allocation1 和 allocation2 进入Survivor,allocation3 进入老年代
当第二次GC时,allocation1 和 allocation2 加起来为0.5M大于Survivor的一半,直接进入老年代,新生代清0
当设置-XX:TargetSurvivorRatio = 100,可看到仍进入Survivor,第二次GC时,allocation1 和 allocation2 仍在Survivor
空间分配担保
在Minor GC之前
- 检查老年代最大可用连续空间是否大于新生代所有对象总空间
- 若不成立,查看-XX:HandlePromotionFailure是否允许分配担保
- 若允许,检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,若大于则进行一次有风险的Minor GC
- 若小于或-XX:HandlePromotionFailure设置不允许或担保失败则Full GC
分配担保指的是当出现大量对象在Minor GC后存活,把Survivor无法容纳的对象直接送入老年代
-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-XX:+PrintGCDetails -XX:+UseSerialGC
在JDK6u23运行代码
public class Test {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3,allocation4,allocation5,allocation6,allocation7;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation1 = null;
allocation4 = new byte[2 * _1MB]; //第一次gc
allocation5 = new byte[2 * _1MB];
allocation6 = new byte[2 * _1MB];
allocation4 = null;
allocation5 = null;
allocation6 = null;
allocation7 = new byte[2 * _1MB]; //第二次gc
}
}
当-XX:+HandlePromotionFailure=false时
- 第一次gc,allocation 1回收,23直接进入老年代(6487K->159K表示新生代暂无数据,6487K->4255K表示整堆还有23),4分配新生代
- 第二次gc,allocation 456想进入老年代,老年代空间空间不够且不允许担保,Full GC回收,回收456,7分配新生代
- 故最后2211K表示7在新生代,4255K表示23仍在老年代
当-XX:+HandlePromotionFailure=true时,老年代空间空间不够但允许担保,触发Mirror GC回收(需注意是从10485K->4255K)
在JDK 6 Update 24之后,该参数无效,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就Minor GC否则Full GC
更多推荐
所有评论(0)