Ответ 1
Чтобы сохранить все в перспективе, рассмотрите возможность запуска этого кода с помощью -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;
}
В зависимости от того, сделаете ли вы прогрев или пропустите его, он взорвется или не ударит. Это связано с тем, что JIT-код правильно 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
вывода в любом случае, но обратите внимание, что в случае отсутствия, например, одно и то же место памяти фактически повторно используется, в отличие от, например,. Это могло бы привести к ожиданию, противоположному наблюдаемому поведению.
Твист...
Основываясь на том, что мы узнали из байт-кода, попробуйте запустить это:
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
Место 2, используемое для a1
, используется повторно для a2
. То же самое верно для кода OP, но теперь мы перезаписываем местоположение ссылкой на безобидный массив нулевой длины и используем другое место для хранения ссылки на наш огромный массив.
Подводя итог...
Спецификация языка Java не указывает, что любой объект мусора должен быть собран, а спецификация JVM говорит только о том, что "фрейм" с локальными переменными уничтожается в целом после завершения метода. Поэтому все поведение, которое мы наблюдаем, - это книга. Невидимое состояние объекта (упомянутое в документе, связанное с кеппилем) - это просто способ описать, что происходит в некоторых реализациях и при некоторых обстоятельствах, но никоим образом не является каноническим поведением.