0%

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

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

一、问题重现

有如下一段代码:

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];
    }
}

编译后执行如下命令:

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日志信息”。
命令执行后结果如下:

[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

有如下验证代码:

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];
    }
}

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

[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

有如下验证代码:

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];
    }
}

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

[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

有如下验证代码:

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);
    }
}

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

[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)内存。

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