jvm高级特性(4)(内存分配回收策略)

JVM高级特性与实践(四):内存分配 与 回收策略 

一. 内存分配 和 回收策略

1,对象内存分配的概念:

往大方向讲,它就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),

  • 对象主要分配在新生代的Eden区上,如果启动了局部线程分配缓冲,将按线程优先在TLAB上分配。
  • 少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,
  • 其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存有关的参数设置。

补充:线程TLAB局部缓存区域(Thread Local Allocation Buffer)

2,Java堆内存

3,两个重要的概念

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭(即生命周期特别短)的特征,所以MinorGC非常频繁,一般回收速度也比较快。
老年代GC(Major GC
/ Full GC):指发生在老年代的GC
出现了Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。
老年代GC 的速度一般比 新生代GC慢10倍以上。

4,代码实战设置环境条件

下面代码测试是在Client模式虚拟机运行,未特意指定收集器组合,也就是说验证的是 Serial/ Serial Old收集器

(ParNew / Serial Old收集器组合的规则也基本一致)下的内存分配和回收策略。

以下代码测试都将加上了以下参数:

-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

以上设置意味着将Java堆内存大小限制设置为20M,由于新生代和老年代各占一半,所以新生代占10M内存。

Eden区和Survivor区的比例是8,在新生代中由一块Eden区和两块大小相等的Survivor区组成,所以Eden区内存为8M,每个Survivor区大小为1M。

 

补充:

XX:NewRatio=4:表示年老代与年轻代的比值为4:1 
XX:SurvivorRatio的解释是:Eden区与Survivor区的大小比值,

二. 五大策略

1 对象优先在 Eden 分配

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

代码实践与日志展示

【新生代 Minor GC】
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: 6487K->152K(9216K), 0.0040116 secs] 6487K->6296K(19456K), 0.0040436 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
        Heap 
         def new generation   total 9216K, used 4576K [0x32750000, 0x33150000, 0x33150000) 
          eden space 8192K,  54% used [0x32750000, 0x32ba1fa8, 0x32f50000) 
          from space 1024K,  14% used [0x33050000, 0x33076150, 0x33150000) 
          to   space 1024K,   0% used [0x32f50000, 0x32f50000, 0x33050000) 
         tenured generation   total 10240K, used 6144K [0x33150000, 0x33b50000, 0x33b50000) 
           the space 10240K,  60% used [0x33150000, 0x33750030, 0x33750200, 0x33b50000) 
         compacting perm gen  total 12288K, used 376K [0x33b50000, 0x34750000, 0x37b50000) 
           the space 12288K,   3% used [0x33b50000, 0x33bae2c0, 0x33bae400, 0x34750000) 
            ro space 10240K,  55% used [0x37b50000, 0x380d1140, 0x380d1200, 0x38550000) 
            rw space 12288K,  55% used [0x38550000, 0x38bf44c8, 0x38bf4600, 0x39150000) 

补充:

PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,(即方法区)
read-only space,只读空间;read-write space 可读写空间
[GC [DefNew: 6487K->152K(9216K), 0.0040116 secs] 6487K->6296K(19456K), 0.0040436 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  

6487K->152K:新生代使用的内存,gc后从6487变为152 
(9216k) : 新生代总大小 (只是伊甸园区+1个生存区(只算from区,不算to区)) 6487K
->6296K:堆内存使用的内存,gc后变化不大;(对象从新生代到老年代)
(19456k) : 堆的大小(新生代+老年代)

结果分析:

  • testAllocation() 方法中,尝试分配3个2MB大小和1个4MB大小的对象。
  • 从输出结果中可以发现“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216KB(Eden区 + 1个 Survivor区的总容量)。
  • 执行testAllocation() 方法中的分配 allocation4 对象的语句会发生一次 Minor GC,这次GC的结果是新生代6651KB变为148KB,而总内存占用量则几乎没有减少,
  • 因为allocation1 、allocation2 、allocation3 三个对象都是存活的,虚拟机几乎没有找到可回收的对象。

这次GC发生的原因:

  1. 给allocation4 分配内存时,发现Eden区 已经被占用了6MB,剩余空间已不足以分配allocation4 所需的4MB内存,因此发生了 Minor GC。
  2. GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入 Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。
  3. 此次GC结束后,4MB的 allocation4 对象顺利分配在 Eden中,因此程序执行完后的结果是 Eden区占用4MB(被allocation4占用 ),
  4. Survivor空间处于空闲状态,老年代被占用6MB(被allocation1、allocation2、allocation3占用)。

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

大对象”是:需要大量连续内存空间的Java对象最典型的大对象就那种很长的字符串以及数组(例如如下代码中的byte[]数组)。

大对象对虚拟机的内存分配就是一个坏消息

  • (对Java虚拟机而言,比遇到一个大对象更坏的情况时遇到一群“朝生夕灭”的“短命大对象”,编写程序时应当避免此现象产生)
  • 经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集获取足够的连续空间来“安置”它们

2,1,测试环境设置:

大体的新生代与老年代内存大小设置同以上一样,只是这里多设置了一个限制:

  • 虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置的对象直接在老年代分配。
  • 目的是为了避免在Eden区及两个Survivor区之间发生大量的内存复制(注意:新生代采用复制算法)。

2,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 testPretenureSizeThreshold() {
    byte[] allocation;
    allocation = new byte[4 * _1MB];  //直接分配在老年代中
}

运行结果:

 Heap 
def new generation   total 9216K, used 507K [0x32750000, 0x33150000, 0x33150000) 
 eden space 8192K,   6% used [0x32750000, 0x327cef38, 0x32f50000) 
 from space 1024K,   0% used [0x32f50000, 0x32f50000, 0x33050000) 
 to   space 1024K,   0% used [0x33050000, 0x33050000, 0x33150000) 
tenured generation   total 10240K, used 4096K [0x33150000, 0x33b50000, 0x33b50000) 
  the space 10240K,  40% used [0x33150000, 0x33550010, 0x33550200, 0x33b50000) 
compacting perm gen  total 12288K, used 376K [0x33b50000, 0x34750000, 0x37b50000) 
  the space 12288K,   3% used [0x33b50000, 0x33bae3b8, 0x33bae400, 0x34750000) 
   ro space 10240K,  55% used [0x37b50000, 0x380d1140, 0x380d1200, 0x38550000) 
   rw space 12288K,  55% used [0x38550000, 0x38bf44c8, 0x38bf4600, 0x39150000) 

2,3,结果分析:

  • 执行完testPretenureSizeThreshold() 方法后,查看打印日志的“the space 10240K, 40% used”,可以发现Eden空间几乎没有被使用,
  • 而老年代的10MB空间被使用了40%,也就是4MB的allocation 对象直接被分配到老年代中,
  • 因为PretenureSizeThreshold被设置为3M(就是3145728,此参数不能像 -Xmx之类的参数那样写成3MB),因此超过3MB的对象都会直接在老年代进行分配。

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

3,1,对象年龄(Age)计数器

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别对象应放在新生代还是老年代。
为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。
  • 如果对象在Eden出生并经过第一次 Minor GC后仍然存活,并且能被Survivor 容纳的话,将被移动到 Survivor空间中,并且对象年龄设为1
  • 对象在Survivor 区 每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就会晋升到老年代中

3,2,测试环境设置

大体的新生代与老年代内存大小设置都是一样,这里多出现了一种参数:

  • -XX:MaxTenuringThreshold,可通过它来设置对象晋升老年代的年龄阀值

3,3,实践与日志展示

【长期存活的对象进入老年代】

private static final int _1MB = 1024 * 1024;

/**
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -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 参数来运行的结果:

[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 1 (max 1) 
- age   1:     418144 bytes,     418144 total 
: 4695K->408K(9216K), 0.0054252 secs] 4695K->4504K(19456K), 0.0054708 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]  
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 1 (max 1) 
- age   1:        136 bytes,        136 total 
: 4668K->0K(9216K), 0.0013601 secs] 8764K->4504K(19456K), 0.0013867 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
Heap 
 def new generation   total 9216K, used 4260K [0x32750000, 0x33150000, 0x33150000) 
  eden space 8192K,  52% used [0x32750000, 0x32b78fe0, 0x32f50000) 
  //☆☆☆
  from space 1024K,   0% used [0x32f50000, 0x32f50088, 0x33050000) 
  to   space 1024K,   0% used [0x33050000, 0x33050000, 0x33150000) 
 tenured generation   total 10240K, used 4504K [0x33150000, 0x33b50000, 0x33b50000) 
   //☆☆☆
   the space 10240K,  43% used [0x33150000, 0x335b60a0, 0x335b6200, 0x33b50000) 
 compacting perm gen  total 12288K, used 377K [0x33b50000, 0x34750000, 0x37b50000) 
   the space 12288K,   3% used [0x33b50000, 0x33bae5c0, 0x33bae600, 0x34750000) 

以 MaxTenuringThreshold=15 参数来运行的结果:

 ......
  from space 1024K,   39% used [0x32f50000, 0x32f50088, 0x33050000) 
 ......
  the space 10240K,  40% used [0x33150000, 0x335b60a0, 0x335b6200, 0x33b50000) 
......

3,4,结果分析

以上分别将参数 -XX:MaxTenuringThreshold设置成1 和15来进行测试代码中的 testTenuringThreshold() 方法,

此方法中的allocation1 对象需要256KB内存,Survivor空间可以容纳。

  • 当MaxTenuringThreshold = 1 时,allocation1 对象在第二次GC 时进入老年代,新生代已使用的内存GC 后非常干净地变成 0KB。
  • 当MaxTenuringThreshold = 15 时,在第二次GC后,allocation1 对象还留在新生代 Survivor空间,此时新生代仍然有404KB 被占用。

PS:年轻=10m;eden=8m;survivor(to+from)=2m;to区只是过度,实际年轻代只有eden+to_survivor=9m。老年代=10m。

1,allocation1=0.25m,allocation2=4m,依次分配在年轻代的eden区,当需要存放allocation2=4m(4+0.25+4>8)内存不够,触发第一次gc,
2,将allocation1=0.25m移到to区,年龄=1,allocation2=4m(4>1)直接移到老年代中,eden区清空了,然后将to区转换为from区。
3,然后将allocation2+allocation3=8m存入eden(刚好存满),然后allocation3=null即让其成为垃圾对象。
当allocation3再赋值新对象,eden空间不够触发第二次gc,eden中两个对象一个被清理一个将被移到老年代。
from区的allocation1
=0.25也被第二gc处理,年龄+1,然后进入老年代( 因为设置年龄大于1可以进入老年代)。
第二次gc年轻代内存都被清空。

参考
堆分代(年轻代,老年代

4 动态对象年龄判断

  • 为了能够更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象年龄必须达到MaxTenuringThreshold规定值才能晋升老年代,
  • 如果在 Survivor 空间中相同年龄所有对象大小的总和大于Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到参数的规定值。

4,1,代码实践与日志展示

大体的新生代与老年代内存大小设置一样,这里将参数-XX:MaxTenuringThreshold(对象晋升老年代的年龄阀值)设置为15。

【动态对象年龄判断】
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];
}

运行结果:

[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 1 (max 15) 
- age   1:     680304 bytes,     680304 total 
: 4951K->664K(9216K), 0.0033210 secs] 4951K->4760K(19456K), 0.0033442 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
[GC [DefNew 
Desired survivor size 524288 bytes, new threshold 15 (max 15) 
- age   1:        136 bytes,        136 total 
: 4924K->0K(9216K), 0.0011772 secs] 9020K->4760K(19456K), 0.0011987 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
Heap 
 def new generation   total 9216K, used 4260K [0x32750000, 0x33150000, 0x33150000) 
  eden space 8192K,  52% used [0x32750000, 0x32b78fe0, 0x32f50000) 
  from space 1024K,   0% used [0x32f50000, 0x32f50088, 0x33050000) 
  to   space 1024K,   0% used [0x33050000, 0x33050000, 0x33150000) 
 tenured generation   total 10240K, used 4760K [0x33150000, 0x33b50000, 0x33b50000) 
   the space 10240K,  46% used [0x33150000, 0x335f60b0, 0x335f6200, 0x33b50000) 
 compacting perm gen  total 12288K, used 377K [0x33b50000, 0x34750000, 0x37b50000) 
   the space 12288K,   3% used [0x33b50000, 0x33bae5c0, 0x33bae600, 0x34750000) 

结果分析:

执行完代码后,结果中的 Survivor空间占用仍为 0%,而老年代比预期增加了 6%,

也就是说:allocation1 、allocation2 对象都直接进入老年代,而没有 等到15岁的临界年龄。

因为这两个对象加起来已达了512KB,并且它们是同年的,满足同年对象达到Survivor 空间的一半规则。

(只需注释掉其中一个对象的new操作,就会发现另外一个就不会晋升到老年代中)

补充:Survivor空间溢出实例

 

5,空间分配担保

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

1,如果以上条件成立,那么 Minor GC可确保时安全的。
2,若不成立,则虚拟机会查看HandlePromotionFailure参数设置值是否允许担保失败。

如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。 

1,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;
2,如果小于或者HandlePromotionFailure参数设置不允许“冒险”,此时改为进行一次 Full GC。

5,2,“冒险”概念解析:

  1. 在前面提过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个 Survivor空间作为轮换备份,
  2. 因此当出现大量对象在 Minor GC 后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),
  3. 就需要老年代进行分配担保,把 Survivor无法容纳的对象直接进入老年代。
  • 前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,
  • 所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行 Full GC 来让老年代腾出更多空间。

5,3,测试环境设置

大体的新生代与老年代内存大小设置都是一样,新加了一个参数:-XX:HandlePromotionFailure,用来设置是否允许担保失败

【空间分配担保】
private static final int _1MB = 1024 * 1024;
/**
 * VM参数:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
 */
@SuppressWarnings("unused")
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];
    allocation5 = new byte[2 * _1MB];
    allocation6 = new byte[2 * _1MB];
    allocation4 = null;
    allocation5 = null;
    allocation6 = null;
    allocation7 = new byte[2 * _1MB];
}

以 HandlePromotionFailure = false参数来运行的结果:

[GC [DefNew: 6487K->152K(9216K), 0.0040346 secs] 6487K->4248K(19456K), 0.0040639 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
[GC [DefNew: 6546K->6546K(9216K), 0.0004896 secs] 10642K->4248K(19456K), 0.0005141 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

以 HandlePromotionFailure = true参数来运行的结果:

[GC [DefNew: 6487K->152K(9216K), 0.0040346 secs] 6487K->4248K(19456K), 0.0040639 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
[GC [DefNew: 6546K->152K(9216K), 0.0004896 secs] 10642K->4248K(19456K), 0.0006143 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

5,4,结果分析:

从日志可看出,设置HandlePromotionFailure 参数不同的值,影响到虚拟机的空间分配担保原则,当参数为true时,即允许担保失败,

继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,来决定后续是 Minor GC 还是 Full GC。

 

posted @ 2018-03-10 14:54  假程序猿  阅读(168)  评论(0编辑  收藏  举报