为了使事情保持正确,请考虑使用以下代码运行此代码-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 摆布。
Java HotSpot(TM) 64-Bit Server VM (build 23.3-b01, mixed mode)
在 OS X 上
测试。
字节码分析
查看带有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 example 中,实际上重用了相同的内存位置,与example相比。如果有的话,这将导致与观察到的行为相反的期望。
一个转折...
根据我们从字节码中学到的知识,尝试运行这个:
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
用于 的位置 2a1
被重新用于a2
。OP 的代码也是如此,但现在我们用对无害的零长度数组的引用覆盖该位置,并使用另一个位置来存储对我们巨大数组的引用。
把它们加起来...
Java 语言规范没有指定必须收集任何垃圾对象,JVM 规范只说带有局部变量的“框架”在方法完成时作为一个整体被销毁。因此,我们目睹的所有行为都是书本上的。对象的不可见状态(在keppil链接的文档中提到)只是描述在某些实现和某些情况下发生的事情的一种方式,但绝不是任何类型的规范行为。