内存分配与回收策略

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

对象的内存分配,往大的方向讲就是在堆上分配,但也可能经过JIT编译后被拆散为标量类型间接地在栈上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,就按线程优先在TLAB上分配,少数情况下可能直接分配在老年代中。

大对象直接进入老年代

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

-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:+UseSerialGC -XX:SurvivorRatio=8通过该参数限制Java堆大小为20M且不可扩展,10M为新生代10M为老年代,Eden空间和Survivor空间比例时8:1,Eden空间大小为8192K、From Survivor空间大小为1024K、To Survivor空间大小为1024K,新生代是Eden区From Survivor区的总容量为9216K

1
2
3
4
5
6
7
8
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
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];
}

程序在给allocation4对象分配内存时,发现Eden空间已经被占用了6M,剩余空间已经不足分配给allocation4所需的内存,因此发生了Minor GC,在GC期间发现allocation1、allocation2、allocation3都无法放入Survivor空间,只能通过分配担保机制提前转移到老年代中。所以GC结束后Eden空间被占用4M、Survivor空间空闲、老年代被占用6M

1
2
3
4
5
6
7
8
9
10
11
[GC[DefNew: 7669K->547K(9216K), 0.0048823 secs] 7669K->6691K(19456K), 0.0049234 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
def new generation total 9216K, used 5136K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
eden space 8192K, 56% used [0x00000000f9a00000, 0x00000000f9e7b610, 0x00000000fa200000)
from space 1024K, 53% used [0x00000000fa300000, 0x00000000fa388d40, 0x00000000fa400000)
to space 1024K, 0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
tenured generation total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
the space 10240K, 60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
compacting perm gen total 21248K, used 2987K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
the space 21248K, 14% used [0x00000000fae00000, 0x00000000fb0eb120, 0x00000000fb0eb200, 0x00000000fc2c0000)
No shared spaces configured.

大对象对虚拟机内存分配来说是一个坏消息,经常出现大对象容易导致还有部分内存就提前触发垃圾收集以获得足够连续空间

虚拟机可以通过-XX:PretenureSizeThreshold参数设置,大于该参数值的对象直接在老年代中分配内存,目的是为了避免在Eden区和两个Survivor区之间发生大量内存复制。但是该参数只对Serial和ParNew有效

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

虚拟机是通过给每个对象定义一个对象年龄计数器来是被新生代和老年代。

如果对象在Eden出生并经过一次Minor GC后任然存活且能被To Survivor容纳对象年龄将会被设置为1每熬过一次Minor GC年龄就增加1,当年龄增加到一定程度(默认15岁),对象就会被晋升到老年代中。

对象晋升老年代的年龄法制可以通过-XX:MaxTenuringThreshold参数设置。

动态对象年龄判断

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

空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果成立Minor GC确保安全。

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

如果允许会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果大于尝试进行一次Minor GC,如果小于或HandlePromotionFailure设置不允许冒险,这时将进行一次Full GC

当然如果担保失败,也将在失败后重新发起一次Full GC