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? 🙂

4 komentarze to “Zagadka z Java Memory”

  • dembol says:

    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 zmiennej i po anonimowym bloku:

    public void go();
      Code:
       0:	aload_0
       1:	getfield	#6; //Field dataSize:I
       4:	newarray byte
       6:	astore_1
       7:	aload_0
       8:	getfield	#6; //Field dataSize:I
       11:	newarray byte
       13:	astore_1
       14:	return
    

    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 mechanizmowi gc zwolnienie pamięci (mimo, iż zmienna zadeklarowana jest w bloku anonimowym).

    Spójrzmy co się stanie po dodani deklaracji jakiejś zmiennej po bloku anonimowym:

    public void go();
      Code:
       0:	aload_0
       1:	getfield	#6; //Field dataSize:I
       4:	newarray byte
       6:	astore_1
       7:	iconst_0
       8:	istore_1
       9:	aload_0
       10:	getfield	#6; //Field dataSize:I
       13:	newarray byte
       15:	astore_2
       16:	return
    

    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 tablicy bytes1. Alokacja zmiennej i powoduje więc nadpisanie referencji do starej tablicy bajtów. Dzięki temu instrukcja w linii 13 powoduje uruchomienie gc, zwolnienie pamięci (bo nie istnieje już silna referencja do bytes1) i alokację nowej tablicy bytes2.

  • Karol 'JavAgnostic' Ciba says:

    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 zmiennej bytes1 ze scopea) a tworzenie nowej tablicy?

  • wolf says:

    ogolnie wystarczylo odkomentowac tylko linijke 10 i 12, efekt ten sam, tyle ze bez baristy 😉

Leave a Reply