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
public class JVMPuzzle { private final int dataSize = (int) (Runtime.getRuntime().maxMemory() * 0.6); public void go() { { byte[] bytes1 = new byte[dataSize]; } int i = 0; byte[] bytes2 = new byte[dataSize]; } public static void main(String[] args) { new JVMPuzzle().go(); } }Tak jak w poprzedniej zagadce spójrzmy na zdekompilowany kod metody
go(). Najpierw bez deklaracji zmiennejipo 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 mechanizmowigczwolnienie 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
ii odłożenie jej wartości na stosie na tej samej pozycji gdzie zapisana jest referencja do tablicybytes1. Alokacja zmiennejipowoduje 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
GCnie ma z jakiegoś powodu możliwości wstrzelenia się między kończący się blok (i zniknięcie zmiennejbytes1ze scopea) a tworzenie nowej tablicy?ogolnie wystarczylo odkomentowac tylko linijke 10 i 12, efekt ten sam, tyle ze bez baristy 😉