JVM理论:(二/1)内存分配策略

Posted zjxiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM理论:(二/1)内存分配策略相关的知识,希望对你有一定的参考价值。

  Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决两个问题:给对象分配内存以及回收分配给对象的内存。

对象的分配可能有以下几种方式:

1、JIT编译后被拆散为标量类型并间接地栈上分配

2、对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配

3、少数情况下也会直接分配在老年代 

参考下图:

  技术分享图片

5种内存分配策略

1、对象优先在Eden分配

  大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

  先来看看两种GC类型的定义

  新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。

  老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(Parallel Scavenge里可设置直接进行Major GC,所以并非绝对),Major GC的速度一般会比Minor GC慢10倍以上。 

经典案例代码:
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[4 * _1MB]; // 出现一次Minor GC } 运行结果: [GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

  先看看设置的虚拟机参数,-Xms20M -Xmx20M -Xmn10M这3个参数限制了Java堆大小为20MB,不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1,所以Eden有8MB的空间,一个Survivor区有1MB的空间,-XX:+PrintGCDetails表示虚拟机会在垃圾回收时打印内存回收日志。

  testAllocation()方法中尝试分配3个2MB大小和1个4MB大小的对象,在执行分配allocation4对象的语句时,会发现Eden区已经被占用了6MB,剩余2M空间,已不足以分配allocation4所需的4MB内存,因此会发生一次Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入只有1MB大小的Survivor空间,所以根据分配担保机制会提前转移到老年代去。

  根据打印的GC日志,6651K->148K(9216K), 6651K->6292K(19456K)也可以看出,新生代从6651K变为148K,但总内存占用量几乎没有减少,也证实allocation1、allocation2、allocation3三个对象都是存活的,只是被转移到了老年代,虚拟机几乎没有找到可回收的对象。这次GC后,程序执行完的结果是,Eden被allocation4占用4MB,Survivor空闲,老年代被allocation1、allocation2、allocation3三个对象占用6MB。

  所以,对象在新生代Eden区中分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。

*//堆空间分布日志
Heap  
  def new generation   total 9216K, used 4326K    //年轻代分布   
  eden space 8192K,  51% used ≈ 4MB
  from space 1024K,  14% used ≈ 148KB,应该是之前的垃圾,忽略  
  to space 1024K,   0% used
  tenured generation   total 10240K, used 6144K    //老年代分布  
  the space 10240K,  60% used ≈ 2048KB*3
  compacting perm gen  total 12288K, used 2114K    //永久代(方法区)分布,本例不考虑  
  the space 12288K,  17% used

  再从上例中的内存对分布的角度来重新推测过程:当分配到allocation4时,发现eden空间不足,这时进行GC,但由于Survivor只有1MB,存不下allocation1、allocation2、allocation3中的任意对象,所以这三个对象都直接进入老年代,最后allocation4分配在Eden,占4MB。

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

  大对象是指需要大量连续内存空间的Java对象,典型大对象有长字符串和数组,大对象对虚拟机的内存分配来说是一个坏消息,写程序时还要避免创建一些朝生夕死的短命大对象,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来安置他们。

  虚拟机提供-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配,这样避免再Eden及两个Survivor区间发生大量的内存复制,这个参数只对Serial和ParNew两款收集器有效,如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的组合。

3、根据对象年龄判定进入老年代

   虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中,可以通过-XX:MaxTenuringThreshold参数来设置年龄阈值。

代码示例:
private static final int _1MB = 1024 * 1024;
/**
 * VM参数: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
 * -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
 * -XX:+PrintTenuringDistribution
 */
@SuppressWarnings("unused")
public static void testTenuringThreshold() {
    byte[] allocation1, allocation2, allocation3;
    allocation1 = new byte[_1MB / 4];
    //什么时候进入老年代决定于XX:MaxTenuringThreshold设置
    allocation2 = new byte[4 * _1MB];
    allocation3 = new byte[4 * _1MB];
    allocation3 = null;
    allocation3 = new byte[4 * _1MB];
}
堆空间分布日志
以MaxTenuringThreshold=1参数来运行的结果:
Heap  
def new generation   total 9216K, used 4178K   //年轻代分布
eden space 8192K,  51% used ≈ 4MB 
from space 1024K,   0% used  
to   space 1024K,   0% used    
tenured generation   total 10240K, used 4500K  //老年代分布
the space 10240K,  43% used ≈ 4MB+256KB
compacting perm gen  total 12288K, used 2114K  //永久代分布先忽略
the space 12288K,  17% used
 以MaxTenuringThreshold=1的情况来分析,当要分配allocation3时,Eden空间不足,准备开始GC,照理Survivor(1MB)是能容纳下allocation1(256KB)的,但因为MaxTenuringThreshold=1,allocation1对象在第二次GC发生时进入老年代,所以Survivor区没有对象,老年代存放了
allocation1和allocation2,eden中存的是allocation3。
以MaxTenuringThreshold=15参数来运行的结果:
Heap  
def new generation   total 9216K, used 4582K   //年轻代分布
eden space 8192K,  51% used ≈ 4MB 
from space 1024K,  39% used ≈ 256KB 
to   space 1024K,   0% used  
tenured generation   total 10240K, used 4096K  //老年代分布
the space 10240K,  40% used = 4MB
 以MaxTenuringThreshold=15的情况来分析,当要分配allocation3时,Eden空间不足,准备开始GC,这次MaxTenuringThreshold=15,所以allocation1不会直接进入老年代,会到Survivor区域内,Survivor存不了allocation2,所以allocation2被移动到老年代,最后在Eden分配allocation3。

4、Survivor空间中相同年龄的对象过半直接进入老年代

   上一点讲到MaxTenuringThreshold参数,但虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

private static final int _1MB = 1024 * 1024;  
/**  
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 
 * -XX:+PrintTenuringDistribution  
 */  
@SuppressWarnings("unused")  
public static void testTenuringThreshold2() { 
  byte[] allocation1, allocation2, allocation3, allocation4;  
  allocation1 = new byte[_1MB / 4];   
  // allocation1+allocation2大于survivo空间一半  
  allocation2 = new byte[_1MB / 4];    
  allocation3 = new byte[4 * _1MB];  
  allocation4 = new byte[4 * _1MB];  
  allocation4 = null;  
  allocation4 = new byte[4 * _1MB];  
}

Heap  
def new generation   total 9216K, used 4178K      //年轻代
eden space 8192K,  51% used ≈ 4MB 
from space 1024K,   0% used  
to   space 1024K,   0% used  
tenured generation   total 10240K, used 4756K     //老年代 
the space 10240K,  46% used ≈ 4MB+256KB*2

  根据堆内存分布日志分析,当要分配allocation4时,发现Eden空间不够进行GC,由于-XX:MaxTenuringThreshold设置为15,且Survivor区是可以容纳下allocation1和allocation2的,照理说这两个对象应该是要进入Survivor区的,但这两个对象对没有进入Survivor而是直接进入了老年代,这就是因为allocation1和allocation2两对象相加为512KB,达到了Survivor的一半,且它们同年龄,所以会直接进入老年代。Survivor区不够存allocation3也直接进入老年代,最后在Eden上分配allocation4。 

5、空间分配担保

  关于分配担保机制JDK6前后有一点差别。  

  JDK6之前,在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。

  如果检查老年代最大可用的连续空间大于新生代所有对象总空间,那么Minor GC可以确保是安全的。

  如果检查不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。

      如果允许担保失败,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,这里取平均值是因为在实际完成内存回收前无法明确知道有多少对象会存活下来,所以也存在一定风险。

      如果大于历次平均大小,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,Minor GC若执行失败也会进行执行一次Full GC,这样的失败绕的圈子是最大的;

      如果小于历次平均大小,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

  对以上的步骤归纳一下,先看老年代的可用空间能否容下新生代的所有对象,不能的话看是否开启了分配担保机制,允许就先执行Minor GC,否则直接进行Full GC。大部分情况下还是会将HandlePromotionFailure开启分配担保,避免频繁Full GC。

  JDK6后,HandlePromotionFailure不再影响到虚拟机的空间分配担保策略,变为只要老年代的连续空间大于新生代对象总大小或历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

 

参考链接:

  https://www.jianshu.com/p/fa3569127416

  https://segmentfault.com/a/1190000004606059



以上是关于JVM理论:(二/1)内存分配策略的主要内容,如果未能解决你的问题,请参考以下文章

深入理解JVM读书笔记二: 垃圾收集器与内存分配策略

深入了解JVM——垃圾收集器与内存分配策略

深入理解JVM内存分配策略

JVM性能调优 第七章 内存分配策略

JVM之GC日志分析与对象内存分配回收策略

Java千百问_07JVM架构(003)_内存分配有哪些策略