本文内容中所使用的JDK版本为jdk1.6.0_23
。
一、问题重现
有如下一段代码:
1 | public class Example { |
编译后执行如下命令:
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 | [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] |
二、原因分析
“一、问题重现”中的代码执行结果抛出了OutOfMemoryError
异常。但是,我们知道,当出现“分配内存不足”时,会触发GC过程,变量b原来引用的占据4M内存的对象会被回收掉,而变量a引用的对象占据3M内存,故而此时,老年代剩余(10M-3M=7M)内存,大于所需5M内存,那么为何还会抛出OutOfMemoryError
异常呢?
原因就在于执行顺序问题:只有当占据5M内存的对象被成功分配内存后,该地址值才会被赋值给变量b,否则在此之前,变量b依旧引用原来的占据4M内存的对象,因此,在GC时,该占据4M内存的对象不会被回收。
三、验证
3.1、验证1
有如下验证代码:
1 | public class Example { |
执行相同的指令,得到如下所示结果:
1 | [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] |
由于b=null
,因此,在GC时,变量b原来引用的占据4M内存的对象被回收,占据5M的对象内存分配成功,并且该地址值被赋值给变量b,此时,老年代使用(3M+5M=8M)内存。
3.2、验证2
有如下验证代码:
1 | public class Example { |
执行相同的指令,得到如下所示结果:
1 | [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] |
由于a=null
,因此,在GC时,变量a原来引用的占据3M内存的对象被回收,占据5M的对象内存分配成功,并且该地址值被赋值给变量b,此时,老年代使用(4M+5M=9M)内存。需要注意的是,变量b原来引用的占据4M内存的对象在下一次GC时会被回收。
3.3、验证3
有如下验证代码:
1 | public class Example { |
执行相同的指令,得到如下所示结果:
1 | [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] |
由于a=null
,因此,在GC时,变量a原来引用的占据3M内存的对象被回收,占据5M的对象内存分配成功,并且该地址值被赋值给变量b,此时,老年代使用(4M+5M=9M)内存。再由于执行System.gc()
命令,即手动触发GC过程(参见结果中以“Full GC (System)”开头的日志记录),变量b原来引用的占据4M内存的对象被回收,最终老年代使用(5M)内存。