JVM内存分配

作者: Sid05Wang | 来源:发表于2018-03-24 13:01 被阅读0次

    对象的内存分配,大致上讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程缓冲,将按线程优先在TLAB(Thread-local allocation buffer)上分配。少数情况下也可能直接分配到老年代中,分配的规则并非固定,取决于使用的是哪一种垃圾收集器组合,还有内存相关的一些参数设置。
    接下来是几条最普遍的内存分配规则,并通过代码去验证。验证的是在使用Serial/Serial Old(串行)收集器下的内存分配和回收。其他收集器组合不尽相同。

    1、对象优先在Eden区分配

    一般的情况下,对象在新生代的Eden去分配。当Eden区没有足够空间进行分配时,虚拟机将进行一次Minor GC。-XX:PrintGCDetails这个收集器日志参数,通知虚拟机在发生垃圾收集行为时打印内存日志,并且在进程退出时输出当前内存各区域分配情况。
    代码实例1,testAllocation()运行时通过参数-Xms20M、-Xmx20M、-Xmn10M限制Java堆大小为20M,其中10M分配给新生代,剩余的10M给老年代。-XX:SurvivorRatio=8设置新生代中Eden区与一个Survivor的空间比例是8:1,因此新生代总可用空间为9M(Eden区的容量+1个Survivor的容量)。
    testAllocation()尝试分配3个2M大小和1个4M大小的对象,再执行到分配allocation4对象的语句时会发生一次Minor GC。引起的原因是给allocation4分配内存时,Eden去已经被占用了6M,剩余空间已不足以分配allocation4所需的4M内存,因此发生Minor GC。这次GC的结果是新生代6635K变为177K,而Java堆的内存总占用量几乎没有减少,因为allocation1、allocation2、allocation3都是存活的,虚拟机几乎没有可回收的对象。GC期间虚拟机又发现allocation1、allocation2、allocation3 3个2M大小的对象无法放入Survivor空间(Survivor空间大小只有1M),所以只能通过分配担保机制提前转移到老年代。
    这次Minor GC结束后,allocation4对象顺利的分配在Eden区中,因此程序执行完内存各区域的分配情况是,Eden区占用4MB(被allocation4占用),Survivor区空闲,老年代占用6MB(被allocation1、allocation2、allocation3占用)。

    代码实例1


    private static final int _1MB=1024*1024;
    
    /*VM参数
    -verbose:gc
    -Xms20M
    -Xmx20M
    -Xmn10M
    -XX:+PrintGCDetails
    -XX:SurvivorRatio=8
    */
    public static void testAllocation(){
        byte[] allocation1,allocation2,allocation3,allocation4;
        allocation1=new byte[2*_1MB];
        allocation2=new byte[2*_1MB];
        allocation3=new byte[2*_1MB];
        allocation4=new byte[2*_1MB]; //出现一次 Minor GC
    }
    

    运行结果:


     [GC [DefNew: 6635K->177K(9216K), 0.0075871 secs] 6635K-
     >6321K(19456K), 0.0076708 secs] [Times: user=0.00 sys=0.02, 
     real=0.01 secs]
     Heap
     def new generation   total 9216K, used 2553K [0x26de0000, 
     0x277e0000, 0x277e0000)
     eden space 8192K,  29% used [0x26de0000, 0x27031f50, 
     0x275e0000)
     from space 1024K,  17% used [0x276e0000, 0x2770c728, 
     0x277e0000)
     to   space 1024K,   0% used [0x275e0000, 0x275e0000, 
     0x276e0000)
     tenured generation   total 10240K, used 6144K [0x277e0000, 
     0x281e0000, 0x281e0000)
     the space 10240K,  60% used [0x277e0000, 0x27de0030, 
     0x27de0200, 0x281e0000)
     compacting perm gen  total 12288K, used 386K [0x281e0000, 
     0x28de0000, 0x2c1e0000)
     the space 12288K,   3% used [0x281e0000, 0x282409e8, 
     0x28240a00, 0x28de0000)
     ro space 8192K,  67% used [0x2c1e0000, 0x2c749040, 
     0x2c749200, 0x2c9e0000)
     rw space 12288K,  54% used [0x2c9e0000, 0x2d05d348, 
     0x2d05d400, 0x2d5e0000)
    

    2、大对象直接进入老年代

    大对象是指需要大量连续内存空间的Java对象。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。
    通过设置-XX:PretenureSizeThreshold参数,令大于这个阀值的对象直接在老年代分配。这样做的目的是避免在Eden区以及两个Surivivor区之间发生大量的内存复制。
    代码实例2中testPretenureSizeThreashold()方法执行后,Eden区几乎没有被使用,而老年代的10M空间被占去40%(4M的allocation对象直接分配在老年代中)。这是因为-XX:PretenureSizeThreshold设置为3M(就是3145728,此参数不能像-Xmx之类的参数那样直接写3M),因此超过3M的对象都会直接在老年代分配。

    代码实例2


    private static final int _1MB=1024*1024;
    
    /*VM参数
    -verbose:gc
    -Xms20M
    -Xmx20M
    -Xmn10M
    -XX:+PrintGCDetails
    -XX:SurvivorRatio=8
    -XX:PretenureSizeThreshold=3145728
    */
    public static void testPretenureSizeThreashold() {
        byte[] allocation;
        allocation=new byte[4*_1MB];
    }
    

    运行结果:


     Heap
     def new generation   total 9216K, used 819K [0x26de0000, 
     0x277e0000, 0x277e0000)
     eden space 8192K,  10% used [0x26de0000, 0x26eace40,      
     0x275e0000)
     from space 1024K,   0% used [0x275e0000, 0x275e0000, 
     0x276e0000)
     to   space 1024K,   0% used [0x276e0000, 0x276e0000, 
     0x277e0000)
     tenured generation   total 10240K, used 4096K [0x277e0000, 
     0x281e0000, 0x281e0000)
     the space 10240K,  40% used [0x277e0000, 0x27be0010, 
     0x27be0200, 0x281e0000)
     compacting perm gen  total 12288K, used 385K [0x281e0000, 
     0x28de0000, 0x2c1e0000)
     the space 12288K,   3% used [0x281e0000, 0x28240608, 
     0x28240800, 0x28de0000)
     ro space 8192K,  67% used [0x2c1e0000, 0x2c749040, 
     0x2c749200, 0x2c9e0000)
     rw space 12288K,  54% used [0x2c9e0000, 0x2d05d348, 
     0x2d05d400, 0x2d5e0000)
    

    3、长期存活的对象进入老年代

    虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden区,并经过第一次Minor GC后仍然存活且能被Survivor容纳的话,将被移到Survivor区,对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1,当年龄增加到一定程度(默认为15),就将会晋升到老年代中。通过-XX:MaxTenuringThreshold参数设置对象晋升老年代的年龄阀值。
    代码实例3,-XX:MaxTenuringThreshold设置为1来执行testMaxTenuringThreashold()方法。方法中的allocation1对象需要256K内存,Survivor空间可以容纳,在第一次Minor GC后,放入Survivor空间。在第二次Minor GC后allocation1进入老年代,新生代已使用的内存变成0K。

    代码实例3


    private static final int _1MB=1024*1024;
    
    /*VM参数
    -verbose:gc
    -Xms20M
    -Xmx20M
    -Xmn10M
    -XX:+PrintGCDetails
    -XX:SurvivorRatio=8
    -XX:MaxTenuringThreshold=1
    -XX:+PrintTenuringDistribution
    */
    public static void testMaxTenuringThreashold() {
        byte[] allocation1,allocation2,allocation3;
        allocation1=new byte[_1MB/4];//什么时候进入老年代取决于-XX:MaxTenuringThreshold设置
        allocation2=new byte[4*_1MB];
        allocation3=new byte[4*_1MB];//出现第一次Minor GC
        allocation3=null;
        allocation3=new byte[4*_1MB];//出现第二次Minor GC
    }
    

    运行结果:


     [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 1 (max 1)
      - age   1:     445288 bytes,     445288 total
      : 5007K->434K(9216K), 0.0071760 secs] 5007K-     
      >4530K(19456K), 0.0072607 secs] [Times: user=0.02 sys=0.00,      
      real=0.01 secs]
     [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 1 (max 1)
      : 4615K->0K(9216K), 0.0014664 secs] 8711K->4530K(19456K), 
      0.0015449 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Heap
      def new generation   total 9216K, used 4316K [0x26de0000, 
      0x277e0000, 0x277e0000)
      eden space 8192K,  52% used [0x26de0000, 0x272170e8, 
      0x275e0000)
      from space 1024K,   0% used [0x275e0000, 0x275e0000, 
      0x276e0000)
      to   space 1024K,   0% used [0x276e0000, 0x276e0000, 
      0x277e0000)
      tenured generation   total 10240K, used 4530K [0x277e0000, 
      0x281e0000, 0x281e0000)
      the space 10240K,  44% used [0x277e0000, 0x27c4ca60, 
      0x27c4cc00, 0x281e0000)
      compacting perm gen  total 12288K, used 389K [0x281e0000, 
      0x28de0000, 0x2c1e0000)
      the space 12288K,   3% used [0x281e0000, 0x28241668, 
      0x28241800, 0x28de0000)
      ro space 8192K,  67% used [0x2c1e0000, 0x2c749040, 
      0x2c749200, 0x2c9e0000)
      rw space 12288K,  54% used [0x2c9e0000, 0x2d05d348, 
      0x2d05d400, 0x2d5e0000)
    

    4、对象年龄动态判定

    虚拟机并不是永远地等到对象的年龄达到了-XX:MaxTenuringThreshold才能晋升老年代。如果在Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
    代码实例4中,设置-XX:MaxTenuringThreshold=15,执行testMaxTenuringThreashold()方法,会发现结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%,说明allocation1、allocation2对象进入了老年代,而没有等到15的临界年龄。因为这两个对象加起来已经超过了512K,并且是同年龄的,满足同年龄对象达到Survivor空间的一半,进入老年代的规则。

    代码实例4


    private static final int _1MB=1024*1024;
    
    /*VM参数
    -verbose:gc
    -Xms20M
    -Xmx20M
    -Xmn10M
    -XX:+PrintGCDetails
    -XX:SurvivorRatio=8
    -XX:MaxTenuringThreshold=15
    -XX:+PrintTenuringDistribution
    */
    public static void testMaxTenuringThreashold() {
        byte[] allocation1,allocation2,allocation3,allocation4;
        allocation1=new byte[_1MB/4];
        allocation2=new byte[_1MB/4];//allocation1+allocation2大于Survivor空间一半
        allocation3=new byte[4*_1MB];
        allocation4=new byte[4*_1MB];//出现第一次Minor GC
        allocation4=null;
        allocation4=new byte[4*_1MB];//出现第二次Minor GC
    }
    

    运行结果:


      [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 1 (max 15)
      - age   1:     706424 bytes,     706424 total
      : 5099K->689K(9216K), 0.0063450 secs] 5099K-               
      >4785K(19456K), 0.0064277 secs] [Times: user=0.02 sys=0.00,      
      real=0.01 secs]
      [GC [DefNew
      Desired survivor size 524288 bytes, new threshold 15 (max 15)
      - age   1:        264 bytes,        264 total
      : 4949K->0K(9216K), 0.0020751 secs] 9045K->4786K(19456K), 
      0.0021526 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Heap
      def new generation   total 9216K, used 4345K [0x26de0000, 
      0x277e0000, 0x277e0000)
      eden space 8192K,  53% used [0x26de0000, 0x2721e438, 
      0x275e0000)
      from space 1024K,   0% used [0x275e0000, 0x275e0108,         
      0x276e0000)
      to   space 1024K,   0% used [0x276e0000, 0x276e0000, 
      0x277e0000)
      tenured generation   total 10240K, used 4785K [0x277e0000, 
      0x281e0000, 0x281e0000)
      the space 10240K,  46% used [0x277e0000, 0x27c8c6f8, 
      0x27c8c800, 0x281e0000)
      compacting perm gen  total 12288K, used 389K [0x281e0000, 
      0x28de0000, 0x2c1e0000)
      the space 12288K,   3% used [0x281e0000, 0x28241698, 
      0x28241800, 0x28de0000)
      ro space 8192K,  67% used [0x2c1e0000, 0x2c749040, 
      0x2c749200, 0x2c9e0000)
      rw space 12288K,  54% used [0x2c9e0000, 0x2d05d348, 
      0x2d05d400, 0x2d5e0000)
    

    5、空间分配担保

    在每次Minor GC之前,虚拟机会先检查老年大最大可用连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许担保失败,那这是要改为进行一次Full GC。
    代码实例5,是在JDK 6 Update 24 之前的版本中运行测试的。之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升到老年代对象的平均大小就进行Minor GC,否则将进行Full GC。

    代码实例5


    private static final int _1MB=1024*1024;
    
    /*VM参数
    -verbose:gc
    -Xms20M
    -Xmx20M
    -Xmn10M
    -XX:+PrintGCDetails
    -XX:SurvivorRatio=8
    -XX:+HandlePromotionFailure或者-XX:-HandlePromotionFailure
    */
    public static void testHandlePromotion(){
        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];//出现一次Minor GC
        allocation5=new byte[2*_1MB];
        allocation6=new byte[2*_1MB];
        allocation4=null;
        allocation5=null;
        allocation6=null;
        allocation7=new byte[2*_1MB];//设置-XX:+HandlePromotionFailure出现第二次Minor GC 设置-XX:-HandlePromotionFailure 出现Full GC
    }
    

    运行结果:


    设置-XX:+HandlePromotionFailure

     [GC [DefNew: 6635K->177K(9216K), 0.0059513 secs] 6635K-          
     >4273K(19456K), 0.0060350 secs] [Times: user=0.00 sys=0.00, 
     real=0.01 secs]
     [GC [DefNew: 6573K->178K(9216K), 0.0014895 secs] 10669K-
     >4274K(19456K), 0.0015706 secs] [Times: user=0.00 sys=0.00, 
     real=0.00 secs]
    

    设置-XX:-HandlePromotionFailure

     [GC [DefNew: 6635K->177K(9216K), 0.0056952 secs] 6635K-
     >4273K(19456K), 0.0057779 secs] [Times: user=0.00 sys=0.00, 
     real=0.01 secs]
      [GC [DefNew: 6573K->6573K(9216K), 0.0000482 secs]
      [Tenured: 4096K->4274K(10240K), 0.0125304 secs] 10669K-
      >4274K(19456K), [Perm : 385K->385K(12288K)], 0.0127614 
      secs] [Times: user=0.00 sys=0.02, real=0.01 secs]
    

    参考
    [《深入理解Java虚拟机》]

    相关文章

      网友评论

        本文标题:JVM内存分配

        本文链接:https://www.haomeiwen.com/subject/jxfqcftx.html