Подтвердить что ты не робот

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];
}
4b9b3361

Ответ 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 говорит только о том, что "фрейм" с локальными переменными уничтожается в целом после завершения метода. Поэтому все поведение, которое мы наблюдаем, - это книга. Невидимое состояние объекта (упомянутое в документе, связанное с кеппилем) - это просто способ описать, что происходит в некоторых реализациях и при некоторых обстоятельствах, но никоим образом не является каноническим поведением.

Ответ 2

Это связано с тем, что пока a1 не находится в области видимости после скобок, он находится в состоянии, называемом невидимым, пока метод не вернется.

Большинство современных JVM не устанавливают переменную a1 на null, как только она покидает область действия (на самом деле, существуют ли внутренние скобки или нет, даже не меняют сгенерированный байт-код), потому что это очень неэффективен и, как правило, не имеет значения. Поэтому a1 не может быть выгружен мусором до тех пор, пока метод не вернется.

Вы можете проверить это, добавив строку

a1 = null;

внутри скобок, что заставляет программу работать нормально.

Термин невидимый и объяснение взято из этого старого документа: http://192.9.162.55/docs/books/performance/1st_edition/html/JPAppGC.fm.html.