天天看点

实战:内存分配与回收策略

对象优先在Eden分配

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

大对象直接进入老年代

  大对象就是指需要大量连续内存空间的Java对象,最典型的大对象便是那种很长的字符串,或者 元素数量很庞大的数组,本节例子中的byte[]数组就是典型的大对象。大对象对虚拟机的内存分配来说 就是一个不折不扣的坏消息,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对 象”,我们写程序的时候应注意避免。在Java虚拟机中要避免大对象的原因是,在分配空间时,它容易 导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好它们,而当复 制对象时,大对象就意味着高额的内存复制开销。HotSpot虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden区及两个Survivor区 之间来回复制,产生大量的内存复制操作。

/**
 * @author Wen
 * 代码清单3-8 大对象直接进入老年代
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC
 */
public class test1 {

    private static final int _1MB = 1024 * 1024;

    public static void testPretenureSizeThreshold() {
        byte[] allocation;
        //直接分配在老年代中
        allocation = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
        testPretenureSizeThreshold();
    }
}
           
Heap
 def new generation   total 9216K, used 3054K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  9% used [0x00000000fec00000, 0x00000000feefbb38, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffc00010, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 3250K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
           

来 假装分析一波

首先我们通过-XX:SurvivorRatio=8这个参数来指定Eden和Survivor的From、To区域的内存比例为8:1:1。
-Xms20M -Xmx20M -Xmn10M 来指定堆的大小为20M不可扩展且设置年轻代的大小为10M,
通过设置-XX:PretenureSizeThreshold=3145728的大小来限制当对象大于这值时,直接进入老年代.

通过日志打印可以看出Survivor区域并没有被使用,老年代被使用了40%所以我们创建的allocation 直接被分配到了老年代
           

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

  HotSpot虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存 活对象应当放在新生代,哪些存活对象放在老年代中。为做到这点,虚拟机给每个对象定义了一个对 象年龄(Age)计数器,存储在对象头中。对象通常在Eden区里诞生,如果经过第一次 Minor GC后仍然存活,并且能被Survivor容纳的话,该对象会被移动到Survivor空间中,并且将其对象 年龄设为1岁。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程 度(默认为15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX: MaxTenuringThreshold设置。

当-XX:MaxTenuringThreshold=1
/**
 * @author Wen
 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseSerialGC
 */

public class test2 {

    private static final int _1MB = 1024 * 1024;

    @SuppressWarnings("unused")
    public static void testTenuringThreshold() {
        byte[] allocation1, allocation2, allocation3;
        // 什么时候进入老年代决定于XX:MaxTenuringThreshold设置
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }
    public static void main(String[] args) {
        testTenuringThreshold();
    }

}
           
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:    1048576 bytes,    1048576 total
: 7243K->1024K(9216K), 0.0029620 secs] 7243K->5314K(19456K), 0.0029979 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age   1:        640 bytes,        640 total
: 5203K->0K(9216K), 0.0010193 secs] 9494K->5294K(19456K), 0.0010311 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4233K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff022540, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400280, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5293K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  51% used [0x00000000ff600000, 0x00000000ffb2b7b8, 0x00000000ffb2b800, 0x0000000100000000)
 Metaspace       used 3246K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

           

来 假装分析er波

allocation1 = 0.25M		进入Eden = 0.25M
allocation2 = 4M			进入Eden = 4 + 0.25M
allocation3  = 4M			进入Eden  = 8.25 > 8M,这时候Eden内存被占满,触发GC
这时候Survivor也放不下allocation2和allocation3 ,但是可以放下allocation1 ,这时候allocation1 进入From = 0.25M,分代年龄+1,
allocation2 进入老年代 = 4M,allocation3  进入Eden = 4M。
之后allocation3 = 4M 进入Eden,Eden满了触发GC,但是由于我们设置了-XX:MaxTenuringThreshold=1,意味着分代年龄达大于1的对象将送往老年代,
           

动态对象年龄判定

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

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
 * @author Wen
 */
public class Test3 {
    private static final int _1MB = 1024 * 1024;

    public static void test(){
        byte[] allocation1,allocation2,allocation3,allocation4;
        // allocation1+allocation2大于survivo空间一半
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }

    public static void main(String[] args) {
        test();
    }
}
           

参数分析

-Xms20M:分配堆的最小内存

-Xmx20M:分配堆的最大内存(-Xms =-Xmx表示设定堆不可动态扩展内存)

-Xmn10M:分配年轻代的内存

-XX:+PrintGCDetails:在控制台打印日志

-XX:+UseSerialGC:指定年轻代使用Serial搜集器

-XX:SurvivorRatio=8:指定Eden与survivor中Form区和To区内存占比为8:1:1

-XX:MaxTenuringThreshold=15:设置对象分代年龄达到15后进入老年代区域

-XX:+PrintTenuringDistribution:打印分代年龄

[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:    1048568 bytes,    1048568 total
: 7499K->1023K(9216K), 0.0034811 secs] 7499K->5553K(19456K), 0.0035137 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 524288 bytes, new threshold 15 (max 15)
- age   1:        400 bytes,        400 total
: 5203K->0K(9216K), 0.0023783 secs] 9733K->9649K(19456K), 0.0023921 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4234K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff022758, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400190, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 9649K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  94% used [0x00000000ff600000, 0x00000000fff6c558, 0x00000000fff6c600, 0x0000000100000000)
 Metaspace       used 3279K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 355K, capacity 388K, committed 512K, reserved 1048576K
           

来 假装分析er波

allocation1	进入Eden = _1MB/4 < 8M
allocation2	进入Eden = _1MB/4 + _1MB/4 = _1MB/2 < 8M
allocation3	进入Eden = _1MB/2 + 4M  < 8M
allocation4	进入Eden = _1MB/2 + 4M + 4M  > 8M 触发GC,这时候由于Form和To都放不下allocation3,所以
	allocation3直接进入老年代,allocation1、allocation2进入From区分代年龄+1,allocation4	进入Eden = 4M
allocation4 = null		为后面清理未被使用的对象做准备
allocation4 = new byte[4 * _1MB]	进入Eden = 4M + 4M >8M(因为这里还要算上对象头和内存
	补白的大小所以大于8M),触发GC,发现前面allocation4 = null	的对象未被使用,直接清除
	这时候allocation1 + allocation2 大于from区_1MB/2(满足分代年龄相等且内存总和大于survivor的一半)
	直接进入老年代,Eden = 4M,Survivor = 0M,老年代 >4+_1MB/2
           

空间分配担保(后续补充)