这篇文章描述一段很有趣的内存溢出现象。
下面的代码会抛出OutOfMemoryError:
public class JavaMemoryPuzzle {
private final int dataSize = (int)(Runtime.getRuntime().maxMemory() * 0.6);
public void foo(){
{
byte[] data = new byte[dataSize];
}
byte[] data1 = new byte[dataSize];
}
public static void main(String[] args){
JavaMemoryPuzzle puzzle = new JavaMemoryPuzzle();
puzzle.foo();
}
}
而下面的代码就不会:
public class JavaMemoryPuzzle {
private final int dataSize = (int)(Runtime.getRuntime().maxMemory() * 0.6);
public void foo(){
{
byte[] data = new byte[dataSize];
}
String msg = "Hello";
byte[] data1 = new byte[dataSize];
}
public static void main(String[] args){
JavaMemoryPuzzle puzzle = new JavaMemoryPuzzle();
puzzle.foo();
}
}
区别在哪里?
我们只在第一个版本的foo方法的内部block块后下加了
String msg = "Hello";
很明显,这段代码的加入促使JVM在实例化data1时发生了一次垃圾收集,为何?
利用javap -verbose JavaMemoryPuzzle,我们来看第一版本的foo方法的汇编代码:
0: aload_0
1: getfield #24; //Field dataSize:I
4: newarray byte
6: astore_1
7: aload_0
8: getfield #24; //Field dataSize:I
11: newarray byte
13: astore_1
14: return
第0行将this放置到操作数栈顶部,第1行弹出this并从this的实例堆内存中加载dataSize数据到操作数栈顶部,第4行生成数组,将数组的指针放到操作数栈顶部,第6行从操作数栈顶部弹出刚刚生成的数组指针并将之放到第2个栈变量。接下来在11行再次生成一个数组,这个数组很大,以至于JVM无法分配足够的内存来实例化,抛出OutOfMemoryError。为何在第二次生成很大的数组时,JVM没有回收data的空间,毕竟data在方法块内。原来,按照这段汇编代码,第二次生成数组时,data的应用还在栈的第二个变量位置处保存,data还是一个强引用变量,JVM当然不去回收他。
从生成的汇编可以看到,版本1的代码和下面的代码等价:
byte[] data = new byte[dataSize];
data = new byte[dataSize];
我们再来看版本2的汇编:
0: aload_0
1: getfield #24; //Field dataSize:I
4: newarray byte
6: astore_1
7: ldc #31; //String Hello
9: astore_1
10: aload_0
11: getfield #24; //Field dataSize:I
14: newarray byte
16: astore_2
17: return
秘密就爱第7行和第9行。第7行从常量池加载"Hello"到操作数栈,第9行清空操作数栈,将栈顶部的"Hello"赋值给第二个栈变量,就是这次赋值,使data应用的一片内存区域变成了孤魂野鬼,第14行再次生成一个大数组时,JVM回收了原来data引用的那一大片数组内存。