Kompilacja kodu w Javie

Ile razy kod Javy trzeba kompilować, aby optymalny…

Wpis ten jest wstępem do kolejnych wpisów, więc może być dosyć ogólny, względnie nudny dla przeciętnego Senior Tech JVM Performance Leada 😉

OpenJDK/OracleJDK

Największą częścią rynku (91%) są dystrybucje JVMa z rodziny OpenJDK (z OracleJDK włącznie). Dystrybucje te bazują na tym samym kodzie źródłowym, zatem można je spokojnie omawiać wspólnie.

Kompilacja plików .java

Chronologicznie należałoby zacząć od kodu źródłowego zapisanego w plikach.java.

Kod kompilowany jest do standardowo bytecode‚u i ta kompilacja jest wykonywana przed uruchomieniem programu. Spokojnie tę kompilację można nazwać pierwszą kompilacją i – bynajmniej – nie jest ona optymalna 😉 Wszelkie metody optymalizacji są tutaj bardzo ograniczone. Można zrobić jakieś Constant Folding (o którym pisałem już chociażby we wpisach o finalach), jednak nie jest to szczyt technik optymalizacyjnych.

Kod kompilowany do bytecode‚u kompiluje się jednak znacznie szybciej, niż kompilowany do kodu natywnego. Jest też bardziej elastyczny. Czyli do developmentu „good enough”.

JIT

Kompilacja just-in-time (swobodne tłumaczenie to „rychło w czas”) polega na kompilowaniu kodu w czasie działania aplikacji.
Jakby się głębiej zastanowić, to widać podobieństwa do samolotu w tejże reklamie.

Założenie jest takie, że większość kodu jest wykonywana na tyle rzadko, że można ją po prostu wykonać w trybie interpretowanym. Szkoda zatem cykli procesora na szalone optymalizacje fragmentów kodu, wykonywanych jednokrotnie. Warto się jednak skupić na metodach wykonywanych tysiące razy.

Co więcej, skoro mamy już pewien narzut na interpretację kodu bajtowego, to do czasu porządnej optymalizacji warto poświęcić procesor i zbierać dodatkowe statystyki. Mogą one być bardzo przydatne – jeśli w ciągu przykładowo 1000 wykonań program ani razu nie wszedł w jakiegoś ifka, to prawdopodobnie przy 1001 wywołaniu również nie wejdzie. Można przykładowo pominąć treść sekcji, którego ten ifek dotyczy (zaoszczędzimy bajtów i nie tylko, acz o tym kiedy indziej). W razie czego, gdyby przez przypadek ifek stał się prawdziwy, można przykładowo odesłać do wykonywania źródeł w bajtkodzie (to tylko przykładowy przykład; trochę inaczej się to robi w realu).

C1, C2

Dawno temi, w zamierzchłych czasach Javy 1.6 istniały dwa osobne kompilatory C1 (szybki, acz niedokładny – kliencki) oraz C2 (wolny, ale zaawansowany – serwerowy/opto). Na starcie wybierało się pożądany kompilator flagą lub JVM sam nam go wybierał bazując na parametrach naszego sprzętu.

W Javie 1.7 została wprowadzona tzn. kompilacja warstwowa. Według niej interpretujemy kod. Po przekroczeniu progu 2 000 wywołań kompilujemy metodę z użyciem C1. Jednak życie toczy się dalej, a metoda dalej jest wywoływana. Po wywoływaniu metody 15 000 razy jest ona kompilowana z użyciem C2.

Jest jednak kilka „ale”.

Wspomniana kompilacja warstwowa ma ponumerowane warstwy. Poziom 0 to nieskompilowana metoda w trybie interpretowanym. Poziom 4 to C2. Za to za warstwy 1-3 odpowiada C1, która ma różne „warianty”. Istnieje wariant z pełnym profilowaniem (3), ale też istnieje lżejsza wersja ze ograniczonym profilowaniem(2) i najlżejsza – bez profilowania (1).

W idealnej sytuacji kompilujemy metodę do warstwy 3 (po 2 000 wykonaniach), a następnie do warstwy 4 (po 15 000 wykonaniach). Jednak nie zawsze tak jest. Trzeba mieć świadomość, że po przekroczeniu tych progów metoda jest wrzucana do kolejki metod do kompilacji. Przykładowo czasem kolejka do kompilatora C2 jest na tyle długa, że może do czasu zwolnienia C2 można ograniczyć profilowanie (przejść z 3 warstwy na 2). Jak głoszą slajdy Oracle’owe, trzeci poziom jest 30% wolniejszy niż drugi.

AoT?

Jak wiemy istnieje też GraalVM i kompilator Graal. Jest to jednak rozwiązanie typowo Oracle’owe, więc w tym wpisie nie będę rozwijał tego tematu.

Są jednak dwie ciekawostki.
Pierwsza jest taka, że jeśli macie ochotę napisać własny kompilator, to nie ma sprawy, OpenJDK (od Javy 9) wesprze Cię w tym wystawiając interfejs,który trzeba zaimplementować pisząc kompilator. Proste 5 metodek 😉

Druga ciekawostka jest mniej znana – również w Javie 9 powstał eksperymentalnie kompilator AoT. Pozwala on kompilować program do kodu natywnego. Istnieje jednak jeszcze drugi tryb kompilacji – kompilowanie do kodu natywnego z profilowaniem. Taka opcja pozwala na rekompilację z użyciem C2 i dodatkowych metadanych zbieranych w czasie działania programu. W założeniu ten tryb miał przyspieszyć włączanie projektu, jednak benchmarki powiadają, że tak się nie dzieje…

Podsumowanie

O ile trochę wiemy co się dzieje, to nie wiemy w jaki sposób. Zatem w kolejnych wpisach napiszę nieco o tym co się kompiluje, kiedy, jak oraz skąd to wszystko mamy wiedzieć…

Z ciekawych linków zostawiam tylko wpis na blogu Microsoftu o AoT i nie tylko.

Gwiazdkujcie, komentujcie i bywajcie zdrów 😉

Pax et Bonum

Oceń wpis

Prywatny projekt w tydzień na produkcji

Czy da się w tydzień wypuścić produkcję MVP własnego prostego projektu robionego po godzinach pracy? Challenge accepted!

Btw. nie martwcie się, że to drugi wpis z rzędu o projektach, zamiast o low level Javie. Nie mam zamiaru zmieniać profilu bloga. Po prostu poświęciłem dużo czasu na ten projekt i szkoda byłoby tego też nie opisać. A w moim mniemaniu wyszło całkiem spoko.

Prolog

Do indeksowania wybranego fragmentu internetu już się wcześniej zabierałem. Moim targetem były audycje radiowe – linki do nich oraz metadane, żeby mieć po czym szukać audycji.

Podobny potencjał dostrzegłem i w zupełnie innej sferze. Pojawiało się na bieżąco mnóstwo contentu, który warto by zaindeksować, aby łatwiej sobie tego pożądanego fragmentu poszukać. Toteż spytałem na jednej grupie Facebookowej, czy taka koncepcja jest sensowna i potencjalnie przydatna. Pomysł spotkał się z dużym entuzjazmem, zatem nic tylko wziąć się do roboty.

Charakterystyka projektu jest mocno nacelowana na czas – im szybciej tym szybciej. Zatem nie ma czasu na rzeźbienie, trzeba korzystać z gotowców, dowozić największy efekt najmniejszym kosztem. Bugi? Na później. Pracochłonny feature? Na koniec backlogu. Słowem – czyste naparzanie kodu 🙂

Technologie

Czas nagli, więc nie będę tworzył CRUDów od zera, stawiał serwera, zbędnie konfigurował itd. Zdarzyło mi się wcześniej korzystać z JHipstera, zatem stwierdziłem, że i w tym przypadku będzie to najlepsze narzędzie.

JHipster to w skrócie generator aplikacji opartych na Spring Boot. Tworzy on CRUDy razem z widokami. Tworzy kontrolery, opcjonalne serwisy, mappery oraz repozytoria Spring Data. Utrwalanie danych możliwe przez wykorzystanie relacyjnej bazy danych (u mnie Postgres, na dev jest H2), choć dostępne są również różne cache oraz NoSQLe, jednak z nich nie korzystałem.

Dostępne technologie front-endowe to Angular, Vue oraz React. Wygląd oparty na Bootstrap 4.

Czas zacząć robić tę robotę…

Na początku trzeba zdefiniować model – encje oraz relacje między nimi. Istnieje do tego ładny edytor webowy JDL-Studio. Można też skorzystać z innych narzędzi do definicji modelu, jednak wówczas nie miałem dostępu do kompa, a na tablecie ładnie dało radę wszystko wyklikać. W godzinę model był gotowy.

I tutaj pierwsze zaskoczenie – po zapisaniu projektu gdzieś w chmurze w tym JDL-Studio można go ładnie poprosić, żeby zaaplikował wygenerowany projekt do wybranego repozytorium na githubie poprzez stworzenie Pull Requesta. Zatem jak uzyskałem dostęp do kompa, można było zrobić git pull i zacząć naparzać kod!

Budowanie projektu to wykonanie ./mvnw. Rzecz jasna trzeba odczekać, aż Maven zaciągnie się sam, a potem zaciągnie pół internetu. Zależności front-endowe również są zaciągane automagicznie. Po jakimś czasie widzimy uruchomiony projekt w wersji developerskiej.

Development

Wygenerowane CRUDy wyglądają dość topornie i trzeba je znacznie podrasować (szczególnie formularz new/edit). Jakkolwiek style, layouty już istnieją predefiniowane i wyglądają nie najgorzej, zatem przynajmniej coś.

Fajne jest to, że do developerki JHipster generuje wstępnie dane użytkowników oraz losowe dane dla naszych encji – nie trzeba tyle klikać. Za to mvn clean czyści również całą bazę H2.

Gdyby ten projekt nie był „na szybkości”, to pewnie spodobało by mi się, że w ramach generowania kodu generowane są testy. I to nie tylko jednostkowe, ale też integracyjne, end to end, a nawet (co mnie zdziwiło) testy architektury aplikacji.

JHipster jednak nie tylko generuje model i widoki. Generuje również całą przydatną otoczkę aplikacyjną. Można wymienić rejestrację, autoryzację, uwierzytelnianie, metryki, zarządzanie loggerami, użytkownikami. Audyt – kto kiedy się logował, healthchecki, podgląd konfiguracji to również przyjemna sprawa. Wszystko to dostępne od ręki dla admina. Zwykły użytkownik może domyślnie tylko zarządzać encjami biznesowymi. Niezalogowany może się co najwyżej zarejestrować lub zalogować (zabezpieczone bodajże przez Spring Security).

Na produkcję z tym!

Wymagana konfiguracja produkcyjna projektu jest minimalna – u mnie ograniczyło się to do konfiguracji skrzynki pocztowej, żeby projekt mógł wysyłać maile do rejestracji. Zatem projekt jest prawie od razu gotowy na produkcje.

Na produkcję, czyli gdzie? Otóż JHipster również i tutaj ma wsparcie dla różnych rozwiązań: AWS, Azure, GCP, Kubernetes, Heroku oraz Openshift.

Osobiście wybrałem Heroku, bo tanio – domyślnie za free możesz testowo postawić Proof of Concept i postawić Postgresa do 10 000 wierszy.

Jednak z darmowym planem jest o tyle problem, że gdy aplikacja nie jest używana to ją ubijają. Za to za 7 $ miesięcznie będzie raz dziennie restartowana, lecz będzie działać cały czas. Limit pamięci – 512MB; metryki (nieco inne niż w JHipster) też ograniczone – do ostatniej doby. Deployment oczywiście z downtime (w przypadku jednej instancji serwisu).

Wrzucenie tego na produkcję z JHipsterem to bajka! Wystarczyło zainstalować integrację do heroku, wpisać jhipster heroku, wybrać wersję Javy i po kilku minutach nowa wersja jest na prodzie! A byłoby jeszcze szybciej, ale testy się „muszą” odpalić 😉

Ostatnie szlify

Nową apkę warto postawić pod jakąś bardziej ambitną domeną niż domyślna: https://<nazwaAplikacji>.herokuapp.com/. Osobiście kupiłem na OVH domenę za 7 zł na rok, więc to też nie jest jakiś super koszt. Domena gotowa do użycia w jeden dzień – tylko skonfigurować.

Warto wspomnieć, że Heroku samo podpina certyfikat od Let’s Encrypt za free i konfiguruje, więc HTTPSa mamy również zerowym kosztem.

Przyda się również pamiętać, żeby zmienić hasło z admin:admin i user:user na jakieś trudniejsze 🙂

Na szybkości zrobiłem też testy wydajnościowe i aplikacja klęka przy mniej więcej 20 req/s.

Epilog

Warto by powiedzieć co tak naprawdę było przedmiotem projektu 😉

Otóż jest taki kandydat na prezydenta, który bardzo mocno poszedł w internety. Codziennie przynajmniej jeden live z odpowiedziami na pytania ludzi. Jednak trudno się odnajduje potem te fragmenty, kiedy jaki temat był na tapecie.

Zatem poindeksowałem, co usłyszałem na 30 filmikach na Youtube, przeczytałem na 42 stronach pdfu z programem/wizją oraz na 8 z 13 stron www z programem w internetach.

Wyszło jakieś 200-300 przeróżnych tematów.
Co by nie mówić, to contentu jest naprawdę sporo… U innych kandydatów roboty byłoby mniej (a na jeszcze innych na program trzeba czekać do 4 dnia przed wyborami, więc bym nie zdążył).

Cały projekt, jeśli chcecie zobaczyć, dostępny na tej oto podlinkowanej stronie.

To by było na tyle. Mam nadzieję, że apka podziała jeszcze 3 tygodnie, a potem będzie można zaorać 😉

A potem wreszcie wrócić do pisania o JVMie!

Oceń wpis

Lambda dla ubogich

Z mojego doświadczenia wynika, że większość programistów posiada jakieś swoje własne projekciki na boku. A to jakiś analizator giełdy, a to jakaś apka do ogarniania smogu, a to parsowanie rozkładów jazdy pociągów, względnie sterowanie żarówkami we własnym smart domu.

Takie projekciki charakteryzują się myślą przewodnią: byle szybko zakodzić i byle działało. Z założenia mogą działać tylko w optymistycznych flow i nie posiadać żadnych testów. Mają być radosnym programowanie i sprawdzeniem się, czy jeszcze potrafimy coś zaprogramować w innych „niepracowych” technologiach.

Takimi też przesłankami kierowałem się tworząc swój mini projekcik o roboczej nazwie:

Lambda dla ubogich

Potrzeba była następująca: posiadam wirtualkę w cloudzie i chciałbym posiadać na niej dużo swoich aplikacji, jednak mam tylko 1GB pamięci i 8GB miejsca na dysku… Bardziej problematyczne jest pierwsze ograniczenie… A Java – jak to Java – zjada tyle pamięci, ile się jej przydzieli.

Można oczywiście skorzystać z Graala, skorzystać z Micronauta/Quarkusa i skompilować projekt do kodu natywnego. Problem pojawia się, gdy dziergamy nasze tajne projekty w Spring Boot od kilku lat i jest za dużo kodu do przepisania.

Zatem gdyby tylko istniało jakieś rozwiązanie, które pozwala na uruchamianie naszych serwisów, gdy są potrzebne, a gdy nie są potrzebne, to je ubija. I gdyby jeszcze było w miarę lekkie, nieobciążające zbytnio pamięci ani procesora. Takie Heroku tylko na VPSie.

Trochę poszukałem w internecie takie rozwiązania i oczywiście nie znalazłem 😛

No dobra, gdybym szukał trochę dłużej, to bym znalazł. Jednak taki własny projekt zawsze czegoś uczy, domena jest inna niż ta w pracy, więc może warto spróbować samemu coś takiego napisać…

Development

Z założenia rozwiązanie miało być przenaszalne, crossplatformowe oraz w miarę proste, zatem wybór padł na Dockera, jako sposób zarządzania aplikacjami. Serwerem proxy jest Nginx (wcześniej już chciałem go nieco poznać). Jednak jak połączyć te dwa światy, żeby za proxy była warstwa zarządzająca kontenerami Dockera?

Okazuje się, że istnieje takie rozwiązanie jak OpenResty, który rozszerza Nginx o możliwość wykonywania skryptów (w języku Pascalo-podobnym o nazwie Lua). Zawsze nowy język do CV 😀 Warto mieć jakieś Api dockerowe i panel administracyjny zatem pożeniłem to wszystko z Portainer.

Zatem poświęciłem kilka wieczorów i oto jest: lambda-for-vps. Jeżli ktoś chciałby spróbować, jak działa, wystarczy zklonować repo i uruchomić:

docker-compose -f docker-compose.yml up

Domyślnie uruchamia się projekt z przykładowym hello-world, który jest dostępny na http://localhost/hello-world.

Działanie

Co nieco o działaniu jest opisane na githubie, jednak po angielsku. Po polsku to w skrócie:

  1. Po uderzeniu na endpoint /hello-world za pomocą Portainer sprawdzany jest stan aplikacji (określonej za pomocą docker-compose.yml).
  2. Jeśli kompozycja nie istnieje, to wszystkie kontenery są automagicznie zaciągane.
  3. Jeśli kompozycja jest nieżywa, wówczas uruchamia całą kompozycję. Po uruchomieniu kontenerów istnieje opcja czekania określonego czasu od wstania kompozycji.
  4. Skoro wszystko działa, to request jest obsługiwany.
  5. Następnie zlecamy ubicie kompozycji o ile nie będzie w między czasie nowych żądań. Innymi słowy kompozycja jest ubijana po podanej liczbie sekund od ostatniego requesta.

Prawdopodobnie projekt nie jest doskonały. Jednak posiada kilka fajnych cech:

  • wg docker images całość waży 181 MB,
  • wg docker stats obydwa kontenery zużywają 12 MB pamięci i szczątkową część procesora,
  • jest w nowym nieznanym mi języku Lua,
  • konfiguracja w popularnym Nginx,
  • zarządza apkami trzymanymi w Dockerze.

zatem jak na mały projekt jest przynajmniej Good enough.

Oceń wpis

Wpływ Garbage Collectorów na działanie JVMa – odczyty z pamięci

Poprzedni wpis był o tworzeniu obiektów w JVMie w zależności od wybranego garbage collectora. W tym wpisie skupię się na przedstawieniu wpływie wybranego gc na odczyt istniejących obiektów.

Benchmark

Przed uruchomieniem Benchmarku tworzona jest statyczna tablica. Następnie w bloku statycznym jest ona wypełniana przykładowymi danymi. Tworzone obiekty to wrappery na int.
Benchmark uruchamiany jest dla młodej oraz starej generacji (o ile istnieje taki podział). Aby wymusić przynależność do starej generacji JVM jest proszony o wykonanie GC kilkakrotnie.

    private static final IntWrapper[] WRAPPERS = new IntWrapper[100000];

    static {
        for (int i = 0; i < WRAPPERS.length; i++) {
            WRAPPERS[i] = new IntWrapper(i);
        }
        for (int i = 0; i < 20; i++) {
//            Runtime.getRuntime().gc();
        }
    }

Następnie w benchmarku zliczana jest suma pierwszych 2000 wartości we wspomnianych obiektach.

    @Benchmark
    public long method() {
        long sum = 0;
        for (int i = 0; i < 2000; i++){
            sum += WRAPPERS[i].getI();
        }
        return sum;
    }

UWAGA! Jak widać w powyższym benchmarku bazujemy jedynie na zmiennych lokalnych (czyt. trzymanych na stosie wątku). Jednocześnie w trakcie benchmarku nie tworzymy ani jednego nowego obiektu. Badamy jedynie wpływ gc na odczyty, a nie wydajność samego gc.

UWAGA 2! We wpisie zakładam, że czytelnik przeczytał poprzedni wpis, a także, że jego treść była w miarę zrozumiała. 😉

Epsilon GC

Na początek EpsilonGC, czyli brak działań. Zarówno wynik dla wariantu bez gc oraz z wymuszonym gc jest taki sam – około 1708 ns/op (wymuszanie gc nie ma w tym przypadku sensu i jest ignorowane, stąd te same wyniki).

W tym wpisie uznaję ten wynik uznać za „wzorcowy” i do niego będę porównywał inne rezultatu.

ParallelGC

W przypadku ParallelGC program działa nieprzerwanie aż do momentu braku miejsca na nowe obiekty (MinorGC) lub braku miejsca na promocję obiektów do starej generacji (FullGC). Wszystkie metadane niezbędne do gc wyliczane są w czasie STW bez dodatkowego narzutu na „runtime”. Zatem można spodziewać się wyniku analogicznego do EpsilonGC.

Tak też się dzieje w przypadku obiektów w młodej generacji – w tym przypadku wynik to 1703 ns/op .

W przypadku starej generacji mamy niespodziankę – wynik 1579 ns/op (szybciej o 8%). Z czego wynika szybsze wykonanie benchmarku?

Otóż przy kopiowaniu danych z młodej do starej generacji wykonywana jest analiza, która pozwala na uporządkowanie danych w bardziej logiczny sposób niż przy tworzeniu obiektów. O ile wcześniej elementy tablicy mogły być rozdzielone innymi obiektami (szczególnie, że statyczna inicjalizacja działa się przy ładowaniu klas), o tyle po wykonaniu gc elementy tablicy prawdopodobnie trzymane są razem.

ConcMarkSweepGC

W tym przypadku również młoda generacja wypada podobnie do EpsilonGc – 1694 ns/op. W przypadku starej generacji wynik to 1837 ns/op, czyli 7% wolniej. Jednak podobnie jak w poprzednim wpisie zostawię tę analizę historykom, skoro w OpenJDK 14 CMSa już nie uświadczymy 😉

G1GC

W przypadku G1Gc nie widać różnicy w porównaniu do EpsilonGc – 1701 ns/op oraz 1698 op/s. Prawdopodobnie wynika to z tego, że zarówno młoda generacja, jak i stara znajdują się w takiej samej liczbie segmentów, które niekoniecznie muszą być zlokalizowane blisko siebie. Aczkolwiek, to tylko domysł, który trudno zweryfikować…

O ile G1Gc korzysta z barier zapisu, o tyle z barier odczytu nie korzysta, stąd też brak dodatkowego narzutu na odczyt.

ZGC

Jak już wspominałem w poprzednim wpisie ZGC korzysta z barier odczytu. Wspomniałem również, że aktualnie nie ma podziału na młodą i starą generację.

Oba te fakty widoczne są w wynikach benchmarku – 1996 ns/op w wersji z gc oraz 2025 ns/op w wersji bez gc. Zatem w tym benchmarku narzut spowodowany korzystaniem z ZGC to 16%. Dość dużo, biorąc pod uwagę, że ten gc nie został ani razu uruchomiony w czasie benchmarkowania…

Shenandoah

O ile Shenandoah w OpenJDK 13 również nie posiada podziału o tyle wyniki dwóch benchmarków się znacząco różnią.

W przypadku braku wcześniejszych gc, wynik jest porównywalny do EpsilonGC (~1700 ns/op).

W drugim przypadku – gdy został wcześniej wymuszony gc – wynik był 4041 ns/op. To jest bardzo duży narzut – 136% więcej w porównaniu do EpsilonGC.

Taka różnica może wynikać z głównego założenia działania Shenandoah. Przed każdym obiektem zawsze znajduje się dodatkowy wskaźnik na ten obiekt, lub na nowe miejsce w pamięci, gdzie został ten obiekt przeniesiony. W pesymistycznym przypadku najpierw odczytujemy wartość wskaźnika (jednocześnie ładując do Cache’u procesora obiekt), po czym okazuje się, że załadowany obiekt nie jest tym, którego szukamy. Wówczas musimy pobrać obiekt z innej lokalizacji. Jednak wydaje się dość nieprawdopodobne, że taka sytuacja by była cały czas.

Trzeba pamiętać, że ten gc jest w wersji 13 eksperymentalny. Plany są takie, by ten gc mógł działać produkcyjnie w wersji 15. Zatem jest szansa, że ten przypadek będzie usprawniony.

Podsumowanie

Wybór Garbage Collectora może mieć wpływ nie tylko na samo czyszczenie pamięci, lecz również na działanie aplikacji. Garbage Collector może przyspieszyć wykonywanie kodu (przypadek ParallelGC) lub spowolnić (Shenandoah, ZGC).

Warto wspomnieć, że Shenandoah i ZGC wejdą produkcyjnie dopiero w OpenJDK 15 zatem można oczekiwać pewnych poprawek.

Standardowo ludziom z nadmiarem czasu polecam prezentacje na Youtube. Bardzo dobre podsumowanie wydajności wykorzystania procesora oraz pamięci – Sergey Kuksenko z Oracle. Ponadto, dla ludzi chcących zaznajomić się z Shenandoah polecam prezentację Alexey’a Shipilev’a.

Przy okazji życzę wszystkiego dobrego na święta Wielkanocne 😉

Oceń wpis

Ile kosztuje Garbage Collection – tworzenie obiektów

Garbage collector to taki stwór, który automagicznie przeczesując pamięć znajduje śmieci i odzyskuje miejsce przez nie zajmowane. Oczywiście za posprzątanie trzeba mu zapłacić.

Ile?

Odpowiedź standardowa – to zależy. Istnieje bowiem bardzo wiele garbage collectorów. Nie są one ustandaryzowane, więc w zależności od implementacji JVMa są dostępne różne implementacje garbage collectorów.

W tym wpisie skupię się na gckach dostępnych w AdoptOpenJDK (HotSpot) w wersji 13. Dostępne w tym JVMie są między innymi:

  • Epsilon GC (mój faworyt 🙂 )
  • Parallel GC
  • CMS (Concurrent Mark Sweep)
  • G1
  • ShenandoahGC
  • ZGC

Większość z nich to skomplikowane mechanizmy, jednak postaram się pokrótce opisać jak działają.

Hipoteza generacyjna

Bardzo ważnym pojęciem dla gcków jest tzw. hipoteza generacyjna. Zakłada ona, że obiekty bardzo często mają bardzo krótki cykl życia (śmiertelność niemowląt), zatem pamięć po takich obiektach można szybko odzyskać. Jednocześnie zakłada ona, że jeśli obiekt przeżył kilka cykli odśmiecania, to można założyć, że będzie żył bardzo długo.

Ta teoria przekłada się na praktykę: w wielu implementacjach gc jest podział na generację młodą oraz starą.

Minor GC

Młoda generacja składa się z dwóch fragmentów pamięci – Eden i Survivor. W Edenie umieszczane są zawsze nowo utworzone obiekty. Jeśli obiekt jest żywy i przetrwał cykl GC, to jest przenoszony do Survivor. Jeśli obiekt przetrwa kilka cykli GC, wówczas jest promowany do starszyzny (starej generacji).

Czyszczenie młodej generacji jest bardzo wydajne, dlatego robi się je oddzielnie od starej generacji. Takie czyszczenie nazywamy Minor Garbage Collection.

MinorGC Najczęściej korzysta z algorytmu Mark-Copy. W pierwszej fazie musimy się dowiedzieć, jakie obiekty są żyjące.
W pierwszym kroku oznaczamy wszystkie obiekty jako nieżywe. Następnie znajdujemy wszystkie obiekty, które z pewnością są żywe tzw. GCRooty (zmienne ze stosów wątków, zmienne statyczne i wiele wiele innych). Oznaczamy je jako żywe, a następnie wszystkie jego pola oznaczamy jako żywe oraz rekurencyjnie oznaczamy pola pól jako żywe.
W drugim kroku, gdy już wiemy, jakie obiekty są żywe, kopiujemy je do bezpiecznych przestrzeni Survivor. Jako, że wszystkie obiekty z Edenu są bezpiecznie przeniesione, to możemy wyczyścić cały Eden.

Oczywiście cały proces jest bardziej skomplikowany i zależy od implementacji, ale na potrzeby tego artykułu mam nadzieję, że ta teoria wystarczy. A teraz do kodu!

Benchmark

W tym benchmarku będziemy sprawdzać, ile obiektów zdążymy zaalokować w czasie sekundy:

    private static class IntWrapper {
        int i = 12;
    }

    @Benchmark
    public void method() {
        new IntWrapper();
    }

Trzeba oczywiście pamiętać o wyłączniu EscapeAnalysis, żeby być pewnym, że te obiekty się rzeczywiście tworzą. Stąd potrzebna flaga -XX:-DoEscapeAnalysis.

Kolejnym potrzebnym argumentem jest odpowiednia wielkość sterty. W tym przypadku użyłem 8GB pamięci.

Ostatnią sprawą o której trzeba pamiętać jest upewnienie się, że ta pamięć jest zarezerwowana dla JVMa. Normalnie, gdy JVM prosi Linuxa o pamięć, to Linux tę pamięć da tak tylko teoretycznie tzn. czasem Linux da pamięć dopiero w momencie zapisu, przez co czasem musi jej poszukać.
Opcja -XX:+AlwaysPreTouch zapewnia, że pamięć będzie od razu gotowa do użycia.

Epsilon GC

Jest to mój faworyt. Ten Garbage Collector nie robi nic do czasu, gdy zacznie kończyć się pamięć. Jeśli pamięć się skończy, to mówi: Nie ma więcej pamięci. I co mi pan zrobi? Czyli rzuca błędemOutOfMemoryError.

Pytanie: na co komu taki nieużyteczny gc? W zamyśle twórców (JEP 318) odpowiedzi jest wiele,a jedna z nich to – do testów wydajnościowych. I z tej cechy tego gc skorzystamy.

W czasie całego Benchmarku prawie całkowicie zapełnia się cały heap.

Wynik benchmarku: 332 942 227 ops/s (przepustowość)

Parallel GC

Dość prosty garbage collector, który jest zrównolegloną wersją Serial GC. Działanie jest następujące: kończy się miejsce, więc zatrzymujemy cały JVM i szukamy pamięci. Najpierw szukamy obiektów żywych – wszystkie żywe przenosimy w bezpieczne miejsce, a całą resztę pamięci oramy (i możemy potem z niej korzystać). To bardzo duże uproszczenie.

W tym GC pamięć dzielimy na stałe części – young to domyślnie 1/2 old. Survivor to 20% Eden. Zatem pamięci w Edenie zostaje jakieś 2GB. Zatem kilka razy Minor GC się odpali. Jednak charakterystyka tych danych – wszystkie obiekty od razu są nieużytkami – powoduje, że nie ma żadnych obiektów do przeszukania, więc Minor GC trwa po 2ms i bardzo nieznacznie wpływa całość przetwarzania.

Wynik: 329 558 528 ops/s (99% Epsilion GC)

Concurrent Mark Sweep

Parallel GC posiada jedną szczególną wadę – faza Stop The World przy dużych Heapach i pełnych kolekcjach (Young + Tenured) potrafi trwać co najmniej kilkaset milisekund. Przy zastosowaniach serwerowych taka pauza potrafi spowodować timeout żądania. Poszukiwano zatem rozwiązania, które skróci pauzy. Najstarszym jest właśnie Concurrent Mark Sweep.

Jednak w przypadku młodej generacji również jest używany analogiczny algorytm jak w przypadku ParallelGC.

Wyniki są zastanawiające, gdyż jest to średnio 183 989 107 operacji na sekundę (55% Epsilion GC), czyli znacznie mniej niż w poprzednich przypadkach. Czasy pauz dla MinorGC to 45ms, są też znacznie częściej.
I bardzo by mnie to zastanawiało, skąd ten stan rzeczy, jednak ten GC szczęśliwie przechodzi do historii począwszy od Javy 14 (JEP-363). Zostawmy historię historykom.

G1

Ten gc wprowadzony w Java 6, a domyślny od Javy 9 był rewolucyjny. Koncepcja zupełnie inna – pamięć jest podzielona na około 2048 fragmentów, z których każdy może być przypisany do jednego z typów (free, eden, survivor, old oraz humongous). Dzięki temu również gc może swobodnie zmieniać proporcje między young a old.

W tym gc głównym założeniem jest zmniejszanie czasu zatrzymania aplikacji. Osiągnięte jest to poprzez przesunięcie wykonywania niektórych operacji na czas działania systemu równolegle do działania aplikacji. Oczywiście to wymaga pewnego narzutu przy wykonywaniu operacji, dodatkowej pamięci i działania osobnych wątków GC.

Wspomniane dodatkowe operacje są wygenerowane przez JIT dla tego GC i zatem skompilowany kod jest większy.

Do rzeczy – tutaj ten prosty benchmark pozwala wykonanie 311 283 463 ops/s (93% Epsilion GC). Należy pamiętać, że oprócz tego wątku równolegle działają również wątki GC, więc rzeczywista przepustowość jest jeszcze mniejsza.

Shenandoah

Ten garbage collector mocno bazuje na G1, próbując usprawnić niektóre etapy tak, by nie wymagały pauzy. Założenia, są takie, że długość pauzy to maksymalnie 10 ms niezależnie od wielkości stosu. Maksymalna strata przepustowości to 15%. Trzeba również nadmienić, że wymaga nieco więcej pamięci, gdyż każdy obiekt musi mieć dłuższy nagłówek. O ile możliwy jest podział na młodą i starą generację, o tyle aktualnie nie jest on zaimplementowany.

Shenandoah został zaprojektowany i jest rozwijany od 2014 roku przez Red Hat, jednak możemy go używać dopiero od Javy 12. Niestety wersje JDK wypuszczane przez Oracle (OracleJDK) nie zawierają Shenandoah, dlatego zaznaczałem na wstępie, że korzystam z AdoptOpenJDK opartego na Hotspot w wersji 13.

Rezultat dla tego GC to 323 042 410 ops/s (97% Epsilion GC), co oznacza, że sama alokacja nowych obiektów jest naprawdę sprawna. Oczywiście trzeba pamiętać o większym narzucie na pamięć oraz na wątki gc pracujące w tle.

ZGC

Założenia ZGC są bardzo podobne do Shenandoah – 10ms, 15% uszczerbku w wydajności. Implementacja jednak znacznie się różni i podobno bliżej jej do C4 Azul Zing. ZGC pojawił się eksperymentalnie w Javie 11 dla Linux i jest aktywnie rozwijany przez zespół Oracle.

Korzysta przede wszystkim z kolorowych wskaźników oraz barier. Kolorowe wskaźniki to koncepcja ukrycia pewnych informacji wewnątrz wskaźnika. Chodzi o to, że 64bitami możemy zaadresować w pamięci jakiś milion terabajtów danych, gdy aktualnie adresujemy maksymalnie kilka terabajtów. Zatem najwyższe bity zawsze będą zerami; można by je zagospodarować i przechowywać w nich jakieś informacje. Tak też się dzieje. Kosztem możliwości obsługi maksymalnie heapów o wielkości zaledwie 4TB nie mamy dodatkowego narzutu w każdym nagłówku obiektu.

W planach jest zwiększenie obsługi do 16TB, zmniejszenie maksymalnej pauzy do 1ms oraz wprowadzenie generacyjności.

A wracając do benchmarku – 306 055 939 ops/s (92% Epsilion GC).

Podsumowanie

Garbage Collectory to temat rzeka. Algorytmy składają się z wielu faz, istnieje wiele struktur danych wykorzystywanych przez gc. Nie sposób ich opisać w jednym wpisie na blogu…

Jeśli ktoś chciałby bardziej zgłębić temat gc, to na początek proponuję Handbook od Plumbr. Czytanie o tym może być trudne, więc warto poszukać prezentacji na youtube o gckach. Po polsku na WJugu Jakub Kubryński, po angielsku na Devoxxie.

Wniosek jest prosty – nie ma nic za darmo… krótkie pauzy, to duży narzut na wydajność. Długie pauzy – brak narzutu na wydajność.

Pytanie na czym nam bardziej zależy…

Oceń wpis

Quarkus – co ciekawego siedzi w środku.

Nie wiem, czy śledzicie wpisy na JvmBloggers, jednak w poprzednim tygodniu były 4 wpisy CRUDowe o Quarkusie. Jeżeli wszyscy o tym piszą, to przecież nie będę gorszy 😉

Jeśli chcecie się dowiedzieć, jak napisać CRUDa to tutaj po polsku, a tu po angielsku. Ja osobiście zajmę się bezużyteczną wiedzą – dlaczego to jest fajne i jaki to ma potencjał. Ale od początku.

GraalVM

GraalVM jest to Oracle’owa maszyna wirtualna bazowana na OpenJDK. Jej celem było stworzenie uniwersalnej maszyny wirtualnej, która obsługuje wiele języków programowania – zarówno JVMowe (Java, Scala, Groovy), jak również skryptowe (Js, Ruby, Python, R) i natywne (C++). Pozwala również na bardzo wydajne wywoływania funkcji zdefiniowanych w różnych językach (takie cross-językowe wywołania).

Jednak stworzenie takiej VMki to była długa droga… Najwięcej działo się przy releasie JDK 9. Wtedy położono fundamenty pod GraalVM – wydzielono w źródłach JDK interfejs kompilatora, poukładano projekt OpenJDK modularnie oraz wprowadzono kompilator Ahead-of-time. Skoro już był wydzielony interfejs kompilatora, to można było wprowadzić nowy kompilator – modularny, piękny i pachnący, o wdzięcznej nazwie Graal. W dodatku cały napisany w Javie (bo kto umie w C++, gdzie nawet GCka nie ma). Uczyniono to w JDK 10 w JEP 317. Tenże kompilator Graal jest w niektórych względach lepszy od C2 – z pewnością lepiej radzi sobie ze różnego rodzaju streamami. Swego czasu popularne było stwierdzenie, że Twitter przeszedł z C2 na Graala i zjadało mu 5% procesora mniej. Jakkolwiek mowa była o Scali, nie o Javie.

W końcu GraalVM wyszedł w maju zeszłego roku. W założeniu miał pozwalać na łatwe wywoływanie międzyjęzykowych funkcji. Standardowym use case’m miało być wydajne wywoływanie funkcji Machne Learningowych napisanych w R lub Pythonie (jak wiadomo Java w tej dziedzinie nie jest zbyt popularna). Innym przykładem może być jednokrotne tworzenie walidacji po stronie frontendu oraz wywoływanie tej walidacji także na backendzie (pozdro Apzumi! 😉 ). Jednak osobiście moim faworytem jest narzędzie do generowania kodu natywnego.

GraalVM Native Image

Narzędzie to pozwala na stworzenie łatwo uruchamianej aplikacji napisanej w Javie skompilowanej do wersji natywnej dla systemu operacyjnego. W pierwszej wersji (19) wspierano tylko Linuxa i MacOSa, w wersji 20 wpierany jest również Windows. GraalVM jest dostępny z JDK w wersji 8 jak i 11. Kompilacja do kodu natywnego jest wykonywana z użyciem wspomnianego wcześniej kompilatora Graal.

W czasie kompilacji do kodu natywnego ma miejsce stworzenie grafu zależności między klasami, polami, metodami, dzięki którego analizie wszystkie nieużywane pola, klasy, metody są usuwane. Wycięcie 90% platformy powoduje, że aplikacja wstaje w milisekundy zamiast sekund. Dodatkowo zużywa znacznie mniej pamięci (mierzone w dziesiątkach megabajtów zamiast setek). Choć na kompilację trzeba poświęcić kilka minut…

Co więcej – GraalVM Native Image nie wspiera wszystkich opcji dostępnych dla standardowego JDK. Brakuje między innymi MethodHandles, InvokeDynamic, serializacji, security managera. W przypadku użyciu refleksji, DynamicProxy, JNI lub dynamicznego ładowania klas wymagana jest rejestracja używanych klas w osobnym pliku konfiguracyjnym. Więcej o ograniczeniach można poczytać na tej podstronie.

Jest kilka rzeczy, które są robione inaczej. Najlepszym przykładem jest wykonywanie inicjalizacji klas na etapie kompilacji źródeł. Dzięki temu GraalVM uruchamia się szybciej. Czasem może jednak to prowadzić do dziwnych błędów. Inną różnicą jest to, że przekazywane propertiesty przy kompilacji są dostępne dla klas inicjalizowanych na etapie kompilacji, a na etapie runtime już nie.

GraalVM umożliwia również wydawanie natywnych bibliotek do systemów operacyjnych, co wcześniej w Javie nie było możliwe.

Jak to w środku działa?

W miarę normalnie – mamy normalny Heap, który co jakiś czas jest odśmiecany. Z tego gdzieś mi się obiło o oczy, a czego znaleźć aktualnie nie potrafię są dostępne 2 GCki. Jeden jest pewną odmianą Parallel GC, drugi jest z rodziny Lowlatency.
Przy uruchamianiu normalnie definiujemy parametry -Xmx oraz -Xms (gdyż normalnie bierze 1/4 całej pamięci 😉 ). Debugować się nie da, chyba, że z użyciem debuggerów natywnych takich jak GDB. VisualVM bodajże jest, choć dla Native Image jest ograniczony. Heap Dump można wykonać, lecz tylko w wersji Enterprise Edition, gdyż z czegoś Oracle musi żyć.

Ciekawe jest to, że na użytek inicjalizacji klas w czasie budowania i obiektów w nim stworzonych jest osobny tzw. Image Heap.

A co na tym można uruchomić?

Z pewnością nie Springa. Bazuje on bowiem z założenia na refleksji, generowaniu bytecode’u, proxy i innych. Nawet gdyby się dało uruchomić, byłoby to wciąż wolne.

Dlatego specjaliści od Red Hata postanowili stworzyć Quarkusa. Sam Quarkus jest nakładką na Vert.x zapewniającą odpowiednie działanie dla GraalVM Native Image. Dodatkowo istnieje wiele „wtyczek” do Quarkusa, w których głównym założeniem jest wykorzystanie pełni potencjału GraalVMa.

Przykładowo mechanizm Dependency Injection ogarnięte jest w czasie kompilacji zamiast w runtime. Inicjalizacja „kontrolerów” webowych również w czasie inicjalizacji.

Nawet Hibernate jest porządnie zoptymalizowany dzięki wtyczce do Quarkusa. A gdy zmigrują Hibernate na Jandexa, wówczas i część inicjalizacji metadanych możnaby przenieść na czas kompilacji.

Podobne pole do optymalizacji widzę również w Jacksonie. Już teraz można cache’ować metadane obiektów w zewnętrznych implementacjach. Nie sprawdzałem jednak, czy w Quarkusie tak się aktualnie dzieje.

Nawet jeśli jeszcze istniejące rozwiązania nie są optymalne, to:
1. Projekt ma mniej niż rok, więc spokojnie 😉
2. Już teraz aplikacje uruchamiają się w czasie mierzonym w milisekundach, więc raczej będzie tylko lepiej.

Czy to działa?

Osobiście w 2 wieczoro/noce zmigrowałem prywatny projekcik z Tomcata, Spring MVC, Hibernate (Criteria API), SpringFilter, Quartz na Quarkusa z Hibernate (JPA Criteria Api) oraz ichniejszego Crona.
Trochę brakuje Spring Data, ale da się żyć.
Działa, nie padło, działa szybciej, wstaje szybciej, zjada znacznie mniej pamięci. Czyli dla prywatnych projekcików postawionych na VPSie działa idealnie 😉

Czy działa gdzieś produkcyjnie? Nie wiem… Ale pewnie internet wie… Spytajcie Googla, może coś powie na ten temat!

Mowa końcowa

Quarkus ma potencjał. Jak nie do małych projekcików, to do lambd wstających w milisekundy.
Migracja ze Springa na Quarkusa nie jest trudna, lecz nieco uprzykrzająca życie ze względu na inne adnotacje. Micronaut ma bardziej zbliżone Api do Springa.

Dla chętnych prezentacja po polsku o Quarkusie z Warszawskiego JUGa przedstawiana przez jednego z jego twórców.

Zostawcie gwiazdki, komcie i inne takie.

Pax!

Oceń wpis

Var

Zarezerwowany typ o nazwie var wprowadzony w Javie 10 jako jeden z rezultatów projektu Amber. Został już dość dobrze opisany na innych mądrych stronach, zatem przytoczę tylko nieoczywiste fakty na jego temat.

var może wpływać na bytecode

Całe wnioskowanie typu dzieje się na etapie kompilacji do bytecode’u. Wtedy również dzieje się podmiana typu zmiennej na typ wywnioskowany.

Czasem przy definicji zmiennej użylibyśmy interfejsu. Jednak gdy użyjemy słowa var, wówczas w pewnych użyciach w bytecodzie pojawi się typ konkretny. Przykładowo w takim kawałku kodu:

        List<String> list = new ArrayList<>();
        list.size();

        var varList = new ArrayList<String>();
        varList.size();

Dekompilowanie poleceniem javap -v -p potwierdza tę tezę:

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
(...)
           16     113     2  list   Ljava/util/List;
           31      98     3 varList   Ljava/util/ArrayList;
(...)

Z tej różnicy wynika konieczność użycia instrukcji invokeinterface zamiast invokevirtual. W trybie interpretowanym invokeinterface ma potencjalnie niezerowy (ale i niezbyt duży) negatywny wpływ na wydajność. Ale o tym już pisałem, więc nie będę rozwijał tego wątku…

var umożliwia wywoływanie metod wcześniej niewidocznych…

… i nie tylko chodzi o to, że w poprzednim przykładzie można wywoływać metody typowe dla konkretnej klasy. Chociaż poniekąd jest to powiązane…

Otóż wyobraźmy sobie klasę anonimową. Standardowo nie mamy informacji o tym, jaka to jest klasa, ponieważ jest… anonimowa… Jednak używając słowa var można wywołać metody specyficzne dla tej klasy.

        var anonymous = new Object() {
            int anInt;
            public int ret() {
                return anInt;
            }
        };
        anonymous.anInt = 4;
        System.out.println(anonymous.ret());

Pachnie Javascriptem, co nie? 😉
Co by nie mówić, wcześniej wywoływanie metod klas anonimowych było możliwe tylko z użyciem refleksji lub methodHandle, więc jest to jakieś usprawnienie.

Czytelność

Dyskusyjną sprawą jest czytelność. Osobiście w jednym projekcie pisanym w Javie 11 doświadczyłem, że var może tę czytelność poprawić. Przykładowo, osobiście taki kod łatwiej się czyta:

var stringsByIds = getStringsById();
var stringList = stringsByIds.getOrDefault(1, List.of());

// zamiast:
Map<? extends Serializable, List<? extends String>> stringsByIds = getStringsById();
List<? extends String> stringList = stringsByIds.getOrDefault(1, List.of());

var w tym przypadku ułatwia nam zrozumienie „co się tu dzieje”. Analizy „jak się to dzieje” raczej nie zaciemnia, bo i tak aby zrozumieć trzeba ponawigować trochę po klasach.

Unikałbym jednak słowa var przy typach prymitywnych z zainicjalizowaną wartością.

        var l = 1; // czy to long?
        var f = 1.0; // czy to float?

Final?

Jak sprawić by nasza zmienna była zmienną ostateczną? Używając final var.

Były proponowane różne podejścia do tego zagadnienia – między innymi była propozycja słowa val lub let. Niestety ostatecznie nie zdecydowano się na wprowadzenie do Javy żadnego z tych słów…

Podsumowanie

var może być przydatny 😉
Podrzucam też bezpośredni JEP odnośnie var oraz podsumowanie dyskusji na temat obecnego kształtu tego słówka.

P.S. Wiem, że czyta ten blog niezerowa liczba osób, ale jeszcze nie dorobiłem mechanizmu, sprawdzającego, ilu się udaje doczytać do końca… Zatem taka prośba: jeżeli doczytaliście aż dotąd, zostawcie po sobie ocenę lub komentarz. Będę wtedy wiedział, że da się te wypociny doczytać do końca 😉

Pax

Oceń wpis

ThreadLocal

ThreadLocal jest trochę jak świnka morska…

Słowem wstępu

Bohaterem tego wpisu jest java.lang.ThreadLocal. Jak sama nazwa wskazuje klasa umożliwia trzymanie pewnej zmiennej w kontekście jednego wątku. Taką klasę można wykorzystać w różnych sytuacjach, a najbardziej typową jest tworzenie obiektów, które nie są thread-safe i przechowywanie takich obiektów osobno dla każdego wątku. Wówczas pozbywamy się wymaganej kosztownej synchronizacji.
Kanonicznym przykładem jest klasa SimpleDateFormatter, która nie jest thread-safe.

Istnieje jeszcze inna klasa zastosowań ThreadLocal. Polega ona na inicjalizacji na początku przetwarzania, następnie w czasie przetwarzania na pobraniu danej wartości (bądź – co gorsza – modyfikacji) a na końcu przetwarzania na usunięciu tej wartości. Przykładowo – Filtr Servletowy:

public class UserNameFilter implements Filter {
    public static final ThreadLocal<String> USER_NAME = new ThreadLocal<>();
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        try {
            USER_NAME.set("Dobromir");
            filterChain.doFilter(servletRequest, servletResponse);
        } finally {
            USER_NAME.remove();
        }
    }
}

Takie zastosowanie to tak na prawdę taki lokalny dla wątku singleton. Taki mechanizm obrazuje mniej więcej:

Asdf movie

Kto takiego kodu nie popełnił, niech pierwszy rzuci kamień 😉

Czasem po prostu nie ma innej opcji, by przekazać coś z jednego miejsca w drugie, bo przykładowo ogranicza nas interfejs/zewnętrzna biblioteka. Jednak, gdy mamy możliwość przekazania czegoś w parametrze metody zamiast w ThreadLocal, warto z tej możliwości skorzystać. Nawet, gdy to będzie przepchanie przez 20 ramek głębiej w stacktrace’ie.

Escape analysis

Warto kontekst przekazywać w parametrach z wielu powodów. Najważniejszym jest jawne ukazanie zależności potem, testowalność itp. Ale gdzieś na sam końcu jest też wydajność.

Dla każdej metody skompilowanej C2 jest uruchamiane Escape Analysis, która pozwala na unikanie fizycznego tworzenia obiektów. Jeśli jednak taki obiekt jest udostępniony w jakimś polu, to automatycznie uniemożliwiamy ominięcie tworzenia obiektu.

Implementacja ThreadLocal

Najprostsza implementacja tej idei to zwykła mapa HashMap<Long, Object>, która w kluczu przyjmuje Thread.getId(). To rozwiązanie jest jednak zasadniczą wadę – jeśli wątek by zakończył swoje działanie, a wpis nie zostałby usunięty, wówczas mielibyśmy klasyczny przykład wycieku pamięci w Javie. Trzymanie jakiegoś rodzaju uchwytu do tych wpisów dla ThreadLocal może i rozwiązało problem, ale mogłoby być kosztowne pamięciowo.

Dlatego OpenJDK robi to inaczej. W każdym obiekcie java.lang.Thread istnieje pole threadLocals będące instancją klasy ThreadLocal.ThreadLocalMap. W tym polu przetrzymywane są wartości dla wszystkich ThreadLocal. Jest to mapa, którą można określić jako HashMap<ThreadLocal,Object>.

Gdy wołamy o ThreadLocal.get() wywoływany jest następujący kawałek kodu:

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocal.ThreadLocalMap map = this.getMap(t);
        if (map != null) {
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                T result = e.value;
                return result;
            }
        }

        return this.setInitialValue();
    }

    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

Ta nieco zawiła implementacja trzyma wszystkie zmienne specyficzne dla wątku blisko reprezentacji tego wątku w Javie. Dzięki temu w czasie kończenia działania wątku, łatwo je udostępnić dla gc’ka (this.threadLocals = null).

Czy ThreadLocal ma jakieś super moce?

Pojęcie Thread-local storage jest pojęciem znanym i powszechnym w różnych językach programowania. Ponadto jest tak często nazywana pewna część pamięci natywnej wyłączna dla wątku systemu operacyjnego. Jednak w przypadku OpenJDK taka pamięć jest wykorzystywana co najwyżej przy jakichś metadanych GCka (wystarczy wyszukać w kodzie źródłowym OpenJDK terminu ThreadLocalStorage). Całość implementacji ThreadLocal bazuje na Heapie.

Co więcej, okazuje się, że ten ThreadLocal nie jest aż tak przywiązany do samego wątku, gdyż można go z poziomu innego wątku zmienić. Można to łatwo sprawdzić wykonując refleksyjną magię:

public class ThreadLocalExperiment {

    private static boolean work = true;
    private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();

    public static void main(String[] args) throws Exception {
        var thread1 = new Thread(() -> {
            THREAD_LOCAL.set(12);
            while (work) { }
            System.out.println(THREAD_LOCAL.get());
        });

        thread1.start();

        var clazz = Thread.class;
        var field = clazz.getDeclaredField("threadLocals");
        field.setAccessible(true);
        var threadLocals = field.get(thread1);
        var method = threadLocals.getClass().getDeclaredMethod("set", ThreadLocal.class, Object.class);
        method.setAccessible(true);
        method.invoke(threadLocals, THREAD_LOCAL, 24);

        work = false;
    }
}

ThreadLocal vs local variable

Generalnie warto też porównać, jaka jest różnica w wydajności między zmiennymi lokalnymi, a ThreadLocal. Prosty benchmark ukazujący skalę różnicy wydajności:

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public Integer local() {
        Integer i = 1;

        return i;
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public Integer threadLocal() {
        THREAD_LOCAL.set(1);
        Integer i = THREAD_LOCAL.get();
        THREAD_LOCAL.remove();
        return i;
    }

Wyniki to 4,358 ± 0,039 ns/op dla zmiennej lokalnej oraz 41,359 ± 2,797 ns/op dla ThreadLocal (1 ns to jedna milionowa milisekundy zatem niewiele 😉 ). Jednak samo sięganie na stertę zamiast na stos wątku jest już pewnym minusem. Ponadto te różnice w pewien sposób zależą od GC, którym wartości ThreadLocal podlegają.

JITowi również nie jest łatwo zinterpretować wartości ThreadLocal jako niezmienialne przez inne wątki. Chociaż swoją drogą mogą być zmienione jak wcześniej zostało wykazane. Brak możliwości zastosowania Escape Analysis również nie pomaga…

Ale o co chodzi z tą świnką morską?

ThreadLocal to taka świnka morska, bo ani świnka, ani morska…

Ani nie są tą jakoś szczególnie wyłączne dane wątku, gdyż są na Heapie, współdzielone, a takie stricte dane wątku są na Off-Heapie. Ani też nie są to szczególnie lokalne dane – przeważnie to są singletony w kontekście wątku.

Niby blisko tego wątku to jest, ale jednak nie za bardzo…

Oceń wpis

Przepisujemy Jacksona z refleksji na LambdaMetafactory [ZOBACZ JAK]

We wpisie o takiej szybszej refleksji porównywałem różne podejścia do wywoływania setterów. Czas na to by tę teorię zastosować w Jacksonie oraz jaki jest wpływ na wydajność tego rozwiązania.

Jackson – wstęp

Jackson Databind to jedna z najpopularniejszych bibliotek do mapowania obiektów na tekst w formacie JSON oraz JSONów na obiekty. Używana domyślnie przez Spring Boota, lecz również przez zyskującego na popularności Micronauta. Aktualnie wersją stabilną jest wersja 2.10 wymagająca do działania Javę 7. Nadchodząca wersja 3.0.0 będzie wymagała minimum Javy 8.

Ciąg znaków w postaci JSON może być zmapowany na obiekt w Javie poprzez wywołanie konstruktora, ustawienie (refleksyjnie) pól lub (refleksyjne) wywołanie metod setterów. Ten ostatni sposób wykorzystujący publiczne settery jest sposobem domyślnym.

I to właśnie wywoływanie setterów jest mechanizmem do podrasowania. Wybranym sposobem usprawnienia setterów jest użycie LambdaMetafactory.metafactory(), który wypadł najlepiej w testach opisanych we wspomnianym wpisie.

Trochę kodu

Klasą bezpośrednio odpowiadająca za ustawienie pól z użyciem setterów jest MethodProperty. Oprócz zapięcia się tam debuggerem można to wywnioskować również z komentarza dotyczącego klasy:

/**
 * This concrete sub-class implements property that is set
 * using regular "setter" method.
 */
public final class MethodProperty
    extends SettableBeanProperty

Działanie Jacksona w obrębie tej klasy (oraz mu podobnych ustawiających pola) można podzielić na przygotowanie metadanych wywoływanego settera (wykonywane jednokrotnie dla danej klasy) oraz wywołanie settera (przy każdym deserializowanym obiekcie klasy).

Zatem w fazie inicjalizacji musimy nie tylko zapisać obiekt Method, lecz również wygenerować implementację interfejsu BiConsumer. Ta implementacja dla podanego obiektu ustawi podaną wartość wywołując odpowiedni setter.

Generowanie takiego obiektu jest nietrywialne. Najpierw trzeba zamienić Method w MethodHandle, wyczarować skądś Lookup, a następnie dopasować odpowiednią sygnaturę BiConsumer wrzucić wszystko do LambdaMetafactory.metafactory() a potem tylko magiczne getTarget() i invoke().
Dużo trudnego kodu, o który nie pytają na rozmowach rekrutacyjnych, więc nie trzeba go znać, ni rozumieć. Jednak jeśli was jeszcze nie zniechęciłem, to można spojrzeć na plik, gdzie umieściłem tę całą magię.

Po zainicjalizowaniu implementacji BiConsumera i zapisaniu jej obiektu w nowym polu klasy MethodProperty można wziąć się za drugą fazę – wywoływanie. W tym przypadku zmiany ograniczyły się do zamiany _method.invoke(object, value) na consumer.accept(instance, value).

I to wszystko?

Oczywiście, że nie 😉 obsłużyliśmy zaledwie ustawianie pól obiektowych (Stringów). Zostało jeszcze 8 typów prymitywnych (czy wymienisz je wszystkie?) tzn. stworzenie 8 interfejsów odpowiadających BiConsumer oraz ich obsługi.

Dodatkowo MethodProperty odpowiada też za settery zwracające ustawione wartości (nie void), które zatem całą pracę trzeba też wykonać dla BiFunction.
I dla 8 typów prymitywnych również.

Na koniec mvn clean install oraz sprawienie, by testy się zazieleniły.

Ostatecznie można przejść do sprawdzania wpływu na wydajność 🙂
Dla ciekawych tych wszystkich zmian – draft pull requesta.

Performance

Zrobiłem zatem porządne testy – dla OpenJDK w wersjach 8 oraz 11 uruchomiłem prosty benchmark – deserializację z użyciem wcześniej stworzonego ObjectMappera (czyli inicjalizacja już poczyniona). Do benchmarku zaprzęgnięty JMH – najpierw porządne rozgrzanie JVMa i benchmarkowanej metody, potem 100 iteracji po 1s każda. Wszystko wykonywane na Ubuntu 18.04 bez trybu graficznego, bez dostępu do internetu, bez innych nadmiarowych procesów.

Zestawy testowe składały się z 3 podzestawów – obiektów z polami obiektowymi (Stringami), obiektów z polami prymitywnymi oraz miks po połowie (String/primitive). Każdy z podzestawów posiadał klasy o 2,6, 10 lub 20 polach.

Wyniki są następujące (wyniki podane w ns/op):

Nazwa testuOpenJDK 8 z refleksjąOpenJDK 8 z LambdaOpenJDK 11 z refleksjąOpenJDK 11 z Lambda
primitive 2375,162371,571420,594424,329
primitive 6883,396833,530888,789833,256
primitive 101423,6831219,3351407,7131540,637
primitive 203294,1293263,1963598,2303708,698
objects 2369,348371,997430,879429,898
objects 6866,949897,4461045,449984,428
objects 101340,5021333,7121562,4671519,283
objects 202874,2112723,3563282,2163286,685
mixed 2383,846382,690454,834447,254
mixed 6865,195818,739975,578970,954
mixed 101370,8341359,1501620,9321598,931
mixed 203106,1883056,0293834,5733573,692

Krótko mówiąc, może czasem jest coś szybciej, ale to niewiele szybciej (średnio 1-3%), jednak czasem nawet bywa wolniej.

Gdzie się podziała ta całą wydajność?

Z najprostszych obliczeń (oraz poprzedniego artykułu) dla „objects-20” czysta refleksja powinna zajmować 60,494ns * 20 pól = 1209,88ns. Wywołanie z LambdaMetafactory powinno kosztować 18,037 * 20 pól = 360,74 ns.
Czyli walczyliśmy o 849,14ns/2874,211ns = 29,5%.

Uruchamiając ponownie benchmark JMH z dodatkowym profilowaniem .addProfiler(LinuxPerfAsmProfiler.class) zobaczyć można, że rzeczywiście procentowo nieco odciążyliśmy metodę odpowiedzialną za przypisania wartości polu.

....[Hottest Methods (after inlining)]..............................................................
<wartość dla wersji z refleksją>
 23,38%         C2, level 4  com.fasterxml.jackson.databind.deser.impl.MethodProperty::deserializeAndSet, version 869 

<wartość dla wersji z lambda>
 21,68%         C2, level 4  com.fasterxml.jackson.databind.deser.impl.MethodProperty::deserializeAndSet, version 915

Gdzie jest reszta? Trzeba zweryfikować założenia.
W poprzednim wpisie podawałem takie zalety MethodHandle / LambdaMetafactory:

  • przy każdym wywołaniu Method.invoke sprawdzamy, czy dostępy się zgadzają
    • Tutaj rzeczywiście oszczędzamy – patrząc głęboko w kod C2 można zauważyć brak sprawdzania dostępów;
  • gdzieś wewnątrz wywołania Method.invoke jest wywoływana metoda natywna – plotki głoszą, że to jest powolne…
    • W trybie interpretowanym rzeczywiście tak jest, jednak C2 potrafi owinąć w klasę (podobnie do LambdaMetafactory), zatem tutaj zysku brak
  • sama treść metody wywoływanej jest za każdym razem zagadką – takiej metody nie da się zinline‚ować treści tej metody do nadrzędnych metod. Wywoływana metoda nie jest rozumiana przez JITa.
    • W tym przypadku rzeczywiście C2 mógłby próbować ziniline’ować treść metody. Niestety kontekst wywoływania metody jest zbyt wąski, a profilowanie typu settera prowadzi do wywnioskowania, że jest wywoływany jeden z 20 setterów o interfejsie BiConsumer. Takiego wywołania „megamorficznego” nie można zinline’ować, przez co musimy wpierw sprawdzić typ, a nastęnie wykonać instrukcję skoku do treści metody.
      Dokładnie to samo dzieje się przy refleksji – skaczemy do treści metody w owiniętej przez refleksję w klasę metodzie. Stąd i tutaj przyspieszenia brak.

No cóż… „Bo tutaj jest jak jest… Po prostu…”.

Podsumowanie

Pomysł na usprawnienie był całkiem dobry, jednak bardziej skomplikowana rzeczywistość rozmyła złudzenia o znacznie wydajniejszym Jacksonie.

Podane rozwiązanie ma jednak pewną wadę – dla każdego settera generujemy klasę. Przeważnie tych setterów jest dużo, co oznacza, że zaśmiecamy dość mocno Metaspace bez brania pod uwagę, czy ten setter jest często wywoływany, czy rzadko. Warto tu zatem użyć zamiast tego MethodHandle – przynajmniej przedwcześnie nie generuje klasy, a wydajność może być niegorsza niż podanego rozwiązania.

Czy da się szybciej?
Prawdopodobnie tak, jednak nie używając setterów, a konstruktorów i pól. Ale to temat na inny wpis 😉

Na koniec w noworocznym prezencie link do artykułu Shipileva o megamorphic calls. Bo to mądry człowiek jest 😉

Pax et bonum.

Oceń wpis

Odczyt finalnych pól instancyjnych

Zaraz koniec roku, trzeba zamknąć pewne tematy.

Ostatni wpis w tej serii jest o optymalizacjach związanych z finalnymi polami instancyjnymi. Zacznę od obiecanego powrotu do tematu z pól statycznych finalnych, czyli obiektu trzymanego w polu statycznym finalnym oraz jego pól.

Gdy właściciel pola finalnego sam jest polem static final

Weźmy pod uwagę hipotetyczną sytuację: hipotetyczny obiekt Owner, który jest przetrzymywany w polu static final posiada pole finalne int value. W jakiejś innej klasie odwołujemy się przez to pole static final do klasy Owner, a następnie do tego int value.
W czasie kompilacji JIT mamy informację dokładnie o ustalonej wartości tego pola value, zatem można by oczekiwać, że w ramach optymalizacji zostanie wykonany Constant Folding. Jaka jest rzeczywistość? Wykonajmy benchmark:

@State(Scope.Benchmark)
public class InstanceFinalFieldBenchmark {
    public static final ConstantClass CONSTANT_CLASS = new ConstantClass(12);

    public static class ConstantClass {
        private final int value;
        public ConstantClass(int value) { this.value = value; }
        public int getValue() { return value; }
    }

    @Benchmark
    @CompilerControl(CompilerControl.Mode.PRINT)
    public int local() {
        return CONSTANT_CLASS.getValue();
    }
}

Adnotacja @CompilerControl(CompilerControl.Mode.PRINT) powoduje wypisanie kodu maszynowego dla tej metody. Spoglądając w jej treść odnajdziemy fragment:

  0x00007f487be1f05c: movabs  $0x716202b90,%r10  ;   {oop(a 'pl/jgardo/field/instance/InstanceFinalFieldBenchmark$ConstantClass'{0x0000000716202b90})}
  0x00007f487be1f066: mov     0xc(%r10),%eax    ;*getfield value {reexecute=0 rethrow=0 return_oop=0}
                                                ; - pl.jgardo.field.instance.InstanceFinalFieldBenchmark$ConstantClass::getValue@1 (line 23)
                                                ; - pl.jgardo.field.instance.InstanceFinalFieldBenchmark::local@3 (line 58)

Te dwie instrukcje odpowiadają za pobranie wartości z pola klasy, co jest też objaśnione komentarzem getfield value.
Innymi słowy brakuje tutaj tej optymalizacji, której byśmy oczekiwali. Dlaczego tak się dzieje?

OpenJDK ma problem z zaufaniem do instancyjnych pól finalnych.

Dzieje się tak z powodu, że istnieje kilka sposobów na popsucie pola final. Te sposoby to:

  • metody natywne,
  • refleksja,
  • method handles,
  • sun.misc.Unsafe.

(m. in. o tych sposobach jest prezentacja Volkera Simonisa „How final is final”, którą polecam 😉 ).

Niestety póki co nie jesteśmy w stanie za dużo zrobić, by final domyślnie odzyskał pełną wiarygodność. Jakkolwiek są pewne dość brudne sposoby, by zmusić JVMa do zaufania finalom.

-XX:+TrustFinalNonStaticFields

Pierwszy to eksperymentalna flaga -XX:+TrustFinalNonStaticFields. Niestety istnieje ona w OpenJDK w wersji 8 i późniejszych, lecz w OracleJDK była w wersji 8, a w 11 już nie…

Jeśli chodzi o skuteczność tej flagi, to w OpenJDK działa ona zgodnie z przewidywaniem, tzn zwraca od razu żądaną wartość:

  0x00007f95c4440bdc: mov     $0xc,%eax

Jeśli ktoś chciałby co nieco poczytać na temat tej flagi, to polecam spojrzeć na tę korespondencję mailową.

@jdk.internal.vm.annotation.Stable

Drugim sposobem na zmuszenie JVMa do zaufania final jest użycie adnotacji @Stable na danym polu finalnym. Taka adnotacja istnieje od OpenJDK w wersji 9, jednak została ona zaprojektowana tylko i wyłącznie do użytku wewnętrznego JVM i nie jest zbyt łatwo dostępna dla zwykłych śmiertelników.

Nie oznacza to jednak, że się nie da jej użyć… 😉
Istnieją dwa ograniczenia zniechęcające do użycia jej:

  • Adnotacja jest dostępna tylko dla modułów: java.base, jdk.internal.vm.ci, jdk.unsupported
    • Ale jeśli dodamy przy kompilacji obiektu korzystającego ze @Stable parametry --add-exports java.base/jdk.internal.vm.annotation=ALL-UNNAMED to się skompiluje,
  • Obiekt korzystający ze @Stable musi być załadowany przez bootclassloader
    • Zatem jeśli dodamy przy uruchomieniu parametr -Xbootclasspath/a:”<ścieżka do jar>”, to też zadziała 😉

Ludzie listy piszą…

Na temat @Stable również istnieje korespondencja mailowa, na którą warto spojrzeć. Dotyczyła ona pytania, dlaczego by nie udostępnić takiej adnotacji dla użytkowników. W tej korespondencji jest wiele ciekawych wątków i linków.

W jednej z odpowiedzi można znaleźć trzeci sposób na zasymulowanie @Stable. Jednak nie testowałem, więc się nie wypowiem.

Co robić, jak żyć?

Jest pewna nadzieja – na samym końcu wspomnianej wyżej korespondencji jest taka wypowiedź:

For optimizing final fields there are much more promising approaches:
   (1) optimistic optimizations in JITs: treat finals as constants and 
track updates invalidating code which relies on that (there were some 
experiments, e.g. [1]);
   (2) forbid final field updates at runtime after initialization is 
over and freely optimize them in JITs.

Both approaches still have some roadblocks on their way (deserialization relies on modifying final fields, hard to track final field values of individual objects in JVM, etc), but the platform steadily moves in the direction of treating final fields as truly final by default.

Zatem trzeba to przyjąć z pokorą i cierpliwością, bo pisanie JVMów do łatwych nie należy…

Chyba, że się jest bogatym, to zamiast cierpliwie czekać, można zainwestować w Azul Zing – tam jest wiele ciekawych flag do użycia z „final” w treści (na stronie chriswhocodes.com można podejrzeć, jakie są dostępne opcje/flagi dla różnych JVMów; można wyszukać po nazwie opcji).
Chociaż osobiście jeszcze nie zgłębiałem możliwości tej JVMki.

A co z polami instancyjnymi?

Jak się okazuje, śledzenie finalnych pól obiektu static final jest nietrywialne, a jeszcze trudniejsze jest śledzenie wartości w polach niestatycznych… Nie znalazłem niestety żadnych optymalizacji dla pola finalnego…

Tym smutnym faktem kończę całą tę sagę o final. Ostatecznie Frodo nie dotarł do Góry Przeznaczenia, Golum odebrał mu pierścień, a słowo kluczowe final dla pól instancyjnych nie ma de facto pozytywnego wpływu na wydajność…

Ale głowy do góry, nadchodzi nowy rok 2020, a z nim nowe, czternaste wydanie OpenJDK, gdzie ma zostać pokazany światu po raz pierwszy nowy sposób dostępu do Off-Heapa. Jednocześnie to może być kolejny krok w stronę zmniejszenia znaczenia sun.misc.Unsafe. A to może w skończonym czasie doprowadzić do wzrostu znaczenia finala.
Cytując klasyka -„Make final final again”. Czy coś podobnego… 😉

Oceń wpis