niedziela, 23 czerwca 2013

Zarządzanie pamięcią w Javie oraz wykrywanie wycieków pamięci

Niecałe 2 tygodnie temu miałem przyjemność poprowadzić prezentację na Wrocław Java User Group poświęconą zarządzaniu pamięcią w Javie oraz wykrywaniu wycieków pamięci za pomocą narzędzia Eclipse Memory Analyzer. Obiecałem kilku osobom, że udostępnię materiały z prezentacji.

Oto one:
Slajdy
jar z kodem źródłowym

Dla osób, które nie uczestniczyły w wykładzie, przygotowałem krótkie wprowadzenie do studium przypadku.

Aplikacja odbiera dane od dostawców danych (klasa DataProvider) w postaci obiektów klasy StoredObject. Następnie zlicza ile razy wystąpił dany obiekt oraz przekazuje dane do klasy FileSaver, która służy do zapisywania wyników na dysku (na potrzeby prezentacji klasa FileSaver nie robi nic).

Punktem startowym programu jest klasa HelloLeak.

Aplikacja w pętli wykonuje kolejno następujące kroki:

1) Wyświetla informację o rozpoczęciu nowej iteracji
2) Odbiera partię 100 000 losowo wygenerowanych obiektów klasy StoredObject od pierwszego dostawcy danych i zapisuje je w pierwszej liście
3) Wrzuca odebrane obiekty do HashMapy (kluczem jest StoredObject, wartością liczba wystąpień obiektu w partii danych)
4) Odbiera 100 000 obiektów od drugiego dostawcy danych i zapisuje je w drugiej liście
5) Wrzuca odebrane obiekty do tej samej HashMapy, co w punkcie 3)
6) Iteruje najpierw po pierwszej, potem po drugiej liście, wyciągając z mapy obiekt i zapisując do pliku ilość jego wystąpień.

Jednak aplikacja zawiera błąd który powoduje wycieki pamięci. Możemy się o tym przekonać, uruchamiając dołączonego jara (polecenie java -Xms64M -Xmx64M -jar helloLeak.jar). Ustawiając parametry Xms i Xmx ograniczamy rozmiar sterty, co spowoduje, że naszej aplikacji szybciej skończy się pamięć i pojawi się OutOfMemoryError.

Natomiast, gdy uruchomimy aplikację z dodatkowym parametrem
-XX:+HeapDumpOnOutOfMemoryError
stan sterty przed wystąpieniem błędu zostanie zrzucony do pliku. Ścieżkę do pliku możemy ustawić za pomocą polecenia -XX:HeapDumpPath=C:\sciezka.
Np. uruchamiając aplikację poleceniem
java -Xms64M -Xmx64M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:\heapDump -jar helloLeak.jar
stan naszej starty zostanie zapisany w folderze c:\heapDump.
Uwaga - folder musi wcześniej istnieć w systemie plików.

Za pomocą Elipse Memory Analyzer'a (którego można pobrać ze strony http://www.eclipse.org/mat/ w postaci aplikacji RCP lub jako wtyczkę z Eclipse Marketplace) otwieramy wygenerowany plik hprof (File->Open Heap Dump ... z głównego menu. W przypadku braku opcji w menu, należy otworzyć perspektywę MemoryAnalysis).

Aby zobaczyć, co nam zjadło pamięć, należy nacisnąć przycisk Open Dominator Tree (screen poniżej).
Korzeniami drzewa są Garbage Collactor Rooty. Dominator tree przedstawia procentowy udział użycia pamięci przez obiekty w danej gałęzi drzewa. Wchodząc głębiej w najbardziej pamięciożerną gałąź drzewa widzimy, że HashMapa zajmuje ponad 85% pamięci. W wdidoku Inspector widzimy, że nasza HashMapa zawiera ponad 2 miliony obiektów (screen poniżej).
Jak to możliwe, skoro W każdej iteracji nasza HashMapa przechowywałą maksymalnie 200 tys. obiektów, a po każdej iteracji powinna być pusta, gdyż przekazując dane do FileSaver'a usuwamy wpis z HashMapy? Okazuje się, odpowiedź na to pytanie tkwi w błędnie zaimplementowanej metodzie hashCode klasy StoredObject. Nie spełnia ona kontraktu z metodą equals. Nie jest ona deterministyczna, ponieważ korzysta z pola statycznego. Umieszczając obiekt w HashMapie trafia ona do innego kubełka niż wtedy, gdy jest szukana podczas wyciągania obiektu z HashMapy (w międzyczasie pole statyczne klasy StoredObject zmienia wartość).

Nie musimy czekać, aż pojawi się OutOfMemoryError, aby podglądnąć zawartośc sterty. Za pomocą Eclypse Memory Analyzera możemy także sami wygenerować zrzut sterty (z menu wybieramy File->Acquire Heap Dump ... i wskazujemy odpowiedni proces javovy z listy).
Eclipse Memoy Analyzer ma także masę innych opcji i widoków, z którymi zachęcam się zapoznać w ramach ćwiczeń.

2 komentarze:

  1. Dzięki za slajdy! Jest jeszcze jedno ciekawe zjawisko związane z kompresją wskaźników: przy zwiększaniu sterty w pewnym momencie JVM musi zrezygnować z kompresji, znacząco zwiększając zapotrzebowanie na pamięć. Jeśli dobrze pamiętam, wzrost z 32 do czegokolwiek poniżej 40 GiB skutkuje efektywnym spadkiem dostępnej pamięci. Szczegóły: http://www.infoq.com/presentations/JVM-Performance-Tuning-twitter (znakomita prezentacja).

    OdpowiedzUsuń