Java OutOfMemoryError dziwne zachowanie

Zakładając, że mamy pamięć max 256M, dlaczego ten kod działa:

public static void main(String... args) {
  for (int i = 0; i < 2; i++)
  {
      byte[] a1 = new byte[150000000];
  }
  byte[] a2 = new byte[150000000];
}
Ale ten rzuca OOME?
public static void main(String... args) {
  //for (int i = 0; i < 2; i++)
  {
      byte[] a1 = new byte[150000000];
  }
  byte[] a2 = new byte[150000000];
}
Author: Marko Topolnik, 2012-11-23

2 answers

Aby zachować perspektywę, rozważ użycie tego kodu z -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;
}

W zależności od tego, czy wykonasz rozgrzewkę, czy pominiesz ją, będzie dmuchać,czy nie. Dzieje się tak dlatego, że poprawny kod JITted null jest poza var, podczas gdy interpretowany kod nie. oba zachowania są akceptowalne zgodnie ze specyfikacją języka Java, co oznacza, że jesteś z tym na łasce JVM.

Testowane z Java HotSpot(TM) 64-Bit Server VM (build 23.3-b01, mixed mode) na OS X.

Bytecode analiza

Spójrz na bajt kodu za pomocą pętli for (prosty kod, bez zmiennej 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

I bez:

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

Brak jawnego null w obu przypadkach, ale zauważ, że w przykładzie no-for ta sama lokalizacja pamięci jest rzeczywiście ponownie używana, w przeciwieństwie do przykładu for. Prowadziłoby to, jeśli już, do oczekiwania przeciwnego do obserwowanego zachowania.

A twist...

Na podstawie tego, czego dowiedzieliśmy się z bytecode, spróbuj uruchomić to:

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

No OOME thrown . Skomentuj deklarację a2 i już jest. Przydzielamy więcej , ale zajmujemy mniej ? Spójrz na kod bajtowy:

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        

Miejsce 2, używane dla a1, jest ponownie używane dla a2. To samo dotyczy kodu OP, ale teraz nadpisujemy lokalizację z odniesieniem do nieszkodliwej tablicy o zerowej długości i używamy innej lokalizacji do przechowywania odniesienia do naszej ogromnej tablicy.

Podsumowując w górę...

Specyfikacja języka Java nie określa, że każdy obiekt śmieci musi być pobrany, a specyfikacja JVM mówi tylko, że "ramka" ze zmiennymi lokalnymi jest niszczona jako całość po zakończeniu metody. Dlatego wszystkie zachowania, których byliśmy świadkami, są zgodne z książką. Stan obiektu invisible (wspomniany w dokumencie linked to by keppil ) jest tylko sposobem na opisanie tego, co dzieje się w niektórych implementacjach i pod niektórymi okoliczności, ale nie jest w żaden sposób kanonicznym zachowaniem.

 35
Author: Marko Topolnik,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-08-30 16:34:28

Dzieje się tak dlatego, że podczas gdy a1 nie znajduje się w zakresie po nawiasach, jest w stanie o nazwie invisible dopóki metoda nie powróci.

Większość nowoczesnych systemów JVM nie ustawia zmiennej a1 na null, gdy tylko opuści zakres (w rzeczywistości to, czy wewnętrzne nawiasy są tam, czy nie, nawet nie zmienia generowanego kodu bajtowego), ponieważ jest to bardzo nieefektywne i zwykle nie ma znaczenia. W związku z tym, a1 nie można zbierać śmieci, dopóki metoda nie powróci.

Możesz sprawdzić to przez dodanie linii

a1 = null;

Wewnątrz nawiasów, co sprawia, że program działa dobrze.

Termin niewidzialny i Wyjaśnienie pochodzi z tego starego papieru: http://192.9.162.55/docs/books/performance/1st_edition/html/JPAppGC.fm.html.

 26
Author: Keppil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-07 04:59:03