Java OutOfMemoryError奇怪的行为

假设我们有一个256M的最大内存,为什么这个代码工作:

public static void main(String... args) { for (int i = 0; i < 2; i++) { byte[] a1 = new byte[150000000]; } byte[] a2 = new byte[150000000]; } 

但是这个扔OOME?

 public static void main(String... args) { //for (int i = 0; i < 2; i++) { byte[] a1 = new byte[150000000]; } byte[] a2 = new byte[150000000]; } 

为了保持-Xmx64m ,请考虑使用-Xmx64m运行此代码:

 static long sum; public static void main(String[] args) { System.out.println("Warming up..."); for (int i = 0; i < 100_000; i++) test(1); System.out.println("Main call"); test(5_500_000); System.out.println("Sum: " + sum); } static void test(int size) { // for (int i = 0; i < 1; i++) { long[] a2 = new long[size]; sum += a2.length; } long[] a1 = new long[size]; sum += a1.length; } 

根据你是否进行热身或跳过,它会吹或不吹。 这是因为JITted代码正确地将null置于var之外,而解释的代码不会。 在Java语言规范下,这两种行为都是可以接受的,这意味着你受此JVM的摆布。

在OS X上使用Java HotSpot(TM) 64-Bit Server VM (build 23.3-b01, mixed mode)进行testing。

字节码分析

for循环查看字节码(简单的代码,没有sumvariables):

 static void test(int); Code: 0: iconst_0 1: istore_1 2: goto 12 5: iload_0 6: newarray long 8: astore_2 9: iinc 1, 1 12: iload_1 13: iconst_1 14: if_icmplt 5 17: iload_0 18: newarray long 20: astore_1 21: return 

没有:

 static void test(int); Code: 0: iload_0 1: newarray long 3: astore_1 4: iload_0 5: newarray long 7: astore_1 8: return 

在这两种情况下都没有明确的null ,但是请注意,在这种情况下,例如,相同的内存位置实际上被重用,与此相反。 如果有的话,这将导致与观察到的行为相反的期望。

扭曲…

根据我们从字节码中学到的东西,试着运行这个:

 public static void main(String[] args) { { long[] a1 = new long[5_000_000]; } long[] a2 = new long[0]; long[] a3 = new long[5_000_000]; } 

没有OOME抛出 。 注释掉a2的声明,它就回来了。 我们分配更多 ,但占用更less ? 看看字节码:

 public static void main(java.lang.String[]); Code: 0: ldc #16 // int 5000000 2: istore_1 3: ldc #16 // int 5000000 5: newarray long 7: astore_2 8: iconst_0 9: newarray long 11: astore_2 12: ldc #16 // int 5000000 14: newarray long 16: astore_3 17: return 

用于a1的位置2被重新用于a2 。 OP的代码也是如此,但是现在我们用一个无关的零长度数组来引用位置,并使用另一个位置来存储对我们巨大数组的引用。

把它们加起来…

Java语言规范并没有指定任何垃圾对象必须被收集,而JVM规范只是说在方法完成时,局部variables的“框架”被作为一个整体销毁。 所以我们目睹的所有行为都是靠书本来的。 一个对象的不可见状态(在keppil中链接的文档中提到)只是一种描述在某些实现中,在某些情况下会发生什么的方法,但绝不是任何一种规范的行为。

这是因为虽然a1不在括号之后的范围内,但在方法返回之前,它处于不可见状态。

大多数现代的JVM一旦离开作用域就不会将variablesa1设置为null (实际上,内部括号是否存在甚至不会改变生成的字节码),因为它非常无效,并且通常不会没关系。 因此,在方法返回之前, a1不能被垃圾回收。

您可以通过添加该行来检查

 a1 = null; 

在括号内,这使程序运行良好。

这个术语是隐形的 ,解释来源于这篇旧论文: http://192.9.162.55/docs/books/performance/1st_edition/html/JPAppGC.fm.html : http://192.9.162.55/docs/books/performance/1st_edition/html/JPAppGC.fm.html