内存分配策略

Java 内存自动管理可归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。

前言

一本好书,每读一遍都会有不同的感受。写读书笔记,一来是便于平时查阅,毕竟技术书籍都比较厚,不方便随时携带;二来是督促自己多读书,理论与实践结合才能不断提升自己。

【深入理解Java虚拟机】阅读笔记: 3.6 内存分配与回收策略

对象优先在 Eden 分配

这里通过 -XX:+UseSerialGC 参数指定使用 Serial / Serial Old收集器。

代码清单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static final int _1MB = 1024 * 1024;

/**
* VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
*/
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
}

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

运行结果:

1
2
3
4
5
6
7
8
9
10
11
[GC[DefNew: 7143K->296K(9216K), 0.0067950 secs] 7143K->6440K(19456K), 0.0068310 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
def new generation total 9216K, used 4558K [0x00000007f9a00000, 0x00000007fa400000, 0x00000007fa400000)
eden space 8192K, 52% used [0x00000007f9a00000, 0x00000007f9e297b0, 0x00000007fa200000)
from space 1024K, 28% used [0x00000007fa300000, 0x00000007fa34a288, 0x00000007fa400000)
to space 1024K, 0% used [0x00000007fa200000, 0x00000007fa200000, 0x00000007fa300000)
tenured generation total 10240K, used 6144K [0x00000007fa400000, 0x00000007fae00000, 0x00000007fae00000)
the space 10240K, 60% used [0x00000007fa400000, 0x00000007faa00030, 0x00000007faa00200, 0x00000007fae00000)
compacting perm gen total 21248K, used 2663K [0x00000007fae00000, 0x00000007fc2c0000, 0x0000000800000000)
the space 21248K, 12% used [0x00000007fae00000, 0x00000007fb099fd0, 0x00000007fb09a000, 0x00000007fc2c0000)
No shared spaces configured.

执行代码中分配 allocation4 对象时发生了一次 Minor GC,新生代
7143K->296K,内存总占用量几乎未减少 7143K->6440K。已有的 3 个 2MB 大小的对象无法放入 Survivor 空间,通过分配担保机制提前转移到了老年代。

大对象直接进入老年代

代码清单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final int _1MB = 1024 * 1024;

/**
* VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC

/**
* VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
* -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold() {
byte[] allocation;
allocation = new byte[4 * _1MB];
}

运行结果:

1
2
3
4
5
6
7
8
9
10
Heap
def new generation total 9216K, used 1163K [0x00000007f9a00000, 0x00000007fa400000, 0x00000007fa400000)
eden space 8192K, 14% used [0x00000007f9a00000, 0x00000007f9b22f38, 0x00000007fa200000)
from space 1024K, 0% used [0x00000007fa200000, 0x00000007fa200000, 0x00000007fa300000)
to space 1024K, 0% used [0x00000007fa300000, 0x00000007fa300000, 0x00000007fa400000)
tenured generation total 10240K, used 4096K [0x00000007fa400000, 0x00000007fae00000, 0x00000007fae00000)
the space 10240K, 40% used [0x00000007fa400000, 0x00000007fa800010, 0x00000007fa800200, 0x00000007fae00000)
compacting perm gen total 21248K, used 2664K [0x00000007fae00000, 0x00000007fc2c0000, 0x0000000800000000)
the space 21248K, 12% used [0x00000007fae00000, 0x00000007fb09a0c0, 0x00000007fb09a200, 0x00000007fc2c0000)
No shared spaces configured.

allocation 对象大小超过了 3MB,直接被分配在老年代中。

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

代码清单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static final int _1MB = 1024 * 1024;

/**
* VM Args: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
* -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
*/
public static void testTenuringThreshold() {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / 4];
// 什么时候进入老年代取决于 -XX:MaxTenuringThreshold
allocation2 = new byte[4 * _1MB];
// 第一次 GC
allocation3 = new byte[4 * _1MB];
allocation3 = null;
// 第二次 GC
allocation3 = new byte[4 * _1MB];
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 566416 bytes, 566416 total
: 5351K->553K(9216K), 0.0057860 secs] 5351K->4649K(19456K), 0.0058320 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC[DefNew
Desired survivor size 524288 bytes, new threshold 1 (max 1)
- age 1: 136 bytes, 136 total
: 4733K->0K(9216K), 0.0021010 secs] 8829K->4640K(19456K), 0.0021340 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
Heap
def new generation total 9216K, used 4178K [0x00000007f9a00000, 0x00000007fa400000, 0x00000007fa400000)
eden space 8192K, 51% used [0x00000007f9a00000, 0x00000007f9e14820, 0x00000007fa200000)
from space 1024K, 0% used [0x00000007fa200000, 0x00000007fa200088, 0x00000007fa300000)
to space 1024K, 0% used [0x00000007fa300000, 0x00000007fa300000, 0x00000007fa400000)
tenured generation total 10240K, used 4640K [0x00000007fa400000, 0x00000007fae00000, 0x00000007fae00000)
the space 10240K, 45% used [0x00000007fa400000, 0x00000007fa888168, 0x00000007fa888200, 0x00000007fae00000)
compacting perm gen total 21248K, used 2664K [0x00000007fae00000, 0x00000007fc2c0000, 0x0000000800000000)
the space 21248K, 12% used [0x00000007fae00000, 0x00000007fb09a1e8, 0x00000007fb09a200, 0x00000007fc2c0000)
No shared spaces configured.

可以看到 allocation1 在第二次 GC 时进入老年代,Survivor 占用变成 0KB

动态对象年龄判定

如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄

空间分配担保

  • 若老年代最大可用的连续空间大于新生代所有对象总空间,则 Minor GC 是安全的

  • 否则,查看 HandlePromotionFailure 是否设置为允许担保失败

  • 若允许,检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小

  • 如果大于,尝试进行一次有风险的 Minor GC

  • 如果小于,直接进行 Full GC

老年代可用连续空间 > 新生代对象总空间?Minor GC 是安全的进行有风险的 Minor GCHandlePromotionFailure=true?老年代可用连续空间 > 历次晋升到老年代平均大小?进行 Full GC

注:在 JDK 6 Update 24 之后,HandlePromotionFailure 参数不会影响虚拟机的空间分配担保策略,即按如下流程执行

老年代可用连续空间 > 新生代对象总空间?Minor GC 是安全的进行有风险的 Minor GC老年代可用连续空间 > 历次晋升到老年代平均大小?进行 Full GC

参考

周志明. 深入理解Java虚拟机