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

254 阅读11分钟

1.对象优先在Eden分配

-verbose:gc 打印垃圾回收信息
-Xms20M 设置堆容量为20M
-Xmx20M 设置堆容量最大为20M(-Xms=-Xmx不可扩展)
-Xmn10M 设置新生代容量为10M
-XX:+PrintGCDetails 打印垃圾回收细节
-XX:SurvivorRatio=8 新生代中Eden区与Survivor区空间比例为8:1
-XX:+UseSerialGC 使用Serial垃圾收集器

public class TestAllocation {
	public static void main(String[] args) {
		int _1MB =1024 * 1024;
		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];//触发GC
	}
}

运行结果

[0.068s][info   ][gc,start     ] GC(0) Pause Young (Allocation Failure)
[0.072s][info   ][gc,heap      ] GC(0) DefNew: 7363K->699K(9216K)
[0.072s][info   ][gc,heap      ] GC(0) Tenured: 0K->6144K(10240K)
[0.072s][info   ][gc,metaspace ] GC(0) Metaspace: 3840K->3840K(1056768K)
[0.072s][info   ][gc           ] GC(0) Pause Young (Allocation Failure) 7M->6M(19M) 3.541ms
[0.072s][info   ][gc,cpu       ] GC(0) User=0.00s Sys=0.00s Real=0.00s
[0.073s][info   ][gc,heap,exit ] Heap
[0.073s][info   ][gc,heap,exit ]  def new generation   total 9216K, used 5035K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
[0.073s][info   ][gc,heap,exit ]   eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff03c2f8, 0x00000000ff400000)
[0.073s][info   ][gc,heap,exit ]   from space 1024K,  68% used [0x00000000ff500000, 0x00000000ff5aecf8, 0x00000000ff600000)
[0.073s][info   ][gc,heap,exit ]   to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
[0.073s][info   ][gc,heap,exit ]  tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
[0.073s][info   ][gc,heap,exit ]    the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
[0.073s][info   ][gc,heap,exit ]  Metaspace       used 3849K, capacity 4486K, committed 4864K, reserved 1056768K
[0.073s][info   ][gc,heap,exit ]   class space    used 345K, capacity 386K, committed 512K, reserved 1048576K

在给allocation4分配内存空间时,由于在Eden区已分配3*2M,剩余空间(2M)不足4M,分配失败触发Minor GC。由于allocation1,2,3仍旧存活,而Survivor区放不下,因此通过分配担保机制将allocation1,2,3提前转移到老年区,此时老年区的内存空间被占用了60%。操作完成后,allocation4便可以被放入Eden区。

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

VM设置在之前设置的基础上增加
-XX:PretenureSizeThreshold=3145728 指定大于3MB的对象直接在老年代分配

public class TestAllocation {
	public static void main(String[] args) {
		int _1MB =1024 * 1024;
		byte[] allocation1;
		allocation1 = new byte[4 * _1MB];//大于3MB直接在老年代分配
	}
}

运行结果:

[0.070s][info   ][gc,heap,exit ] Heap
[0.070s][info   ][gc,heap,exit ]  def new generation   total 9216K, used 1384K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
[0.070s][info   ][gc,heap,exit ]   eden space 8192K,  16% used [0x00000000fec00000, 0x00000000fed5a0c0, 0x00000000ff400000)
[0.070s][info   ][gc,heap,exit ]   from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
[0.070s][info   ][gc,heap,exit ]   to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
[0.070s][info   ][gc,heap,exit ]  tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
[0.070s][info   ][gc,heap,exit ]    the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
[0.070s][info   ][gc,heap,exit ]  Metaspace       used 3846K, capacity 4486K, committed 4864K, reserved 1056768K
[0.070s][info   ][gc,heap,exit ]   class space    used 345K, capacity 386K, committed 512K, reserved 1048576K

老年区的内存空间被占用40%。

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

VM设置在最初设置的基础上增加
-XX:MaxTenuringThreshold=1 对象年龄达到1就进入老年代

public class TestAllocation {
	public static void main(String[] args) {
		int _1MB =1024 * 1024;
		byte[] allocation1, allocation2, allocation3;
		allocation1 = new byte[_1MB / 4];
		allocation2 = new byte[4 * _1MB];
		allocation3 = new byte[4 * _1MB];
		allocation3 = null;
		allocation3 = new byte[4 * _1MB];
	}
}

运行结果:

[0.092s][info   ][gc,heap,exit ] Heap
[0.092s][info   ][gc,heap,exit ]  def new generation   total 9216K, used 4334K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
[0.092s][info   ][gc,heap,exit ]   eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff03bbe0, 0x00000000ff400000)
[0.092s][info   ][gc,heap,exit ]   from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
[0.092s][info   ][gc,heap,exit ]   to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
[0.092s][info   ][gc,heap,exit ]  tenured generation   total 10240K, used 5051K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
[0.092s][info   ][gc,heap,exit ]    the space 10240K,  49% used [0x00000000ff600000, 0x00000000ffaeed18, 0x00000000ffaeee00, 0x0000000100000000)
[0.092s][info   ][gc,heap,exit ]  Metaspace       used 3854K, capacity 4486K, committed 4864K, reserved 1056768K
[0.092s][info   ][gc,heap,exit ]   class space    used 345K, capacity 386K, committed 512K, reserved 1048576K

在创建allocation3对象时,因为Eden区空间不足4MB,因此触发Minor GC,将allocation1转移到Survivor区,并将其对象年龄设为1,因为-XX:MaxTenuringThreshold=1规定对象年龄达到1就进入老年代,因此在第二次创建allocation3对象时,allocation1转移进入老年区,Survivor区此时为空。

调整-XX:MaxTenuringThreshold=15(默认值)
运行结果:

[0.074s][info   ][gc,heap,exit ] Heap
[0.074s][info   ][gc,heap,exit ]  def new generation   total 9216K, used 4334K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
[0.074s][info   ][gc,heap,exit ]   eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff03bbe0, 0x00000000ff400000)
[0.074s][info   ][gc,heap,exit ]   from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
[0.074s][info   ][gc,heap,exit ]   to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
[0.074s][info   ][gc,heap,exit ]  tenured generation   total 10240K, used 5051K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
[0.074s][info   ][gc,heap,exit ]    the space 10240K,  49% used [0x00000000ff600000, 0x00000000ffaeed18, 0x00000000ffaeee00, 0x0000000100000000)
[0.074s][info   ][gc,heap,exit ]  Metaspace       used 3851K, capacity 4486K, committed 4864K, reserved 1056768K
[0.074s][info   ][gc,heap,exit ]   class space    used 345K, capacity 386K, committed 512K, reserved 1048576K

这里有点问题,为什么还是会把Survivor区清空呢?
检查问题↓

public class TestAllocation {
	public static void main(String[] args) {
//		int _1MB =1024 * 1024;
//		byte[] allocation1, allocation2, allocation3;
//		allocation1 = new byte[_1MB / 4];
//		allocation2 = new byte[4 * _1MB];
//		allocation3 = new byte[4 * _1MB];
//		allocation3 = null;
//		allocation3 = new byte[4 * _1MB];
	}
}

运行结果:

[0.065s][info   ][gc,heap,exit ] Heap
[0.065s][info   ][gc,heap,exit ]  def new generation   total 9216K, used 1384K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
[0.065s][info   ][gc,heap,exit ]   eden space 8192K,  16% used [0x00000000fec00000, 0x00000000fed5a080, 0x00000000ff400000)
[0.065s][info   ][gc,heap,exit ]   from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
[0.065s][info   ][gc,heap,exit ]   to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
[0.065s][info   ][gc,heap,exit ]  tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
[0.065s][info   ][gc,heap,exit ]    the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
[0.065s][info   ][gc,heap,exit ]  Metaspace       used 3847K, capacity 4486K, committed 4864K, reserved 1056768K
[0.065s][info   ][gc,heap,exit ]   class space    used 345K, capacity 386K, committed 512K, reserved 1048576K

Eden区还是有16%的内存空间被占用,这部分内存占用中的一部分可能回随着allocation1一起转移至Survivor空间中,结合下面讲的动态对象年龄判定,猜测导致上述问题的应该就是这部分内存占用。

4.动态对象年龄判定

如果在Survivor空间中年龄相同的对象大小的总和达到Survivor空间的一半,年龄大于或等于改年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold设置的年龄。

因此上述问题产生的原因可能就是,多出来的那部分内存占用与allocation1一起转移至Survivor空间中,由于它们的年龄都是1,且空间占用大于Survivor空间的一半以上,直接进入老年代,因此调整-XX:MaxTenuringThreshold从1到15并没有起作用。

5.空间分配担保

在Minor GC前,虚拟机会检查老年代最大可用连续内存空间是否大于新生代所有对象总空间,如果大于这一次Minor GC则是安全的。因为如果Minor GC之后新生代的存活对象所需内存空间还是大于老年代最大可用连续内存空间,分配担保就会失败,就必须进行一次Full GC。在JDK6 Update24之前,可以使用-XX:HandlePromotionFailure参数设置是否接收分配担保失败,如果接收则可能进行Minor GC(Minor GC操作前会比较历史分配担保对象的平均大小和老年代当前最大可用连续内存空间)后分配担保成功,也可能分配担保失败而进行Full GC;如果不接受则直接进行Full GC。在在JDK6 Update24之后,这个参数不起作用了,默认接收分配担保失败,避免频繁进行Full GC操作。

因为没有下载JDk6之前的版本,所以就懒得下载尝试了,毕竟这个参数现在已经不能影响虚拟机的操作了,了解一下即可。