Skip to content

Java 对象内存分配与回收策略

Published: at 16:49:24

通常情况下会先在Eden区分配

大多数的情况下,对象都会先在Eden区分配,当Eden区没有足够的空间分配的时候,会触发一次MinorGC。

MinorGC: 指的是新生代GC,也就是发生在新生代的垃圾回收动作 MajorGC: 指的是老年代GC,也就是发生在老年代的垃圾回收动作,出现MinorGC一般会伴随着至少一次MinorGC,但这并非绝对,在Parallel Scavenge收集器的策略里面就有直接使用MinorGC的过程。

触发新生代垃圾回收代码示例:

JVM参数:-Xmn10m -Xmx20m -Xms20m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
private static final int _1M = 1024*1024;
private static void testMinorGC(){
    byte[] ob1,ob2,ob3,ob4;
    ob1 = new byte[_1M * 2];
    ob2 = new byte[_1M * 2];
    ob3 = new byte[_1M * 2];
    ob4 = new byte[_1M * 4];
}

GC日志输出结果:

[GC (Allocation Failure) [DefNew: **6398K->780K(9216K)**, 0.0033880 secs] **6398K->4876K(19456K)**, 0.0034300 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 9216K, used 7248K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  **eden space 8192K,  78% used** [0x00000000fec00000, 0x00000000ff250f58, 0x00000000ff400000)
  **from space 1024K,  76% used** [0x00000000ff500000, 0x00000000ff5c3268, 0x00000000ff600000)
  **to   space 1024K,   0% used** [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   **the space 10240K,  40% used** [0x00000000ff600000, 0x00000000ffa00020, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 3243K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

执行ob4对象分配时触发第一次MinorGC,新生代的大小变化情况是:6398K->780K(9216K)。 新生代的可使用总大小是9216K(Eden区+一个Survivor区)。

这个GC的原因是:给ob4分配空间的时候发现Eden区不够了(已经被ob2、ob2、ob3占用了6M),因此发生了MinorGC。虚拟机又发生Eden区的三个2M的对象不能放入到Survivor区,所以只能转移(部分)到老年代。最后ob4分配到Eden区。

大对象有可能直接进入到老年代

所谓的大对象是指,需要大量连续空间的JAVA对象,最典型的就是很长的字符串和数组了。

虚拟机提供了一个参数:-XX:PretenureSizeThreshold,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免大对象在Eden区以及两个Survivor区来回复制。

示例:

JVM参数: Xmn10m -Xmx20m -Xms20m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728

JAVA代码:

private static void testPretenureSizeThreshold(){
		int _1M = 1024*1024;
	  byte[] ob1= new byte[_1M * 6];
}

GC日志:

Heap
 def new generation   total 9216K, used 2466K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  30% used [0x00000000fec00000, 0x00000000fee68b50, 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,  60% used** [0x00000000ff600000, 0x00000000ffc00010, 0x00000000ffc00200, 0x0000000100000000)
 Metaspace       used 3250K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

从日志“the space 10240K, 60% used” 可以看出超过3M的对象直接分配到了老年代.

注意:PretenureSizeThreshold 这个参数只针对Serial和ParNew两款垃圾收集器才有效果!!!

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

虚拟机给每个对象定义了一个对象年龄(Age)的计数器,对象每在Survivor区熬过一个MinorGC,年龄就增加1岁,当年龄到了一个特定值(默认15)后,就会被晋升到老年代。

可以通过参数-XX:MaxTenuringThreshold 来设置。

动态年龄

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

空间分配担保

在发生MinorGC之前,虚拟机会检查老年的的可用连续空间是否大于新生代所有对象所占用的空间,如果大于,虚拟机认为MinorGC是安全的,因为就算新生代全部对象都晋升到老年代,老年代也可以容纳下。如果小于,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,虚拟机会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小。如果大于,就尝试一次MinorGC,尽管MinorGC是有风险的,如果小于或者HandlePromotionFailure设置不允许冒险,就进行一次FullGC。

这里尝试一次MinorGC的风险是,尽管执行一次MinorGC,最后新生代存活的对象需要的空间还是可能会很大,极端情况下就是新生代的对象全部存活,这样的话依然会导致担保失败,失败后只好重新发起一次FullGC,一般情况下,为了避免FullGC频繁,还是会将HandlePromotionFailure设置为true。