Java内存不足错误奇怪的行为



假设我们有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:运行此代码

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)测试

字节码分析

看看带有for循环的字节码(简单代码,没有sum变量):

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,但请注意,在No for的示例中,与for

一个转折

根据我们从字节码中学到的知识,尝试运行以下代码:

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的声明,它就回来了。我们分配更多,但占用更少?看看字节码:

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规范只规定在方法完成时将具有局部变量的"框架"作为一个整体销毁。因此,我们所目睹的一切行为都是照本宣科的。对象的不可见状态(在keppil链接到的文档中提到)只是描述某些实现和某些情况下发生的事情的一种方式,但绝不是任何规范行为。

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

大多数现代JVM不会在变量a1离开作用域时立即将其设置为null(实际上,内括号是否存在甚至不会更改生成的字节码),因为这是非常无效的,通常也无关紧要。因此,在方法返回之前,不能对a1进行垃圾回收。

您可以通过添加行来检查

a1 = null;

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

术语不可见及其解释取自这篇旧论文:http://192.9.162.55/docs/books/performance/1st_edition/html/JPAppGC.fm.html

最新更新