sobota, marca 11, 2023

Chciałem popsuć G1 i mi się na razie nie udało ;-)

GC w Java'ie w większości przypadków opiera się na hipotezie, która najlepiej wyraża się słowami: obiekty umierają młodo.

Niby to nie jest ważne, ale jak się dowiadujemy o GC to jest mowa o tym, że obiekty żyją sobie tam gdzieś na pastwisku sterty i wiodą sobie swój żywot tak długo jak nie przyjdzie GC i po stwierdzeniu, że obiekt nie jest już nikomu potrzebny to go morduje i zwalnia pamięć.
To ma oczywiście konsekwencje w tym, że jak już zamorduje to mamy trochę wolnej pamięci, ale przed i za może być obiekt który nadal żyje... co prowadzi do fragmentacji sterty i trzeba tu sprzątać, bo może się okazać, że mamy wystarczająco wolnego miejsca, ale w kawałkach i nie da się nigdzie wcisnąć naszego obiektu... Czyli trzeba jakoś zdefragmentować stertę, ale to jest trudne bo to taki puzzel.

Stąd używając tej hipotezy, że przytłaczająca większość obiektów umiera młodo można podzielić pamięć tak by mieć miejsce gdzie obiekty będą szybko tworzone i szybko będą mogły umierać, a ich zwłoki nie będą nikomu przeszkadzać i ich usunięcie nie będzie zbytnio zużywało CPU, te które przeżyją można już wypuścić na pastwisko i niech się tam pasą i w razie umrą to będzie to na tyle rzadkie, że ich ewakuacja nie będzie taka straszna i będzie rzadka.

Stąd większość kolektorów w Java'ie to Generational Garbage Collectors (chyba najlepszym polskim tłumaczeniem by było pokoleniowe GC ;-)).
I pamięć dzielą mniej więcej tak:
  • eden - obiekty są tu tworzone (prawie wszystkie, poza tymi które wydają się być za dużo, co znaczy chyba 1/4 rozmiaru edenu
  • survivor 1 i survivor 2
  • old gen
Oczywiście przyszedł G1 dawno temu i trochę sprawę popsuł. Np. złośliwa bestia nie dziedziczy po GenCollectedHeap ;-) a wprost po CollectedHeap co by mogło sugerować, że nie jest generacyjny (pokoleniowy), a jest, najwyraźniej po prostu metody z GenCollectedHeap nie pasowały bo w G1 nie ma ciągłych obszarów pamięci dla każdej z generacji...

Ale nadal wszystko opiera się o generacje, tylko, że w G1 są one takie jakby poszarpane... 

Ale nadal jest tak, że obiekt (jeśli jest mały) tworzony jest w edenie, tworzony jest zaraz po poprzednim (OK, może być z wyrównaniem bo CPU mają taki zwyczaj, że zwykle szybciej się potrafią dostać do pamięci która jest pod adresem będącym wielokrotnością jakiejś tam liczby będącej potęgą 2) i przez to jak w edenie się kończy miejsce to można zrobić bardzo prostą i tanią operację i przejść przez wszystkie obiekty, sprawdzić które są jeszcze używane i które już nie i coś z nimi zrobić.
To coś to skopiowanie tych, które nadal żyją do aktualnego survivor (są 2) i nie kopiowanie tych, których już nikt nie używa. Po czym możemy znów zacząć wrzucać do edenu nowe obiekty. Tak obiekty w Edenie chodzą i leżą na zwłokach swoich poprzedników ;-)
Z survivor jest tak, że jak się kończy w nim miejsce to robimy ten sam numer co z edenem tylko przepisujemy te nadal żywe obiekty do drugiego surviovora i je zamieniamy (czyli jak byliśmy w 1, skończył się to przepisujemy do 2, i piszemy nowe do 2, jak się skończy to kopiujemy to 1...).
Oczywiście jest problem bo nagle będzie za dużo rzeczy w survivor... więc stąd też odbywa się promocja do old ;-)
Ona się odbywa głównie "za zasługi", czyli obiekt przeżył ileś tam przepisań z s1 do s2 i z powrotem i wszystko wskazuje, że to jeden z obiektów matuzalemów więc trzeba go wysłać do old.
Jest jeszcze taki deal, że jak survivor jest przepisywany to najpierw sprawdzane jest czy w old jest jeszcze miejsce i jeśli nie to odbywa się ta paskudna operacja defragmentacji sterty i jakby się nie udało odzyskać pamięci tyle by skopiować całego survivor to poleci OutOufMemoryError (to jest Error, nie Exception).
To w końcu prowadzi do najbardziej pożądanego kształtu wykresu pokazującego zużycie pamięci czyli do piły... jak jest trend wzrostowy to jest sugestia wycieku pamięci (na razie tylko sugestia).

W G1 jest to bardziej powariowane, ale z grubsza to tak działa...

Więc ja dziś, czy bardziej wczoraj chciałem pobawić się w G1 wielkością tych obszarów bo zazdroszczę moim developerem którzy pracują nad naszym softem, że właśnie się bawią GC (OK, oni tak na to nie patrzą, tu chyba trzeba wysublimowanego smaku, który potrafi docenić smak GC (podejrzewam, że to podobne jest do takiego kiszonego śledzia czy czegoś podobnego)) i z opisów moja hipoteza jest, że ponieważ do pamięci trafia multum rzeczy, która jest w protobuf to po sparsowaniu tablice char[] które tam są się pałętają po pamięci i przez to GC ma czasem problemy.
Próbowałem zasymulować coś takiego.. nawet by CPU bardziej użyć dodałem sortowanie bąbelkowe (czyli żeby CPU więcej był obciążony) i chciałem drania (G1) popsuć, ale na razie bez sukcesów ;-)
Robiłem to w Java 8 (bo Scala...) i pierwsza lekcja z dziś, Java 8 nadal używała -server żeby odpalić serwerową Java'ę ;-)

W ogóle dziś mi coś dziwnie szło, np. apka obserwowana z JVisualVM nie używała więcej niż 30% CPU.... co przekładało się na jakieś 300% w ActivityMonitor co by znaczyło jakieś 3 rdzenie z 12 (OK, tak naprawdę jest 6 rdzeni, ale jest HyperThread)... a jak odpaliłem z -server to leciało na 12 rdzeniach... ale później zacząłem sprawdzać i jednak jest tak, że -server jest mniej nice niż było -client i od razu próbuje zagarnąć całe CPU, a client tak bardziej subtelnie to robi ;-) [nice jest w *nix i im aplikacja bardziej nice to tym chętniej oddaje CPU, im mniej nice tym mniej chętnie to robi ;-)]
Ustawiałem też np. -Xmx czyli max sterty i mi to ignorował jak przekazywałem do java'y w cmd line, ale jak wrzuciłem do _JAVA_OPTIONS to działało...

Wnioski na dziś takie:
  • zrobienie syntetycznego psuja do GC jest trudne,
  • wiedza nie używana wyparowuje... 8 miesięcy pisania głównie w Pythonie i robienia za Engineering Managera (przy okazji Product Ownera i coś pokroju Data Analyst) i robienie jakichś zadanek na LeetCode prowadzi do atrofii Java'y ;-) stąd trzeba zacząć sobie pisać jakiś projekt, w którym będę mógł trenować (btw. nawet teraz nie pamiętam jak zrobić by testy z lokalnym DynamoDb działały na każdym OSie... a kiedyś to zrobiłem w parę godzin ;-))
  • trzeba znów zacząć czytać kod JVM
  • _JAVA_OPTIONS zawsze działa ;-)



Podobne postybeta
Samochód jako zmniejszacz temperatury.... GC i jak to możliwe, że Young Generation może być zbyt duże, strzeż się finalize() i muzyczka :-) Czyli potok świadomości....
Nieznane skarby JDK - JConsole :-)
finalize() - do czego służy, a do czego nie i z czym to się je.
Referencje w Java'ie
Odrobina miłości i serwery działają ;-)

Brak komentarzy:

Prześlij komentarz