Co się stanie, gdy nie będzie wystarczającej ilości pamięci do wyrzucenia błędu OutOfMemoryError?

207

Wiem, że każdy obiekt wymaga pamięci stosu, a każda operacja podstawowa / referencja na stosie wymaga pamięci stosu.

Gdy próbuję utworzyć obiekt na stercie i nie ma wystarczającej pamięci, aby to zrobić, JVM tworzy błąd java.lang.OutOfMemoryError na stercie i rzuca mi go.

Tak więc domyślnie oznacza to, że JVM ma trochę pamięci zarezerwowanej podczas uruchamiania.

Co się stanie, gdy ta zarezerwowana pamięć zostanie zużyta (zdecydowanie by się zużyła, przeczytaj dyskusję poniżej) i JVM nie ma wystarczającej ilości pamięci na stercie, aby utworzyć instancję java.lang.OutOfMemoryError ?

Czy to po prostu się zawiesza? A może rzuci mnie, nullskoro nie ma pamięci dla newinstancji OOM?

try {
    Object o = new Object();
    // and operations which require memory (well.. that's like everything)
} catch (java.lang.OutOfMemoryError e) {
    // JVM had insufficient memory to create an instance of java.lang.OutOfMemoryError to throw to us
    // what next? hangs here, stuck forever?
    // or would the machine decide to throw us a "null" ? (since it doesn't have memory to throw us anything more useful than a null)
    e.printStackTrace(); // e.printStackTrace() requires memory too.. =X
}

==

Dlaczego JVM nie może zarezerwować wystarczającej ilości pamięci?

Bez względu na to, ile pamięci jest zarezerwowane, nadal można wykorzystać tę pamięć, jeśli JVM nie ma możliwości „odzyskania” tej pamięci:

try {
    Object o = new Object();
} catch (java.lang.OutOfMemoryError e) {
    // JVM had 100 units of "spare memory". 1 is used to create this OOM.
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        // JVM had 99 units of "spare memory". 1 is used to create this OOM.
        try {
            e.printStackTrace();
        } catch (java.lang.OutOfMemoryError e3) {
            // JVM had 98 units of "spare memory". 1 is used to create this OOM.
            try {
                e.printStackTrace();
            } catch (java.lang.OutOfMemoryError e4) {
                // JVM had 97 units of "spare memory". 1 is used to create this OOM.
                try {
                    e.printStackTrace();
                } catch (java.lang.OutOfMemoryError e5) {
                    // JVM had 96 units of "spare memory". 1 is used to create this OOM.
                    try {
                        e.printStackTrace();
                    } catch (java.lang.OutOfMemoryError e6) {
                        // JVM had 95 units of "spare memory". 1 is used to create this OOM.
                        e.printStackTrace();
                        //........the JVM can't have infinite reserved memory, he's going to run out in the end
                    }
                }
            }
        }
    }
}

Lub bardziej zwięźle:

private void OnOOM(java.lang.OutOfMemoryError e) {
    try {
        e.printStackTrace();
    } catch (java.lang.OutOfMemoryError e2) {
        OnOOM(e2);
    }
}
Pacerier
źródło
2
twoja odpowiedź byłaby w dużej mierze zależna od JVM
MozenRath,
23
Jedna biblioteka telefoniczna, z której kiedyś korzystałem (w latach 90-tych), łapała, OutOfMemoryExceptiona następnie robiła coś, co wymagało utworzenia dużego bufora ...
Tom Hawtin - sfałszowano
@ TomHawtin-tackline Co się stanie, jeśli operacje zaangażowane w to spowodują kolejną OOM?
Pacerier
38
Podobnie jak telefon komórkowy wyczerpuje się bateria, ale ma wystarczająco dużo baterii, aby nadal spamować „Wyczerpana bateria”.
Kazuma,
1
„Co się stanie, gdy ta zarezerwowana pamięć zostanie zużyta”: może się to zdarzyć tylko wtedy, gdy program złapie pierwszą OutOfMemoryErrori zachowa odniesienie do niej. Okazuje się, że złapanie a OutOfMemoryErrornie jest tak przydatne, jak mogłoby się wydawać, ponieważ nie można zagwarantować prawie nic ze stanu programu, kiedy go złapie. Zobacz stackoverflow.com/questions/8728866/…
Raedwald,

Odpowiedzi:

145

W JVM nigdy tak naprawdę brakuje pamięci. Z góry wykonuje obliczenia pamięci stosu.

Struktura JVM, rozdział 3 , sekcja 3.5.2 stanowi:

  • Jeśli stosy maszyn wirtualnych Java mogą być dynamicznie rozwijane, a próba rozszerzenia jest podejmowana, ale można udostępnić niewystarczającą ilość pamięci, aby dokonać rozszerzenia, lub jeśli można udostępnić niewystarczającą ilość pamięci, aby utworzyć początkowy stos maszyn wirtualnych Java dla nowego wątku, wirtualna Java maszyna rzuca OutOfMemoryError.

W przypadku sterty , sekcja 3.5.3.

  • Jeśli obliczenia wymagają więcej stosu, niż może być udostępnione przez system automatycznego zarządzania pamięcią masową, wirtualna maszyna Java generuje błąd OutOfMemoryError.

Tak więc wykonuje obliczenia z wyprzedzeniem przed przydzieleniem obiektu.


Co się dzieje, JVM próbuje przydzielić pamięć dla obiektu w pamięci o nazwie Region generacji stałej (lub PermSpace). Jeśli alokacja nie powiedzie się (nawet po tym, jak JVM wywoła Garbage Collector, aby spróbować i przydzielić wolne miejsce), zgłasza OutOfMemoryError. Nawet wyjątki wymagają miejsca w pamięci, więc błąd zostanie zgłoszony w nieskończoność.

Dalsza lektura. ? Ponadto OutOfMemoryErrormoże występować w innej strukturze JVM.

Buhake Sindi
źródło
10
Mam na myśli tak, ale czy wirtualna maszyna Java również nie potrzebuje pamięci, aby zgłosić błąd OutOfMemoryError? Co się stanie, gdy nie będzie pamięci do wyrzucenia OOM?
Pacerier
5
Ale jeśli JVM nie zwróci odwołania do tej samej instancji OOM, czy nie zgadzasz się, że ostatecznie zabraknie jej pamięci zarezerwowanej? (jak pokazano w kodzie w pytaniu)
Pacerier
1
Pozwólcie mi zamieścić odniesienie do komentarza Grahama tutaj: stackoverflow.com/questions/9261705/…
Pacerier
2
Mogłoby być fajnie, gdyby VM zachowało singleton wyjątku OOM dla podanego ekstremalnego przypadku oraz w elektrowni jądrowej.
John K
8
@JohnK: Mam nadzieję, że elektrownie jądrowe nie są zaprogramowane w Javie, podobnie jak promy kosmiczne i Boeing 757 nie są zaprogramowane w Javie.
Dietrich Epp
64

Graham Borland wydaje się mieć rację : przynajmniej moja JVM ponownie używa OutOfMemoryErrors. Aby to przetestować, napisałem prosty program testowy:

class OOMTest {
    private static void test (OutOfMemoryError o) {
        try {
            for (int n = 1; true; n += n) {
                int[] foo = new int[n];
            }
        } catch (OutOfMemoryError e) {
            if (e == o)
                System.out.println("Got the same OutOfMemoryError twice: " + e);
            else test(e);
        }
    }
    public static void main (String[] args) {
        test(null);
    }
}

Po uruchomieniu generuje on następujące dane wyjściowe:

$ javac OOMTest.java && java -Xmx10m OOMTest 
Got the same OutOfMemoryError twice: java.lang.OutOfMemoryError: Java heap space

BTW, JVM, którą uruchamiam (na Ubuntu 10.04) jest następujący:

$ java -version
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)

Edycja: Próbowałem zobaczyć, co by się stało, gdybym zmusił maszynę JVM do całkowitego braku pamięci za pomocą następującego programu:

class OOMTest2 {
    private static void test (int n) {
        int[] foo;
        try {
            foo = new int[n];
            test(n * 2);
        }
        catch (OutOfMemoryError e) {
            test((n+1) / 2);
        }
    }
    public static void main (String[] args) {
        test(1);
    }
}

Jak się okazuje, wydaje się, że zapętla się w nieskończoność. Co ciekawe, próba zakończenia programu za pomocą Ctrl+ Cnie działa, ale daje tylko następujący komunikat:

Java HotSpot(TM) 64-Bit Server VM warning: Exception java.lang.OutOfMemoryError occurred dispatching signal SIGINT to handler- the VM may need to be forcibly terminated

Ilmari Karonen
źródło
Niezły test, to samo dla mnie w wersji „1.7.0_01” 64-bitowa maszyna wirtualna serwera Java HotSpot (TM)
Pacerier
Ciekawy. To sprawia, że ​​wygląda na to, że JVM nie do końca rozumie rekurencję ogona ... (Jakby
używał
Zmodyfikowałem twój kod, aby zobaczyć, ile mam różnych - zawsze wykonuję rozgałęzienie else dokładnie 5 razy.
Irfy,
@Izkata: Wolę powiedzieć, że świadomą decyzją jest wstępne przydzielenie nOOM i ponowne użycie jednego z nich później, aby OOM zawsze mógł zostać wyrzucony. JVM firmy Sun / Oracle nie obsługuje rekurencji ogona w ogóle w IIRC?
Irfy,
10
@Izkata Pętla działa najwyraźniej bez końca, ponieważ JVM ciągle wyrzuca tę samą (5. lub więcej) OOM po wyczerpaniu pamięci. Ma więc nramki na stosie i kończy się tworzeniem i niszczeniem ramki n+1na wieczność, dając wrażenie ciągłego działania.
Irfy,
41

Większość środowisk wykonawczych wstępnie przydzieli podczas uruchamiania lub w inny sposób zarezerwuje wystarczającą ilość pamięci, aby poradzić sobie z sytuacjami głodu pamięci. Myślę, że większość rozsądnych implementacji JVM by to zrobiła.

Graham Borland
źródło
1
stackoverflow.com/questions/9261215/… : Prawda, ale jeśli JVM zarezerwowało 100 jednostek pamięci, aby to zrobić, i wydało 1 jednostkę na pierwszą OOM, co się stanie, jeśli w moim bloku catch OOM wykonam e.printStackTrace ()? e.printStackTrace () również wymaga pamięci. Następnie JVM wyda kolejną jednostkę pamięci, aby rzucić mi kolejną OOM (pozostało 98 jednostek) i łapię to za pomocą e.printStackTrace (), więc JVM rzuca mi kolejną OOM (97 jednostek pozostało) i dobrze, że jest złapana i chciałem ..
Pacerier,
3
Właśnie dlatego OOME nigdy nie zawierało śladu stosu - ślady stosu zabierają pamięć! OOME rozpoczął jedynie próbę włączenia śledzenia stosu w java 6 ( blogs.oracle.com/alanb/entry/… ). Zakładam, że jeśli śledzenie stosu nie jest możliwe, wyjątek jest zgłaszany bez śledzenia stosu.
Sean Reilly,
1
@SeanReilly Mam na myśli, że wyjątek, który nie ma śladu stosu, jest nadal obiektem, który wciąż wymaga pamięci. Pamięć jest potrzebna niezależnie od tego, czy zapewniono śledzenie stosu. Czy to prawda, że ​​w bloku catch, jeśli nie ma już pamięci do utworzenia OOM (nie ma pamięci do utworzenia nawet bez śladu stosu), to złapałbym zero.
Pacerier
17
JVM może zwrócić wiele referencji do pojedynczej instancji statycznej wyjątku OOM. Więc nawet jeśli twoja catchklauzula będzie próbowała wykorzystać więcej pamięci, JVM może po prostu ciągle rzucać tą samą instancją OOM.
Graham Borland,
1
@TheEliteGentleman Zgadzam się, że są to również bardzo dobre odpowiedzi, ale JVM żyje na maszynie fizycznej. Te odpowiedzi nie wyjaśniły, w jaki sposób JVM może magicznie mieć wystarczającą pamięć, aby zawsze zapewniać instancję OOM. „To zawsze ta sama instancja” wydaje się rozwiązać zagadkę.
Pacerier
23

Ostatnim razem, gdy pracowałem w Javie i korzystałem z debuggera, inspektor sterty wykazał, że JVM przydzielił wystąpienie OutOfMemoryError podczas uruchamiania. Innymi słowy, przydziela obiekt, zanim Twój program będzie miał szansę na zajęcie, a co dopiero zabraknie pamięci.

benzado
źródło
12

Ze specyfikacji JVM, rozdział 3.5.2:

Jeśli stosy maszyn wirtualnych Java mogą być dynamicznie rozwijane, a próba rozszerzenia jest podejmowana, ale można udostępnić niewystarczającą ilość pamięci, aby dokonać rozszerzenia, lub jeśli można udostępnić niewystarczającą ilość pamięci, aby utworzyć początkowy stos maszyn wirtualnych Java dla nowego wątku, wirtualna Java maszyna rzucaOutOfMemoryError .

Każda wirtualna maszyna Java musi zagwarantować, że wyrzuci OutOfMemoryError. Oznacza to, że musi być w stanie utworzyć wystąpienie OutOfMemoryError(lub mieć utworzone wcześniej), nawet jeśli nie ma już miejsca na stosie.

Chociaż nie musi to gwarantować, że jest wystarczająca ilość pamięci, aby go złapać i wydrukować ładny ślad stosu ...

Dodanie

Dodano kod, aby pokazać, że JVM może zabraknąć miejsca na sterty, jeśli będzie musiał wyrzucić więcej niż jeden OutOfMemoryError. Ale taka implementacja naruszyłaby powyższy wymóg.

Nie ma wymogu, aby zgłaszane wystąpienia OutOfMemoryErrorbyły unikalne lub tworzone na żądanie. JVM może przygotować dokładnie jedno wystąpienie OutOfMemoryErrorpodczas uruchamiania i wyrzucić je za każdym razem, gdy zabraknie miejsca na stercie - to jest raz w normalnym środowisku. Innymi słowy: przykładem OutOfMemoryErrortego może być singleton.

Andreas Dolk
źródło
Wydaje mi się, że aby to zaimplementować, musiałoby się powstrzymywać od rejestrowania śladu stosu, gdyby było mało miejsca.
Raedwald
@ Raedwald: Tak naprawdę to właśnie robi VM VM, patrz moja odpowiedź.
śleske,
11

Interesujące pytanie :-). Podczas gdy inni dobrze wyjaśnili aspekty teoretyczne, postanowiłem to wypróbować. To jest na Oracle JDK 1.6.0_26, Windows 7 64 bit.

Konfiguracja testowa

Napisałem prosty program do wyczerpania pamięci (patrz poniżej).

Program po prostu tworzy statyczny java.util.Listi utrzymuje w nim świeże ciągi, aż do momentu wyrzucenia OOM. Następnie łapie go i kontynuuje upychanie w nieskończonej pętli (słaba JVM ...).

Wynik testu

Jak widać z danych wyjściowych, pierwsze cztery przypadki wyrzucenia OOME zawierają ślad stosu. Następnie kolejne OOME drukują tylko, java.lang.OutOfMemoryError: Java heap spacejeśliprintStackTrace() wywołaniu.

Najwyraźniej JVM próbuje wydrukować ślad stosu, jeśli może, ale jeśli pamięć jest naprawdę napięta, po prostu pomija ślad, tak jak sugerują inne odpowiedzi.

Interesujący jest również kod skrótu OOME. Zauważ, że kilka pierwszych OOME ma różne skróty. Gdy JVM zacznie pomijać ślady stosu, skrót jest zawsze taki sam. Sugeruje to, że JVM będzie używał nowych (wstępnie przydzielonych?) Instancji OOME tak długo, jak to możliwe, ale jeśli push zostanie wypchnięty, po prostu użyje ponownie tej samej instancji, zamiast mieć nic do rzucenia.

Wynik

Uwaga: Obciąłem niektóre ślady stosu, aby ułatwić odczyt danych wyjściowych („[...]”).

iteration 0
iteration 100000
iteration 200000
iteration 300000
iteration 400000
iteration 500000
iteration 600000
iteration 700000
iteration 800000
iteration 900000
iteration 1000000
iteration 1100000
iteration 1200000
iteration 1300000
iteration 1400000
iteration 1500000
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1069480624
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.Arrays.copyOf(Unknown Source)
    at java.util.ArrayList.ensureCapacity(Unknown Source)
    at java.util.ArrayList.add(Unknown Source)
    at testsl.Div.gobbleUpMemory(Div.java:23)
    at testsl.Div.exhaustMemory(Div.java:12)
    at testsl.Div.main(Div.java:7)
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 616699029
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 2136955031
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Unknown Source)
[...]
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1535562945
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
java.lang.OutOfMemoryError: Java heap space
Ouch: java.lang.OutOfMemoryError: Java heap space; hash: 1734048134
Keep on trying...
[...]

Program

public class Div{
    static java.util.List<String> list = new java.util.ArrayList<String>();

    public static void main(String[] args) {
        exhaustMemory();
    }

    private static void exhaustMemory() {
        try {
            gobbleUpMemory();
        } catch (OutOfMemoryError e) {
            System.out.println("Ouch: " + e+"; hash: "+e.hashCode());
            e.printStackTrace();
            System.out.println("Keep on trying...");
            exhaustMemory();
        }
    }

    private static void gobbleUpMemory() {
        for (int i = 0; i < 10000000; i++) {
            list.add(new String("some random long string; use constructor to force new instance"));
            if (i % 10000000== 0) {
                System.out.println("iteration "+i);
            }
        }

    }
}
Śleske
źródło
Kiedy push przychodzi do wypchnięcia, nie może przydzielić pamięci dla OOME, więc opróżnia już utworzone.
Buhake Sindi
1
Drobna uwaga: niektóre wydruki wydają się być poza sekwencją, prawdopodobnie dlatego, że drukujesz, System.outale printStackTrace()używa System.errdomyślnie. Prawdopodobnie uzyskasz lepsze wyniki, stosując konsekwentnie jeden z tych strumieni.
Ilmari Karonen
@IlmariKaronen: Tak, zauważyłem to. To tylko przykład, więc nie miało to znaczenia. Oczywiście nie użyłbyś tego w kodzie produkcyjnym.
sleske
To prawda, że ​​dopiero po pewnym czasie zrozumiałem, co się dzieje, kiedy po raz pierwszy spojrzałem na wynik.
Ilmari Karonen
6

Jestem całkiem pewien, że JVM upewni się, że ma przynajmniej wystarczającą ilość pamięci, aby zgłosić wyjątek, zanim zabraknie pamięci.

Oscar Gomez
źródło
1
Ale
trafiliby w
4

Wyjątki wskazujące na próbę przekroczenia granic środowiska pamięci zarządzanej są obsługiwane przez środowisko wykonawcze tego środowiska, w tym przypadku JVM. JVM jest własnym procesem, w którym działa IL aplikacji. Jeśli program podejmie próbę wywołania, które przedłuży stos wywołań poza limity lub przydzieli więcej pamięci niż JVM może zarezerwować, środowisko wykonawcze wprowadzi wyjątek, który spowoduje rozwinięcie stosu wywołań. Bez względu na ilość pamięci, jakiej aktualnie potrzebuje Twój program, lub jak głęboki jest stos wywołań, JVM przydzieli wystarczającą ilość pamięci w obrębie własnych granic procesu, aby utworzyć wspomniany wyjątek i wstrzyknąć go do kodu.

KeithS
źródło
„JVM przydzieli wystarczającą ilość pamięci w obrębie własnych granic procesu, aby utworzyć wspomniany wyjątek”, ale jeśli kod zachowuje odniesienie do tego wyjątku, więc nie można go ponownie użyć, to w jaki sposób może stworzyć kolejny? A może sugerujesz, że ma on specjalny obiekt Singleton OOME?
Raedwald
Nie sugeruję tego. Jeśli Twój program przechwytuje i zatrzymuje się na każdym wyjątku, który kiedykolwiek został utworzony, w tym na wyjątkach utworzonych i wstrzykniętych przez JVM lub system operacyjny, w końcu sama JVM przekroczy pewną granicę ustawioną przez system operacyjny, a system operacyjny wyłączy go dla GPF lub podobny błąd. Jednak to przede wszystkim zły projekt; wyjątki powinny zostać obsłużone, a następnie wykraczać poza zakres lub zostać wyrzucone. I NIGDY nie powinieneś próbować łapać i kontynuować na SOE lub OOME; poza „czyszczeniem”, dzięki czemu możesz wyjść z gracją, nic nie możesz zrobić, aby kontynuować wykonywanie w takich sytuacjach.
KeithS
„przede wszystkim to zły projekt”: okropny projekt. Ale pedantycznie, czy JVM byłby zgodny ze specyfikacją, gdyby zawiódł w ten sposób?
Raedwald,
4

Wygląda na to, że mylisz pamięć wirtualną zarezerwowaną przez JVM, w której JVM uruchamia programy Java, z rodzimą pamięcią systemu operacyjnego hosta, w której JVM działa jako proces natywny. JVM na twoim komputerze działa w pamięci zarządzanej przez system operacyjny, a nie w pamięci zarezerwowanej przez JVM do uruchamiania programów Java.

Dalsza lektura:

I jako ostatnia uwaga, próbując złapać się java.lang.Error (i jego potomka klasach), aby wydrukować StackTrace mogą nie daje żadnych użytecznych informacji. Zamiast tego chcesz zrzucić stos.

Michael Tiffany
źródło
4

W celu dalszego wyjaśnienia odpowiedzi @Graham Borland funkcjonalnie JVM robi to podczas uruchamiania:

private static final OutOfMemoryError OOME = new OutOfMemoryError();

Później JVM wykonuje jeden z następujących kodów bajtowych Java: „new”, „anewarray” lub „multianewarray”. Ta instrukcja powoduje, że JVM wykonuje szereg kroków w przypadku braku pamięci:

  1. Powiedzmy, że wywołujemy funkcję natywną allocate() . allocate()próbuje przydzielić pamięć dla nowej instancji określonej klasy lub tablicy.
  2. To żądanie alokacji kończy się niepowodzeniem, więc JVM wywołuje inną funkcję natywną, powiedzmy doGC() , , która próbuje wykonać odśmiecanie.
  3. Kiedy funkcja ta powróci, allocate() próbuje ponownie przydzielić pamięć dla instancji.
  4. Jeśli to się nie powiedzie (*), wówczas JVM, w ramach throw OOME;funkcji replace (), po prostu robi a , odnosząc się do OOME, który utworzył przy uruchomieniu. Zauważ, że nie musiał on alokować tego OOME, on po prostu odnosi się do niego.

Oczywiście nie są to dosłowne kroki; będą się różnić między implementacjami JVM i JVM, ale jest to pomysł na wysokim poziomie.

(*) Przed porażką dzieje się tutaj znaczna ilość pracy. JVM podejmie próbę wyczyszczenia obiektów SoftReference, spróbuje alokacji bezpośrednio do pokrewnej generacji podczas korzystania z kolektora generacji i ewentualnie innych rzeczy, takich jak finalizacja.

ahawtho
źródło
3

Odpowiedzi, które mówią, że JVM wstępnie przydzieli, OutOfMemoryErrorssą rzeczywiście poprawne.
Oprócz przetestowania tego przez wywołanie sytuacji braku pamięci, możemy po prostu sprawdzić stertę dowolnej maszyny JVM (użyłem małego programu, który po prostu się przespał, uruchamiając go przy użyciu JVM Hotspot Oracle z aktualizacji Java 8 31).

Za pomocą jmap widzimy, że wydaje się, że istnieje 9 instancji OutOfMemoryError (mimo że mamy dużo pamięci):

> jmap -histo 12103 | grep OutOfMemoryError
 71: 9 288 java.lang.OutOfMemoryError
170: 1 32 [Ljava.lang.OutOfMemoryError;

Następnie możemy wygenerować zrzut stosu:

> jmap -dump: format = b, file = heap.hprof 12315

i otwórz go za pomocą Eclipse Memory Analyzer , gdzie zapytanie OQL pokazuje, że JVM faktycznie wydaje się wstępnie przydzielać OutOfMemoryErrorsdla wszystkich możliwych komunikatów:

wprowadź opis zdjęcia tutaj

Kod JVM Hotspot Java 8, który faktycznie wstępnie przydziela je, można znaleźć tutaj i wygląda następująco (z pominięciem niektórych części):

...
// Setup preallocated OutOfMemoryError errors
k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_OutOfMemoryError(), true, CHECK_false);
k_h = instanceKlassHandle(THREAD, k);
Universe::_out_of_memory_error_java_heap = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_metaspace = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_class_metaspace = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_array_size = k_h->allocate_instance(CHECK_false);
Universe::_out_of_memory_error_gc_overhead_limit =
  k_h->allocate_instance(CHECK_false);

...

if (!DumpSharedSpaces) {
  // These are the only Java fields that are currently set during shared space dumping.
  // We prefer to not handle this generally, so we always reinitialize these detail messages.
  Handle msg = java_lang_String::create_from_str("Java heap space", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_java_heap, msg());

  msg = java_lang_String::create_from_str("Metaspace", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_metaspace, msg());
  msg = java_lang_String::create_from_str("Compressed class space", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_class_metaspace, msg());

  msg = java_lang_String::create_from_str("Requested array size exceeds VM limit", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_array_size, msg());

  msg = java_lang_String::create_from_str("GC overhead limit exceeded", CHECK_false);
  java_lang_Throwable::set_message(Universe::_out_of_memory_error_gc_overhead_limit, msg());

  msg = java_lang_String::create_from_str("/ by zero", CHECK_false);
  java_lang_Throwable::set_message(Universe::_arithmetic_exception_instance, msg());

  // Setup the array of errors that have preallocated backtrace
  k = Universe::_out_of_memory_error_java_heap->klass();
  assert(k->name() == vmSymbols::java_lang_OutOfMemoryError(), "should be out of memory error");
  k_h = instanceKlassHandle(THREAD, k);

  int len = (StackTraceInThrowable) ? (int)PreallocatedOutOfMemoryErrorCount : 0;
  Universe::_preallocated_out_of_memory_error_array = oopFactory::new_objArray(k_h(), len, CHECK_false);
  for (int i=0; i<len; i++) {
    oop err = k_h->allocate_instance(CHECK_false);
    Handle err_h = Handle(THREAD, err);
    java_lang_Throwable::allocate_backtrace(err_h, CHECK_false);
    Universe::preallocated_out_of_memory_errors()->obj_at_put(i, err_h());
  }
  Universe::_preallocated_out_of_memory_error_avail_count = (jint)len;
}
...

i ten kod pokazuje, że JVM najpierw spróbuje użyć jednego z wstępnie przydzielonych błędów ze spacją dla śledzenia stosu, a następnie powróci do jednego bez śledzenia stosu:

oop Universe::gen_out_of_memory_error(oop default_err) {
  // generate an out of memory error:
  // - if there is a preallocated error with backtrace available then return it wth
  //   a filled in stack trace.
  // - if there are no preallocated errors with backtrace available then return
  //   an error without backtrace.
  int next;
  if (_preallocated_out_of_memory_error_avail_count > 0) {
    next = (int)Atomic::add(-1, &_preallocated_out_of_memory_error_avail_count);
    assert(next < (int)PreallocatedOutOfMemoryErrorCount, "avail count is corrupt");
  } else {
    next = -1;
  }
  if (next < 0) {
    // all preallocated errors have been used.
    // return default
    return default_err;
  } else {
    // get the error object at the slot and set set it to NULL so that the
    // array isn't keeping it alive anymore.
    oop exc = preallocated_out_of_memory_errors()->obj_at(next);
    assert(exc != NULL, "slot has been used already");
    preallocated_out_of_memory_errors()->obj_at_put(next, NULL);

    // use the message from the default error
    oop msg = java_lang_Throwable::message(default_err);
    assert(msg != NULL, "no message");
    java_lang_Throwable::set_message(exc, msg);

    // populate the stack trace and return it.
    java_lang_Throwable::fill_in_stack_trace_of_preallocated_backtrace(exc);
    return exc;
  }
}
Johan Kaving
źródło
Dobry post, zaakceptuję to jako odpowiedź na tydzień, aby dać mu lepszą widoczność przed powrotem do poprzedniej odpowiedzi.
Pacerier
W Javie 8 i nowszych przestrzeń Permanent Generation Space została całkowicie usunięta i nie można już określać wielkości pamięci alokacji sterty w Javie 8 i nowszych, ponieważ wprowadzono dynamiczne zarządzanie pamięcią metadanych klasy Metaspace. Byłoby miło, gdybyś mógł pokazać fragment kodu, który obsługuje również PermGen, i porównać go również z Metaspace.
Buhake Sindi
@BuhakeSindi - Nie rozumiem, co ma z tym wspólnego stałe pokolenie. Nowe obiekty nie są przydzielane w Generacji stałej, jak podajesz w swojej odpowiedzi. Nigdy nie wspominasz również o tym, że OutOfMemoryErrors są wstępnie przydzielone (co jest faktyczną odpowiedzią na pytanie).
Johan Kaving
1
Ok, mówię, że z Javy 8 alokacja obiektów jest obliczana dynamicznie, podczas gdy zanim została przydzielona na predefiniowanym obszarze sterty, być może jest ona wcześniej przydzielona wcześniej. Mimo że OOME jest wstępnie przydzielone, wykonuje się „obliczenia” w celu ustalenia, czy należy wyrzucić OOME (stąd dlaczego zamieszczam odwołanie do specyfikacji JLS).
Buhake Sindi
1
Rozmiar sterty Java jest tak samo wstępnie zdefiniowany w Javie 8, jak wcześniej. Permanent Generation było częścią stosu zawierającego metadane klasy, internowane łańcuchy i statykę klasy. Miał ograniczony rozmiar, który musiał być dostrojony osobno od całkowitego rozmiaru sterty. W Javie 8 metadane klasy zostały przeniesione do pamięci natywnej, a wewnętrzne łańcuchy i staty klas zostały przeniesione do zwykłej sterty Java (patrz np. Tutaj: infoq.com/articles/Java-PERMGEN-Removed ).
Johan Kaving