概述

对象都是在堆上分配,但实际上也有可能经过即时编译后被拆散为标量类型并间接地在栈上分配

分代设计下,新生对象通常会分配在新生代中,少数情况下(例如对象大小超过一定阈值)也可能会直接分配在老年代

《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

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐