0%

一个看起来违反JVM垃圾自动回收机制的例子

本文内容中所使用的JDK版本为jdk1.6.0_23

一、问题重现

有如下一段代码:

1
2
3
4
5
6
7
8
9
10
11
public class Example {
private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws InterruptedException {
byte[] a, b;
a = new byte[3 * _1MB];
b = new byte[4 * _1MB];

b = new byte[5 * _1MB];
}
}

编译后执行如下命令:

1
java -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 Example

以上命令中的虚拟机配置参数表示“Java堆内存20M,其中新生代内存10M,老年代内存10M,新生代中Eden区内存8M,新生代中1个Survivor区内存1M,故而新生代可用内存9M;当对象所需内存大于3M时,直接在老年代进行分配;打印GC日志信息”。
命令执行后结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[GC [Tenured: 7168K->7283K(10240K), 0.0111970 secs] 7495K->7283K(19456K), [Perm : 28K->28K(12288K)], 0.0112600 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[Full GC [Tenured: 7283K->7270K(10240K), 0.0074390 secs] 7283K->7270K(19456K), [Perm : 28K->22K(12288K)], 0.0074690 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Example.main(Example.java:9)
Heap
def new generation total 9216K, used 327K [0x8d770000, 0x8e170000, 0x8e170000)
eden space 8192K, 4% used [0x8d770000, 0x8d7c1f40, 0x8df70000)
from space 1024K, 0% used [0x8df70000, 0x8df70000, 0x8e070000)
to space 1024K, 0% used [0x8e070000, 0x8e070000, 0x8e170000)
tenured generation total 10240K, used 7270K [0x8e170000, 0x8eb70000, 0x8eb70000)
the space 10240K, 71% used [0x8e170000, 0x8e889af8, 0x8e889c00, 0x8eb70000)
compacting perm gen total 12288K, used 22K [0x8eb70000, 0x8f770000, 0x92b70000)
the space 12288K, 0% used [0x8eb70000, 0x8eb75be0, 0x8eb75c00, 0x8f770000)
ro space 10240K, 61% used [0x92b70000, 0x93197708, 0x93197800, 0x93570000)
rw space 12288K, 60% used [0x93570000, 0x93ca7cc0, 0x93ca7e00, 0x94170000)

二、原因分析

“一、问题重现”中的代码执行结果抛出了OutOfMemoryError异常。但是,我们知道,当出现“分配内存不足”时,会触发GC过程,变量b原来引用的占据4M内存的对象会被回收掉,而变量a引用的对象占据3M内存,故而此时,老年代剩余(10M-3M=7M)内存,大于所需5M内存,那么为何还会抛出OutOfMemoryError异常呢?
原因就在于执行顺序问题:只有当占据5M内存的对象被成功分配内存后,该地址值才会被赋值给变量b,否则在此之前,变量b依旧引用原来的占据4M内存的对象,因此,在GC时,该占据4M内存的对象不会被回收。

三、验证

3.1、验证1

有如下验证代码:

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

public static void main(String[] args) {
byte[] a, b;
a = new byte[3 * _1MB];
b = new byte[4 * _1MB];

b = null;

b = new byte[5 * _1MB];
}
}

执行相同的指令,得到如下所示结果:

1
2
3
4
5
6
7
8
9
10
11
12
[GC [Tenured: 7168K->3187K(10240K), 0.0137400 secs] 7495K->3187K(19456K), [Perm : 28K->28K(12288K)], 0.0138020 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
Heap
def new generation total 9216K, used 163K [0x8d770000, 0x8e170000, 0x8e170000)
eden space 8192K, 2% used [0x8d770000, 0x8d798fd0, 0x8df70000)
from space 1024K, 0% used [0x8df70000, 0x8df70000, 0x8e070000)
to space 1024K, 0% used [0x8e070000, 0x8e070000, 0x8e170000)
tenured generation total 10240K, used 8307K [0x8e170000, 0x8eb70000, 0x8eb70000)
the space 10240K, 81% used [0x8e170000, 0x8e98ce88, 0x8e98d000, 0x8eb70000)
compacting perm gen total 12288K, used 28K [0x8eb70000, 0x8f770000, 0x92b70000)
the space 12288K, 0% used [0x8eb70000, 0x8eb770e0, 0x8eb77200, 0x8f770000)
ro space 10240K, 61% used [0x92b70000, 0x93197708, 0x93197800, 0x93570000)
rw space 12288K, 60% used [0x93570000, 0x93ca7cc0, 0x93ca7e00, 0x94170000)

由于b=null,因此,在GC时,变量b原来引用的占据4M内存的对象被回收,占据5M的对象内存分配成功,并且该地址值被赋值给变量b,此时,老年代使用(3M+5M=8M)内存。

3.2、验证2

有如下验证代码:

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

public static void main(String[] args) {
byte[] a, b;
a = new byte[3 * _1MB];
b = new byte[4 * _1MB];

a = null;

b = new byte[5 * _1MB];
}
}

执行相同的指令,得到如下所示结果:

1
2
3
4
5
6
7
8
9
10
11
12
[GC [Tenured: 7168K->4211K(10240K), 0.0163810 secs] 7495K->4211K(19456K), [Perm : 28K->28K(12288K)], 0.0164420 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
Heap
def new generation total 9216K, used 163K [0x8d770000, 0x8e170000, 0x8e170000)
eden space 8192K, 2% used [0x8d770000, 0x8d798fd0, 0x8df70000)
from space 1024K, 0% used [0x8df70000, 0x8df70000, 0x8e070000)
to space 1024K, 0% used [0x8e070000, 0x8e070000, 0x8e170000)
tenured generation total 10240K, used 9331K [0x8e170000, 0x8eb70000, 0x8eb70000)
the space 10240K, 91% used [0x8e170000, 0x8ea8ce88, 0x8ea8d000, 0x8eb70000)
compacting perm gen total 12288K, used 28K [0x8eb70000, 0x8f770000, 0x92b70000)
the space 12288K, 0% used [0x8eb70000, 0x8eb770c0, 0x8eb77200, 0x8f770000)
ro space 10240K, 61% used [0x92b70000, 0x93197708, 0x93197800, 0x93570000)
rw space 12288K, 60% used [0x93570000, 0x93ca7cc0, 0x93ca7e00, 0x94170000)

由于a=null,因此,在GC时,变量a原来引用的占据3M内存的对象被回收,占据5M的对象内存分配成功,并且该地址值被赋值给变量b,此时,老年代使用(4M+5M=9M)内存。需要注意的是,变量b原来引用的占据4M内存的对象在下一次GC时会被回收。

3.3、验证3

有如下验证代码:

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

public static void main(String[] args) throws InterruptedException {
byte[] a, b;
a = new byte[3 * _1MB];
b = new byte[4 * _1MB];

a = null;

b = new byte[5 * _1MB];

System.gc();
// 等待手动触发的GC完成
Thread.sleep(3000);
}
}

执行相同的指令,得到如下所示结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
[GC [Tenured: 7168K->4211K(10240K), 0.0178210 secs] 7495K->4211K(19456K), [Perm : 28K->28K(12288K)], 0.0178700 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[Full GC (System) [Tenured: 9331K->5235K(10240K), 0.0105780 secs] 9331K->5235K(19456K), [Perm : 28K->28K(12288K)], 0.0108090 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 327K [0x8d770000, 0x8e170000, 0x8e170000)
eden space 8192K, 4% used [0x8d770000, 0x8d7c1f48, 0x8df70000)
from space 1024K, 0% used [0x8df70000, 0x8df70000, 0x8e070000)
to space 1024K, 0% used [0x8e070000, 0x8e070000, 0x8e170000)
tenured generation total 10240K, used 5235K [0x8e170000, 0x8eb70000, 0x8eb70000)
the space 10240K, 51% used [0x8e170000, 0x8e68ce88, 0x8e68d000, 0x8eb70000)
compacting perm gen total 12288K, used 28K [0x8eb70000, 0x8f770000, 0x92b70000)
the space 12288K, 0% used [0x8eb70000, 0x8eb77140, 0x8eb77200, 0x8f770000)
ro space 10240K, 61% used [0x92b70000, 0x93197708, 0x93197800, 0x93570000)
rw space 12288K, 60% used [0x93570000, 0x93ca7cc0, 0x93ca7e00, 0x94170000)

由于a=null,因此,在GC时,变量a原来引用的占据3M内存的对象被回收,占据5M的对象内存分配成功,并且该地址值被赋值给变量b,此时,老年代使用(4M+5M=9M)内存。再由于执行System.gc()命令,即手动触发GC过程(参见结果中以“Full GC (System)”开头的日志记录),变量b原来引用的占据4M内存的对象被回收,最终老年代使用(5M)内存。

您的支持将鼓励我继续分享!