Zagadka z Java Memory
Ostatnia zagadka okazała się bardzo łatwa. Zatem dziś pora na nieco trudniejszą i ciekawszą.
Oto kod zagadki.
public class JVMPuzzle { private final int dataSize = (int) (Runtime.getRuntime().maxMemory() * 0.6); public void go() { { byte[] bytes1 = new byte[dataSize]; } // for (int i = 0; i < 3; i++) { // System.out.println("Barista prosi: JVM zwolnij pamięć!"); // } byte[] bytes2 = new byte[dataSize]; } public static void main(String[] args) { new JVMPuzzle().go(); } }
Po skompilowaniu i uruchomieniu powyższego kodu otrzymujemy java.lang.OutOfMemoryError
:
<barista@javaczyherbata.pl> java JVMPuzzle Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at JVMPuzzle.go(JVMPuzzle.java:14) at JVMPuzzle.main(JVMPuzzle.java:18)
Natomiast jeśli odkomentujemy linie 10-12, skompilujemy i uruchomimy program ponownie, to błąd braku pamięci się nie pojawi.
Jak to jest, że JVM słucha się Baristy? 🙂
Generalnie działający kod można uprościć do poniższej postaci, więc pierwsze, błędne podejrzenie, że to Barista czaruje i nakazuje JVM poprawne działanie są wyssane z palca :p
Tak jak w poprzedniej zagadce spójrzmy na zdekompilowany kod metody
go()
. Najpierw bez deklaracji zmienneji
po anonimowym bloku:Widzimy, że instrukcje 0-6 odpowiadają za blok anonimowy czyli w naszym przypadku alokację tablicy bajtów i zapis referencji na stosie na pozycji 1. Instrukcje 7-13 odpowiadają z kolei za alokację tablicy bajtów i zapis nowej referencji na stos, także na pozycję 1. W momencie alokacji (instrukcja 11) na stosie istnieje jednak stara referencja do zmiennej
bytes1
, co uniemożliwia mechanizmowigc
zwolnienie pamięci (mimo, iż zmienna zadeklarowana jest w bloku anonimowym).Spójrzmy co się stanie po dodani deklaracji jakiejś zmiennej po bloku anonimowym:
Linie 7 i 8 powinny dać odpowiedź. Odpowiadają one za załadowanie stałej dla zmiennej
i
i odłożenie jej wartości na stosie na tej samej pozycji gdzie zapisana jest referencja do tablicybytes1
. Alokacja zmienneji
powoduje więc nadpisanie referencji do starej tablicy bajtów. Dzięki temu instrukcja w linii 13 powoduje uruchomieniegc
, zwolnienie pamięci (bo nie istnieje już silna referencja dobytes1
) i alokację nowej tablicybytes2
.Próbujesz stworzyć dwie tablice o sumarycznej wielkości 1.2 zarezerwowana pamięć? A
GC
nie ma z jakiegoś powodu możliwości wstrzelenia się między kończący się blok (i zniknięcie zmiennejbytes1
ze scopea) a tworzenie nowej tablicy?ogolnie wystarczylo odkomentowac tylko linijke 10 i 12, efekt ten sam, tyle ze bez baristy 😉