JVM 对象分配规则

对象的内存分配,从大方向上将,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地在栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况也可能直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

 
打印内存情况代码:
for (MemoryPoolMXBean memoryPoolMXBean : ManagementFactory.getMemoryPoolMXBeans()) {
    System.out.println(
        memoryPoolMXBean.getName() 
        + "\t总量:" + (memoryPoolMXBean.getUsage().getCommitted()/1024 + "K")
        + "\t使用的内存:" + (memoryPoolMXBean.getUsage().getUsed()/1024)  + "K");
}

  

 
 
一、对象优先分配在Eden区
 
测试代码:
public class EdenTest {
    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[4 * _1MB];// 出现一次Minor GC
    }
} 

JVM参数:

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-XX:+PrintGCDetails
参数说明:
-Xms20M、-Xmx20M和-Xmn10M这3个参数限制Java堆大小为20M,且不可扩展,其中10MB分配给新生代,剩下的10M就分配给老年代了。-XX:SurvivorRatio=8决定了新生代中Eden和Survivor区的空间比例为8:1
输出结果:
上述PSYoungGen total 9216K=9M,为什么?因为有一块Survivor区必须保证为空的。
 
 
二、大对象保存到老年代(优先保存在Eden区)
 
所谓大对象,就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组(byte[]数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(更加坏的情况就是遇到一群朝生夕死的短命大对象,写程序时应该避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置大对象。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接进入老年代中分配。这样避免在Eden区及两个Survivor区之间发生大量的内存拷贝。
注意:实际测试中,加PretenureSizeThreshold这个参数并未启到作用。所谓大的对象并不能说比PretenureSizeThreshold设置的参数大的称为大对象,大的对象就是指很大的字符串,或者大的数组。
 
大对象进入老年代遵循以下原则:
(1)新生代内存空间充足时,保存在新生代,遵循对象优先分配原则,保存在Eden区;
(2)新生代内存空间不足时,直接进入到老年代;
(3)当进行第一次GC时,无论内存是否充足,存活的大对象直接全部进入老年代;
为什么要优先保存在Eden?
试想一下,如果这个大的对象是朝生夕死的话,如果新生代足够充足的话,完全没有必要进入到老年代。
 
测试代码:
public static void main(String[] args) {
    final int _1MB = 1024 * 1024;
    byte[] b1 = new byte[4 * _1MB];
    byte[] b2 = new byte[5 * _1MB];  
    // System.gc();
}

JVM参数:同上

输出结果:
Heap
 PSYoungGen      total 9216K, used 4932K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 60% used [0x00000000ff600000,0x00000000ffad1028,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 5120K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 50% used [0x00000000fec00000,0x00000000ff100010,0x00000000ff600000)
 PSPermGen       total 21504K, used 2505K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
  object space 21504K, 11% used [0x00000000f9a00000,0x00000000f9c72658,0x00000000faf00000)

上例中,新生代10M(Eden区8M)

当执行 b1 = new byte[4 * _1MB]; Eden区内存空间充足,直接保存在Eden区,此时Eden区还剩余4M空间;
当执行 b2 = new byte[5 * _1MB]; Eden区空间不足,所以直接保存到老年代。
 
 
 
三、长期存活的对象将进入老年代
这里使用对象到达一定的年龄直接进入老年代更准确,对象默认年龄为15;我们可以通过修改参数-XX:MaxTenuringThreshold来设置。
 
JVM参数:
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails 
-XX:MaxTenuringThreshold=5
由于大对象在进行第一次GC的时候,会进入到老年代,所以这里不能使用大的数组来测试。因此也不好直接验证

 

示例:

public class GCDemo {
    public static void main(String[] args) {
    	System.out.println("测试GC");
//        System.gc();
//        System.gc();
//        System.gc();
//        System.gc();
//        System.gc();
//        System.gc();
    }
}

执行上述代码,在没有进行GC时老年代空间使用率为 0%

在进行1~4次GC的时候,老年代空间使用率为 4%
在进行5次以上GC的时候,老年代空间使用率为 4%
 
 
 
4. 动态对象年龄判定
如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代
为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄所有对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
 
 
五、空间分配担保
当发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。
 
新生代使用复制收集算法,但为了内存利用率,值使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时(最极端就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下去,在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验,与老年代的剩余空间进行对比,决定是否进行Full GC来让老年代腾出更多空间。
 
取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次Minor GC存活后的对象突增,远远高于平均值时,依然会导致担保失败(Handle Promotion Failure)。如果出现HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
 
 
 
 
 
 
 
posted @ 2020-06-16 11:26  cao_xiaobo  阅读(900)  评论(0编辑  收藏  举报