Mateusz Kamiński - Czy Java może być wydajna?
Streszczenie odcinka
Podczas naszej dyskusji poruszamy następujące tematy:
Jakie zagadnienia najbardziej Cię interesują w IT?
Dlaczego programujesz w Javie?
Kiedy Java jest wydajna, a kiedy nie?
W jaki sposób można optymalizować działanie aplikacji w Javie?
Czy są do tego jakieś narzędzia, biblioteki, dobre praktyki?
Czy kolejne wersje Javy są szybsze / bardziej efektywne?
Jakie są najważniejsze zmiany w Javie od wersji 8 od strony programisty?
Czy Java nadal szybko się zmienia?
Kiedy warto skorzystać z innych języków programowania działających w ramach JVM?
Jakie są najczęstsze błędy popełniane przez programistów JVM?
Transkrypcja odcinka
Cześć, witam was w kolejnym odcinku podcastu Stacja IT. Dzisiaj będziemy rozmawiać o wydajności w Java oraz o tym co nowego dzieje się w tym języku. A z nami jest ekspert od Java i platformy JVM, Mateusz Kamiński.
Cześć, cześć Mateusz. Cześć, cześć wszystkim. Mateusz jest specjalistą od Java, wiele lat pracuje z tym językiem, pracuje w Sagess, prowadzi też szkolenia z Java i z wydajności i z innych tematów związanych właśnie z platformą JVM.
Czy chcesz na początek parę słów jeszcze powiedzieć o sobie?
Więc ogólnie u mnie sytuacja wygląda tak, że od ponad już 10 lat mam do czynienia z Java albo z językami wokół Java, czyli czy to Scala, czy Kotlin, czy kiedyś był Groovy albo Clojure, ale ogólnie wciąż właśnie ciekawą rzeczą jest to, że ta Java zawsze zostaje gdzieś na przodzie, więc tak, tutaj u mnie tak wygląda. No i poza tym oczywiście, że jestem programistą, to w moim doświadczeniu byłem też architektem, tak, miałem okazję tworzyć system od zera albo rozwijać istniejące, no a także prowadziłem zespoły, byłem takim można powiedzieć mentorem, kierownikiem, ale właśnie ja bardziej wolę mentorować ludzi niż być takim bossem typowym, więc tak, pomagam innym i teraz właśnie się bardziej jeszcze skupiłem na szkoleniach, żeby przekazywać swoją wiedzę szerzej w jakimś tam aspekcie też dotyczącym nie tylko samej Java, ale też architektury oprogramowania.
No to super, bo właśnie trochę z tej wiedzy chcieliśmy zaczerpnąć dzisiaj, jakby takim głównym motywem tej rozmowy, no to ma być właśnie Java versus ten cały hype, który jest wokół jakichś innych języków, dużo się mówi o jakichś nowinkach, tak, o językach też nowych, które są na JVM-ie, nie mówiąc już o mnóstwie technologii, które są związane z frontendem, które są bardzo seksji, natomiast ta Java gdzieś tam cały czas w tle jest, to od wielu lat, no i pytanie, czy ona jest już nudna, czy należy zająć się czymś innym, czy ona jest niewydajna, czy ona się nie zmienia, to chcielibyśmy dzisiaj o tym porozmawiać.
No może zacznijmy od tego, co jeszcze właśnie ciebie teraz najbardziej w tym programowaniu, w Javie w szczególności kręci, czy właśnie te kwestie wydajnościowe, czy architektoniczne, gdzie, gdzie znajdujesz ten fun, żeby właśnie cały czas jakby aktywnie pracować w tym wszystkim.
No to Java właśnie wbrew pozorom, mimo, że już ma 25 lat, ponieważ właśnie ogólnie rok 95 to początek Javy, wtedy wyszła pierwsza wersja, no i teraz widzimy, że w ostatnich latach mamy duży rozwój, w sensie był taki czas, kiedy była Java 8 mniej więcej, kiedy trochę to przystopowało, nie wychodziły nowe wersje Javy przez dłuższy czas. To wynikało ze zmian, ponieważ firma Sun upadła, Oracle ją przyjął, no i w tym momencie właśnie oni musieli się pewnie tam wewnętrznie zorganizować. Teraz jesteśmy już na takim poziomie, że co 6 miesięcy właśnie wychodzi nowa wersja Javy, tylko co 2 lata wychodzi wersja taka długoterminowa, czyli LTS, natomiast co 6 miesięcy mamy coś pomiędzy, możemy coś nowego przetestować, natomiast oczywiście jeżeli mamy jakiś większy system, który chcemy stabilizować, no to wybieramy tę wersję LTS.
Podobnie nie wiem Ubuntu na Linuxie też ma taki system wydawania, więc to jest dosyć popularne i dobrze się sprawdza w praktyce.
I to właśnie to, co tutaj dla ciebie jest fajne, to że te nowe elementy języka, nowe funkcje języka się właśnie pojawiają i to jest jakby taki element, który właśnie pozwala ci znaleźć ten fun w programowaniu?
No tak, po pierwsze nowe elementy, czyli taki powiedzmy syntax sugar dla programisty jest istotny, ponieważ chcemy w szybszym czasie zrobić to samo. Powiedzmy, to jest taki główny cel. Oczywiście jeżeli ktoś jest purystą, to lubi różne ciekawe rzeczy, ale wtedy raczej wchodzimy już w te pochodne języki, żeby na przykład programować bardziej funkcyjnie, niż da się w Javie.
Natomiast no co do zasady w Javie mamy przynajmniej taki kompromis, że wybieramy stabilność właśnie zmian w języku względem właśnie tych nowości, które są wybierane jako pochodne z innych języków. W sensie Java bazując być może na takim języku jak Groovy, jak Scala wybiera z nich najlepsze rzeczy i je standardyzuje właśnie w obrębie Javy. Natomiast jeszcze oprócz tego zmiany w Javie nie dotyczą samego języka, ale przede wszystkim wirtualnej maszyny.
W Javie tam się bardzo dużo dzieje i to ma największy wpływ właśnie na wydajność. To, że przechodząc na nowsze wersje Javy, oczekujemy większej wydajności, ponieważ właśnie są stosowane, nie wiem lepsze algorytmy odśmiecania pamięci albo po prostu nawet sama kompilacja kodu do poziomu natywnego, może jeszcze o tym za chwilę powiemy, jest wykonywana w lepszy sposób, bardziej zaawansowany niż dotychczas.
Okej, a jak to się w ogóle u Ciebie zaczęło? Właśnie dlaczego Java? Czy to był przypadek?
Czy właśnie wiedziałeś, że tutaj jakby większość takich aplikacji korporacyjnych jest właśnie w Javie pisana i tutaj jest największy rynek i największa przyszłość? Czy coś innego zadecydowało?
Właśnie to jest ciekawe, ponieważ ja w 2014 roku zacząłem od praktyk, a w ogóle miałem takie dwie opcje na samym początku. W sensie mogłem wybrać C++, a jedna firma w C++ pisała. Na szczęście tam nie poszedłem, wybrałem drugą firmę, która robiła w Javie i to był właśnie, okazało się, że to był lepszy wybór.
No i w praktyce już wtedy zacząłem się zajmować, w sensie zostałem rzucony w głęboko wodę, tak? Miałem dobrego kierownika, którego pozdrawiam, natomiast właśnie już wtedy my bardzo jakby nacisk mieliśmy na to, żeby wytwarzać wydajny kod, żeby ten kod był dobry, no i żeby po prostu klienci byli zadowoleni z tego, jak to robimy. Natomiast to jak się programowało właśnie wtedy w Javie, a jak się programuje obecnie, to jest naprawdę duża różnica, więc tylko na przestrzeni tych 10 lat język przeszedł olbrzymie zmiany, a pewnie jeszcze te pierwsze wersje Javy od 1995 roku podobnie można byłoby powiedzieć, że tam też dużo się działo.
No ja właśnie mam doświadczenie od Javy 6 i wzwyż, widzę, jakie tam zmiany zaszły w języku, ale też poza samym językiem, czyli głównie co się stało w wirtualnej maszynie Javy.
To może właśnie zacznijmy od tego, żeby powiedzieć, jakie są te główne elementy, które wpływają na wydajność, bo tutaj powiedziałeś już trochę o tym, że coś zależy od maszyny wirtualnej, od JVM-a, coś od języka, ale jakby to tak usystematyzować, to znaczy po pierwsze trzeba pisać wydajny kod zapewne, w sensie w taki sposób pisać, żeby nie było to naiwne, tylko żeby stosować właściwe podejście, algorytmy itd. Są jakieś zmiany w języku, które to właśnie też ułatwiają zapewne i właśnie jest ten JVM, czy to są te główne elementy, czy o czymś tutaj jeszcze należałoby wspomnieć, jakby o tych głównych kategoriach, na które trzeba popatrzeć, żeby się zastanowić, czy moja aplikacja jest wydajna, czy nie.
No to tak, tutaj możemy podzielić JVM-a na takie dwie powiedzmy sfery. Pierwsza dotyczy odśmiecania pamięci, a druga kompilacji do kodu natywnego przez JIT-a. Właśnie jak ja zaczynałem w ogóle pracować w Javie, przy Javie, no to niestety wiele źródeł mówiło o tym, że Java to jest interpretowany język.
Co ciekawe, a to jest absolutnie nieprawda, ponieważ już od dawien dawna, tak naprawdę od wersji 1.1 wprowadzono JIT-a, czyli Just In Time Compiler i to oznacza, że w trakcie jak nasza aplikacja działa, Java cały czas bada, ile razy pewne metody się wykonują. Tam może to nawet dochodzić na poziom gałęzi, czyli powiedzmy ifów. No i dzięki temu kod może zostać zoptymalizowany w trakcie działania systemów w takich gorących miejscach.
To są tak zwane hotspoty. No i wtedy to jest skompilowane do poziomu tak naprawdę takiego samego, jakbyśmy to skompilowali przy pomocy C++-a. Ale no niestety tutaj jest jedna drobna różnica, że właśnie wciąż mamy odśmiecanie pamięci.
To jest dla naszej wygody. Przede wszystkim, żeby błędów głupich nie robić w kodzie, tylko zająć się faktycznie tym, co jest najważniejsze. Natomiast odśmiecanie pamięci troszkę kosztuje i trzeba tutaj na to przede wszystkim uważać, bo najwięcej problemów, jakie się pojawiają, to zwykle właśnie dotyczą odśmiecania pamięci.
Natomiast o ten aspekt kompilacji do kodu natywnego, to wystarczy, że zadbamy o to, że nasz kod będzie dobrze napisany. Po prostu będziemy dbali o czystość kodu, bo im mniejsze są te metody w klasach, im klasy mają mniej metod, czyli tak jakby to jest dobrze zorganizowane, tym ten JIT ma łatwiej to zoptymalizować. Więc to jest taki ciekawy aspekt, bo tutaj też powiem, że spotykałem się z takimi nie wiem, sugestiami, że po co tam na to jakoś bardzo zwracać uwagę, w sensie komputer, procesor wszystko przyjmie i wykona.
No nie do końca tak, jak widać jest, ponieważ nawet sam styl kodu ma znaczenie dla jakości potem i wydajności działania. Natomiast wracając do tego algorytmu odśmiecania pamięci, to w nowych wersjach Javy mamy od groma tak naprawdę opcji do wyboru, bo kiedyś powiedzmy tych garbicz kolektorów było mniej. Obecnie są dodane do tych nowych wersji JVM-a garbicz kolektory dla dużych ster, czyli jeżeli mamy aplikacje, które wymagają naprawdę pokaźnych obszarów pamięci, żeby coś tam przetworzyć, no to wtedy powinniśmy zmienić tego domyślnego garbicz kolektora na przykład ZGC.
To jest jeden z wyborów i to obecnie wygląda tak, że ten ZGC za jakiś czas stanie się standardem, czyli nawet zastąpi ten aktualny garbicz kolektor. Więc tutaj nie musimy może na pierwszy rzut tym się interesować, jak programujemy w Javie, ale w końcu dojdziemy do takiego momentu, że pewnie trzeba będzie po prostu zacząć w to wchodzić i rozumieć różnicę w wyborze rozwiązania na poziomie JVM-a.
Ok, czyli mamy tutaj po pierwsze kwestie związane z tą kompilacją w czasie rzeczywistym, tak? Mamy, mamy garbicz kolektory, a czy sam język też jakoś się zmienił w taki sposób, że nie wiem, łatwiej pewne algorytmy zapisywać, czy pewne jakby często powtarzające się schematy oprogramować, czyli jak gdyby od strony samej konstrukcji języka, czy też coś się zmieniło przez te lata, co może wpływać na wydajność?
W Javie ósmej wprowadzono funkcyjne elementy do języka, tak? Wcześniej można powiedzieć, że trzeba było trochę to symulować. Obecnie mamy faktycznie możliwość programowania w języku zgodnie ze stylem, który obecnie się nazywa data-oriented programming.
To jest paradygmat, który wywodzi się z obiektowości właśnie oraz funkcyjnego języka, czyli gdzieś pomiędzy mamy data-oriented programming. I o co w tym chodzi? Chodzi o to, że stosujemy takie zbiorniki typy danych, które są niemutowalne i na nich je procesujemy w odpowiedni sposób, zapisując je w pewnej hierarchii dziedziczenia.
One zwykle powinny dziedziczyć się po pewnym interfejsie. Tutaj, co ciekawe, interfejsy w Javie zostały rozwinięte, że możemy mieć oczywiście domyślne metody, czyli metody z jakąś implementacją, a także możemy mieć prywatne metody, prywatne metody statyczne. To w pierwszych wersjach języka ja nawet mi tego brakowało osobiście.
Teraz to wszystko już mamy. Więc mamy jakieś interfejsy, które definiują zachowania i mamy szczegółowe implementacje, które tak naprawdę są zbiornikami danych. I tu wykorzystujemy typy nowe w Javie, czyli rekordy.
Już nie mamy tylko klasy interfejsów, mamy też rekordy. No i wtedy, jak te rekordy implementują te zachowania, to dalej w kodzie możemy nie bezpośrednio na te rekordy jakby się zapinać w kodzie, tylko na te interfejsy, które po prostu mają pewne atrybuty i zachowania. Więc troszeczkę kodowanie w Javie się zmienia, ale oczywiście tutaj mamy problem, że wiele systemów już jest napisane w takim czystym stylu obiektowym, więc nagłe przejście w takim przypadku na właśnie ten nowy paradygmat może być trudne.
Ale jak tworzymy coś od zera właśnie w nowej wersji Javy, to jak najbardziej proponuję skorzystać z takiej opcji, bo wtedy naprawdę ten kod jest napisany w nowoczesny sposób i on przypomina to, co tak naprawdę mamy w tych językach pochodnych od Javy, czyli Kotlin, Scala, Groovy. No to jakby już powiedzmy na tym samym poziomie. Ja osobiście nie widzę różnicy, czy bym to zapisał w Scali, czy w Javie.
W zasadzie mogę zrobić to samo. Oczywiście są tam jeszcze inne aspekty funkcyjnego programowania, ale mówię w Javie, tak jak mówiłem, bierzemy te najlepsze, najważniejsze rzeczy i je standaryzujemy po prostu.
A tak właśnie jakby wychodząc od Javy ósmej, która tak jak mówiłeś była jakimś takim dłużej trwającym etapem w rozwoju Javy, to czy też są od tego czasu jakieś takie inne właśnie duże zmiany, o których warto wspomnieć oprócz tych tutaj struktur, tych rekordów, o których powiedziałeś?
No to tak. JDK to jest taka gruba biblioteka, z której się korzysta na co dzień, programując w Javie i tam pojawiło się dużo rzeczy, dużo rozwojów. Przykładowo sam taki prosty typ jak string, czyli ciąg znaków otrzymał wiele metod w Javie 21.
Prowadzili nawet metodę, ale to już bardziej na poziomie klasy charakter, gdzie możemy sprawdzić, czy mamy emoji. Takie już ciekawostki można by powiedzieć, ale wbrew pozorom no to pewnie jest odpowiedź na wymagania dzisiejszego właśnie świata, więc Java nadal jakby się rozwija. Oprócz tego mamy mnóstwo innych opcji na tworzenie lepszego kodu.
Teraz mamy takie nowe switche, nie te stare, które znamy z języka C, tylko właśnie bardziej funkcyjne switche, gdzie możemy rozebrać na pierwsze części taki rekord, o którym mówiłem już i od razu się dostać do pojedynczych pól, czyli to jest tak zwany w Javascriptie na przykład to nazywają destrukturyzację. W Javie akurat takiego słowa nie ma, bo w Javie te rekordy składają się z komponentów, co ciekawe, więc po prostu mówimy o tak zwanych patternach, czyli możemy programować na wzorcach, tak można byłoby to nazwać, więc to jest taka nowość. Oprócz tego to nie wiem, możemy ciągi znaków, które mają wiele linii stosować.
Kiedyś to nie było możliwe, trzeba było zawsze je konkatenować, a teraz już mamy pod tym względem łatwiej. Niestety wycofali tutaj interpolacje stringów właśnie w Javie 23, bo nie mówiliśmy o tym, ale też mamy pewne, jak jest coś wprowadzone do Javy, to jest pewien czas na przyjęcie tego, na standaryzację, czyli na przykład jak jakaś nowa funkcja w języku pojawia się w Javie 12, to ona przez następne na przykład 3 wersje jest w tak zwanym trybie preview, no i to oznacza, że ona może zostać wycofana, więc my możemy stosować te rzeczy, ale nie mamy niestety pewności, czy to nie zostanie wycofane i rzadko się zdarza, że oni coś wycofują, raczej jak już to się zmienia w jakiś sposób API, natomiast co do zasady właśnie tutaj taki przypadek powiedziałbym bolesny, jest związany z tą interpolacją stringów.
Tutaj wytłumaczę, że po prostu można było w ramach pojedynczego stringa wstrzyknąć jakby jakieś zmienne z zewnątrz i to było ok, ale pojawiły się problemy, że to jest zagrożenie dla security, ponieważ ludzie by tego nadużywali na przykład w SQLach, czy w innych miejscach, gdzie no takie rzeczy nie powinno się robić, więc ostatecznie oni się zdecydowali na wycofanie, co ciekawe tej zmiany i ma wyjść jakaś nowa propozycja chyba w Javie 25, tutaj jeszcze to nie jest pewne, więc tak czy inaczej, coś na pewno się pojawi, a oprócz tego tutaj też warto powiedzieć o tym, że Java od samego początku jest wielowątkowa, czyli tutaj bardzo łatwo możemy doprowadzić do tego, że nasz system działa wielowątkowo, no i tutaj bardzo ważna zmiana, która weszła już jako standard w Javie 21, czyli wirtualne wątki, ja o tym mówię na szkoleniach, propaguje to nowość, ponieważ to jest bardzo ważne, ponieważ trzeba zakładać, że Java jest używana głównie do tworzenia systemów, które muszą przetwarzać wielowątkowo dużo rzeczy, czy to jakieś obliczenia, czy po prostu żądania od klientów, czyli po prostu procesowanie tak zwanych requestów, czy to HTTP, czy jakichś innych, więc jakby te lekkie wątki są czymś ciekawym, co może w ogóle zmienić właśnie to, jak podchodzimy do aspektów wielowątkowości w Javie, a także wydajności z tym związanym, ponieważ tutaj jeszcze wspomnę, dotychczas martwiliśmy się polami wątków, pole wątków ograniczały liczbę tych wątków fizycznych, czyli takich, które są rejestrowane na poziomie systemu operacyjnego, a przy wirtualnych wątkach system operacyjny już nie wie o tym, że jest takie coś jak wirtualny wątek, to sama Java wewnętrznie jakby ogarnia i tego może być miliony, podczas gdy tak naprawdę tych standardowych wątków to powiedzmy, aplikacja nie powinna zwykle przekraczać tysięcy, ale nawet przy wielkich aplikacjach, a zwykle zostajemy na poziomie do stu, więc to jest bardzo duża zmiana w języku.
Czyli jakby tym zarządzaniem wątków zarządza bardziej maszyna wirtualna niż system operacyjny. A czy masz jakieś spostrzeżenia co do tego, jak wiele z tych nowych tutaj powiedzmy, funkcji w języku jest jakoś szeroko wykorzystywanych przez programistów, czy to jest tak, że właśnie większość zatrzymała się na tym poziomie Java 8 i jakby w ten sposób programuje, czy to rzeczywiście szerzy się szybko ta informacja o tym, w jaki sposób to programowanie się zmienia, z czego można skorzystać, właśnie zmieniają się te paradygmaty i ludzie rzeczywiście z tego korzystają jakoś szeroko.
Java 8 właśnie wprowadziła funkcyjność i myślę, że obecnie jest to standardem. Ja pamiętam w 2014, 2015 roku tak naprawdę wychodziła Java 8, zaczęła być stosowana w praktyce no i właśnie wtedy pamiętam przyjęcie tych nowości, bo to była totalna rewolucja w języku, było dość trudne, ale co ciekawe obecnie można powiedzieć, że każdy już umie w ten sposób programować, natomiast to, co się pojawia w tych nowszych wersjach języka, może nie jest na tyle rewolucyjne, co w Java 8, ale wciąż jest problem pewnie, żeby zapoznać się z tym, zwłaszcza jeżeli mamy projekty, które niestety pozostają na starszych wersjach języka, więc od czego trzeba zacząć?
To od tego, żeby nie bać się migrować na nowsze wersje Java i tutaj znowu trzeba wrócić do tego, że Java właśnie jest standardem, w sensie zmiany, które zachodzą są wstecznie kompatybilne, czyli tak naprawdę jak mamy coś, co jest napisane nawet w Javie pierwszej, to tak naprawdę to nadal jest kompilowalne i potencjalnie możliwe doruchomienia na Javie 21. Problemem są po drodze frameworki, biblioteki, narzędzia budowania. To są trzy rzeczy, które sprawiają problemy przy migracjach.
No i teraz pytanie jest, jak bardzo jesteśmy od tych rzeczy uzależnieni w projektach? Jeżeli mocno, to niestety jeżeli te frameworki utrudniają przejście na nowszą wersję języka, to wtedy po prostu nie możemy tego w ogóle zrobić, więc często to się wiąże wtedy z totalną refaktoryzacją. W projekcie wiadomo, to oznacza duże koszty, nie każda firma może sobie na to pozwolić, prawda?
Wróćmy może do kwestii wydajności, bo trochę powiedzieliśmy o zmianach w języku, ale wracając do tego głównego wątku, czy jest rzeczywiście jakiś taki obszar zastosowań, w których Java z założenia będzie niewydajna, albo jakieś takie sytuacje, w których jakby z góry jesteśmy skazani na porażkę, tak? Czyli grupy zastosowań, czy właśnie sposób korzystania z języka, który tu przychodzi ci do głowy, który jakby należy po prostu unikać i korzystać z jakiejś innej technologii?
To tak. Myślę, że głównie to wynika, jeżeli gdzieś nie możemy Java zastosować, to właśnie z powodu takiego, że mamy te algorytmy odśmiecania pamięci, bo zwykle wtedy, powiedzmy, musimy taki system pewnie zaprojektować przy pomocy języków CC++, obecnie Rust gdzieś tam się pojawił, no ja nie mam akurat doświadczenia wielkiego w tych językach, natomiast rozumiem, z czego to wynika, czyli powiedzmy, jeżeli mamy restrykcje na wykonanie czasowe pewnych funkcji systemu, czyli to są tak zwane systemy czasu rzeczywistego, no to wtedy na pewno Java tutaj nie powinna być używana, ale obejściem jest stosowanie GraalVM-a. GraalVM jest alternatywną wirtualną maszyną Javy, która tak naprawdę skupia się na tym, że kompilujemy od razu cały kod na poziom natywny, ale wciąż tam występuje ten algorytm odśmiecania pamięci, więc tak czy inaczej, jeżeli mamy bardzo duże restrykcje pod tym względem, no to nadal to nie będzie ok, ale powiedzmy, że na przykład jak mamy jakieś zastosowania właśnie obecnie w klaudzie, gdzie aplikacja musi szybko się uruchomić i jak najszybciej wykonać pewien kod, to wtedy można tego użyć.
Tutaj co ciekawe też w nowych wersjach języka pojawił się tak zwane Epsilon GC, czyli tak zwany Now Operational Garbage Collector, to wtedy zakładamy, że Java nigdy nie zatrzyma się, żeby coś tam odśmiecić z tej pamięci, no i wtedy mamy zapewnione, że ten kod się wykona w tym czasie, który zakładaliśmy, więc można to obejść, ale pewnie, jeżeli mamy duże restrykcje, to już lepiej użyć czegoś innego. No i druga sfera problematyczna dla Javy to jest sfera, która wymaga kart graficznych, ale trwają prace nad tym, żeby jakby zintegrować kod w Javie, z możliwością wykonania na dowolnym powiedzmy CPU albo GPU i to jest projekt, który dopiero jest w trakcie rozwoju, więc być może za rok, za dwa byśmy się spotkali, byśmy mogli wtedy porozmawiać, że jednak to, o czym ja teraz mówię już jest nieprawda, ponieważ możemy na karcie graficznej przy pomocy Java również programować rzeczy. Tutaj wtedy raczej mówimy pewnie o zastosowaniu w sztucznej inteligencji, tak, w tych aspektach, które tego potrzebują. Mhm, okej.
A w takim razie tutaj przechodząc już do jakichś konkretnych dobrych praktyk, tak, czy jakby sposobów, w jaki sposób te aplikacje należy tworzyć, żeby były wydajne, tutaj wcześniej powiedzieliśmy o tym, że są to kilka obszarów, tak, no właśnie samo tworzenie języka, maszyna wirtualna, tak, garbage collector i tak dalej, to czy coś tutaj można właśnie powiedzieć, o znowu właśnie zaczynając od samego programowania, o takich dobrych praktykach, które powodują, że dzięki temu dobrze współpracujemy z tą maszyną wirtualną, z tym garbage collectorem. Wspomniałeś przez chwilę o tym, żeby właśnie tutaj funkcje przez nas stworzone były niezbyt długie, żeby ten just-in-time compiler sobie z tym poradził, czy tutaj możemy powiedzieć więcej no właśnie. jak to wygląda, tak, w sensie krótkie to znaczy, jak krótkie, jak to mniej więcej należy rozumieć.
No to nie jest proste, ponieważ właśnie to, co my widzimy jako krótkie w kodzie maszynowym, może zajmować, nie wiem, miliony linii kodu, powiedzmy. Natomiast możemy oczywiście weryfikować, czy wirtualna maszyna Java, tak naprawdę, czy ten JIT potrafi skompilować nasz kod. Do tego po prostu trzeba zastosować albo profilera, albo włączyć logowanie informacji o tym, jak przebiega kompilacja.
Tutaj ta kompilacja w ogóle przebiega na czterech poziomach. Ten najwyższy to odpowiada temu, co chyba w C++ można osiągnąć przy „-o3”, czy coś w tym stylu. No i chodzi o to, że wtedy po prostu kod się kompiluje dość długo, ponieważ taka metoda może nawet się kompilować przez jedną milisekundę, co by oczywiście blokowało wykonanie tego kodu na jedną milisekundę.
Więc w praktyce jak to działa, to się dzieje właśnie na wielu, po pierwszych poziomach. Te pierwsze poziomy prowadzają najprostsze optymalizacje, ale są OK dla takich metod, które rzadziej się wykonują. Natomiast jeżeli coś ciągle musi być wykonywane, no to ostatecznie dojdzie do tego ostatniego poziomu.
No i teraz ta kompilacja dzieje się na dodatkowych wątkach. W ogóle jak my startujemy proces w Javie, to funkcja main to jest tylko jeden właśnie z wielu wątków, które startują razem właśnie z wirtualną maszyną Java, bo mamy tam oczywiście wątki dotyczące odśmiecania pamięci, ale też właśnie te, które dotyczą Jita. No i właśnie to wygląda tak, że jak jakaś metoda się skompiluje, to natrafia do takiego cacha na ten kod natywny i dostęp do tego cacha jest zapewniony no bardzo dobrze, ponieważ to jest, siedzi wszystko w pamięci RAM, ale jest to dosyć nieduży obszar, który można poszerzyć, jeżeli nasza aplikacja właśnie jest takim ciężkim monolitem, to warto sprawdzić, czy nie przekraczamy tego cacha, bo co się stanie, jeżeli przekroczymy rozmiar cacha na ten kod skompilowany, no to po prostu te najrzadziej używane funkcje z niego niestety zostaną usunięte i możemy dochodzić do takiego efektu, że ten cach będzie się ciągle zmieniał, czyli liczba tak zwanych hitów będzie no tam ograniczona i jakby nasz system może przez to stracić na wydajności, więc na to trzeba uważać, czyli pierwsza rzecz, na co warto w ogóle zwrócić uwagę to na to, czy nie przekraczamy tego cacha, raczej rzadko do tego dochodzi, bo naprawdę nasz system musi być dosyć sporych rozmiarów, chociaż to mówię, zależy od tego, jak potem wygląda ten kod natywny, więc po pierwsze możemy to sprawdzić przy pomocy profilera, w profilerze mamy dokładne wtedy informacje, że jeżeli są jakieś hotspoty, to na jakim poziomie ta metoda została skompilowana.
No i wtedy możemy zobaczyć właśnie, gdzie są ewentualne problemy, co powinniśmy zoptymalizować, co powinniśmy zrefaktoryzować, bo po prostu na przykład widocznie nawet w tej metodzie pewna tylko część powinna być zoptymalizowana dość mocno, powiedzmy na tym najwyższym poziomie, a może to wszystko, co się dzieje dookoła, wcale tego nie potrzebuje, także warto te metody pisać, właśnie jak najmniejsze. Kiedyś, pamiętam, spotykałem się z takim pojęciem, że metoda w Javie nie powinna przekraczać 10 linii kodu, ale myślę, że obecnie to już nie jest prawda. I nawet kiedyś pewnie to nie była prawda.
To po prostu mówiono tak, żeby ludzie zwracali na to uwagę. Bardziej o to chodzi niż o to, jak to potem wygląda w praktyce. Ale no to są takie, powiedzmy, proste rzeczy, czyli mówimy tutaj o tym, że mamy możliwość kompilacji do kodu natywnego, ale ważne jest też to, co ostatecznie w kodzie robimy oczywiście.
No i tutaj metody programistyczne głównie skupiają się na tym, żeby zaobserwować, czy nie przesadzamy z alokacją pamięci. I najczęściej, co się pojawia w naszym kodzie, to ciągi znaków, no i typy liczbowe, powiedzmy. To są takie podstawy.
No i teraz w Javie mamy możliwości optymalizacji obu rzeczy. Po pierwsze, jeżeli chodzi o ciągi znaków, możemy je internować, czyli to jest proces, który powoduje, że zawartość takiego obiektu trafia też do takiego cacha w ramach sterty. Kiedyś w poprzednich wersjach Javie, powiedzmy przed Java 8, to była sterta, no to nie było w ramach sterty, tylko to się działo poza stertą.
Można było tym zarządzać. Obecnie postanowiono, że w sumie nie ma to sensu i lepiej wszystko trzymać w obrębie sterty. No i te internowane ciągi znaków wtedy są unikalne, czyli jeżeli nam się powtarza coś w kodzie, ale to nawet nie musi być akurat coś, co zapiszemy jako ciąg znaków bezpośrednio w kodzie, tylko to może być np. coś, co przychodzi z jakiegoś API albo z bazy danych. Jeżeli to zinternujemy, no to wtedy mamy ograniczenie wpływu na właśnie tych ciągów znaków na pamięć. Tak dokładnie na to, że one są zduplikowane na stercie.
Tylko, że problem jest taki, że z tym nie można przesadzać, czyli używamy tej funkcji tam, gdzie mamy, powiedzmy, takie dane słownikowe zapisane w formie ciągów znaków, ponieważ możemy przekroczyć tego cache’a, oczywiście go znowu można rozszerzyć, ale nie ma sensu internować czegoś, co i tak się nie powtarza, prawda?
Ale to jak rozumiem, to trzeba jakby dodatkowo zadbać o to, żeby jakiś konkretny ciąg był w tym mechanizmie scashowany, tak?
Trzeba wywołać jedną metodę wtedy dodatkowo, czyli jak mamy stringa, to wywołujemy intern i to wszystko tak naprawdę. Ale tutaj, co ciekawe, zauważono, że w sumie właśnie może programista nie powinien o tym decydować i od Java 11 mamy w standardzie mechanizm na poziomie garbicz kolektora, który pozwala wykrywać te duplikaty i samodzielnie on je próbuje wtedy połączyć, tylko to nie jest na tyle efektywne, ponieważ no tu chodzi o czas wykonania. Te porównywania tych ciągów znaków mogą trwać dosyć długo, więc tam mamy pewne ograniczenia, więc ostatecznie dobrze to może włączyć dla większych aplikacji, które dosyć intensywnie korzystają z tych ciągów znaków, ale no trzeba też uważać na to, czy to na pewno dobrze działa, czyli zweryfikować, czy ostatecznie mamy jakiś pozytywny rezultat.
No i teraz wróćmy jeszcze do typów liczbowych, ponieważ to jest ciekawa rzecz, że od dłuższego czasu stosujemy typy opakowane, głównie w Java, czyli jakby te typy prymitywne, czyli takie standardowe inty, floaty, double, no to my tego nie powinniśmy używać w kodzie biznesowym, ponieważ to jest zbyt niski poziom, a niestety w wielu miejscach jest tak, że my robimy jakieś przeliczenia na tych liczbach, czyli chcemy, żeby to się jak najszybciej wykonało, no i wtedy trzeba wrócić jednak do tych typów prymitywnych, żeby po prostu to mogło być zoptymalizowane na poziomie procesora, tak, ponieważ te typy opakowane po pierwsze będą zajmowały więcej przestrzeni w pamięci, no a po drugie one muszą być sprowadzane i tak do takiego poziomu prymitywnego, jak się wykonuje na poziomie natywnym.
Więc jeżeli nie mamy właśnie wymagania, że jakaś liczba może nie istnieć, to ja bym preferował, żeby zawsze w klasach, jeżeli dajemy takie pole, a na pewno w tych rekordach, o których mówiłem, umieszczać typy prymitywne zamiast typów opakowanych. To jest naprawdę, daje dużo. I teraz jeszcze, jaki problem może się pojawić, to w obrębie kolekcji niestety nie mamy tutaj możliwości w tych standardowych kolekcjach w Javie używania typów opakowanych, kiedy nie typy tablicowe mogą być właśnie typami prymitywnymi związane.
Więc no tutaj możemy użyć dodatkowych bibliotek. Ja osobiście proponuję używać takiej biblioteki, która się nazywa Fast Util, więc nie bez powodu to jest fast, no bo o to chodzi, żeby po prostu mieć kolekcje, które rozszerzają te już istniejące. Czyli my nie uczymy się czegoś zupełnie na nowo, ale mamy do dyspozycji na przykład mapek, w której kluczami są typy prymitywne int, tak?
No tego typu rzeczy i to w takich gorących miejscach właśnie w kodzie warto myślę tego użyć, tak?
Pomniałeś o korzystaniu z profilera. No tam, jak się tutaj znajdujemy, te hotspoty, miejsca, które są jakoś tam najczęściej wykonywane. I co możemy wtedy zrobić?
Przyjrzeć się takiej funkcji i zastanowić, co tam zoptymalizować, jak rozumiem, tak? Czyli właśnie, że gdzieś tam stosujemy jakiś nieoptymalny algorytm, albo właśnie korzystamy z, nie wiem, z tych opakowanych typów liczbowych i zamieniamy je wtedy na takie prymitywne, tak?
Tak, tak, tak. To jeżeli chodzi o profilery, to mamy tutaj powiedzmy dwie grupy profilerów. Jedne bazują na instrumentalizacji kodu i to jest starsze podejście, ponieważ wtedy polega to na tym, że ten profiler wchodzi w ten kod, powiedzmy binarny i na tym się zapina, zbierając informacje.
No i wtedy mamy problem, że to może kosztować, więc dla badania jakichś algorytmów, które faktycznie muszą się szybko wykonywać, taki profiler może spowodować, że on ma wpływ na te czasy wykonania. Obecnie od Java 11 mamy już właśnie inny standard dostępny i to są tak zwane asynchroniczne profilery. No i najłatwiej z tego skorzystać przy pomocy Java Mission Control.
To jest darmowe oprogramowanie, podobne do, powiedzmy wizualnie do Eclipse’a. Może nie za piękne, ale nie o to w tym chodzi, bo chodzi o to, że to nam dostarcza naprawdę dużo cennych informacji, czyli mamy dokładne informacje na temat czasu wykonywania, ale też są rysowane tak zwane flame grafy, czyli wtedy widzimy dokładnie cały stos, który doprowadził do tego, że ta metoda się tyle razy wykonuje, czyli od samego początku, aż do tej metody i widzimy dzięki temu na przestrzeni wybranego czasu, jakie metody faktycznie najczęściej się wykonują i możemy je optymalizować. No i oprócz tego, co możemy w takim profilerze zobaczyć, to to, czy na przykład występują wyjątki, które są po cichu ukrywane.
To jest ciekawy aspekt, ponieważ wyjątki są kosztowne. Z tego wiele osób sobie może nie zdawać sprawy, ponieważ to się wydaje takie naturalne, że w wielu miejscach w Javie mamy wyjątek, który trzeba złapać, ale najgorszym właśnie podejściem jest stosowanie wyjątków biznesowych, tak zwanych z innych możliwości powiedzenia, że ta metoda ma coś zwrócić innego, niż wartość oczekiwano. No i niestety to jest zły pomysł, tak?
Funkcyjne programowanie w ogóle z tego rezygnuje, czyli jak stosujemy te aspekty funkcjonalności w Javie, to w ogóle nie powinniśmy tych wyjątków na pewno samodzielnie rzucać, ponieważ no niestety połączenie na przykład z bazą danych może spowodować wyjątek i to jest normalne, ale chodzi o to, żeby nasz kod, który właśnie wykonuje się, nie wiem, milion razy nie rzucał co tysięczny raz jakiegoś wyjątku, bo to spowalnia niestety działanie, czyli to jest kolejny aspekt programistyczny, należy unikać po prostu wyjątków.
Jeżeli coś takiego, chcemy coś takiego zaprogramować, to powinniśmy inaczej to zrobić, czyli mieć jakiś typ opakowany w obrębie naszego oczekiwanego rezultatu, ale też dodatkowo jakąś informację w tym typie, która mówi, że coś tu poszło nie tak i w funkcyjnym programowaniu to się zwykle, to taka jest abstrakcja, że mamy left-right, taką parę, czyli jest lewa strona, zwykle jest tym, co jest poprawne, a prawa strona reprezentuje wynik działania, który spowodowałby wyjątek pewnie, tak? No i co tutaj jeszcze, na co warto zwrócić uwagę, to to, że ten profiler nam też powie, ile obiektów, w jakim czasie zostało stworzone w obrębie tych poszczególnych metod.
Mamy dokładne informacje, że ta metoda spowodowała, że powstało, nie wiem, tysiąc obiektów tego typu. W takim czasie tam oczywiście mamy oś czasu, możemy to sobie ograniczać do dowolnych momentów, które nas interesują, więc naprawdę tych informacji taki asynchroniczny profiler dostarcza mnóstwo i ja proponuję, żeby z tego korzystać. Jeden minus jest taki, że trzeba przejść na tą nowszą wersję Javy, która to wspiera.
Pierwsza wersja, która już zaczęła to wspierać, to była Java 8, ale tam jeszcze nie było to na tyle rozwinięte, jak właśnie zostało to rozwinięte od Jawy 11, więc mamy coś fajnego i to naprawdę dużo daje, żeby zweryfikować wydajność i problematyczne miejsca w kodzie. Ok.
To mamy tutaj tę warstwę kodu, to jakby sposób programowania profiler, natomiast wspomniałeśmy też na początku właśnie jeszcze o tych kwestiach związanych z samą maszyną wirtualną, z garbeczko-lektorem. Czy tutaj coś można jeszcze powiedzieć właśnie, co jest istotne z punktu widzenia wydajności? Na przykład mówiłeś o różnych garbeczko-lektorach i jakimś tam właśnie jednym z nich, który dobrze się sprawdza przy dużych aplikacjach, to od czego to zależy?
Od jakby sposobu miejsca, gdzie nasza aplikacja działa, nie wiem, od rozmiaru aplikacji, od jakichś warunków, czy tutaj rzeczywiście mamy jakieś opcje do wyboru, jeśli chodzi o garbeczko-lektory?
To powiedzmy, że tutaj garbeczko-lektor musimy dobrać do tak naprawdę sprzętu albo wirtualnej maszyny, na której uruchamiamy naszą aplikację, czyli przykładowo, jeżeli mamy, zwłaszcza pewnie w obszarach cloudowych, te maszyny są dosyć nieduże i zwykle tych procesorów tam jest też mało, no to wtedy nie ma sensu wybierać jakiegoś zaawansowanego garbeczko-lektora, który działa wielowątkowo, ponieważ i tak to zostanie ograniczone przez liczbę dostępnych CPU.
Więc mamy do dyspozycji albo właśnie taki najprostszy garbeczko-lektor jednowątkowy, który istnieje od początków istnienia Java, natomiast oczywiście większość aplikacji jednak działa wielowątkowo, no i tutaj mamy więcej opcji do wyboru. I teraz w zależności, czy nasza sterta jest duża czy mała, wybieramy jeden lub inny. No i te nowsze właśnie garbeczko-lektory powstały po to, żeby minimalizować czas, powiedzmy, zatrzymania, chociaż nie do końca to tak należy rozumieć, ale można tak uprościć, że one powodują, że nasz kod przestaje działać maksymalnie na przykład przez 10 milisekund, ten czas nie może trwać dłużej i co się uda zrobić przez ten czas, to się uda.
Zwykle to tam jest na tyle sprytnie zrobione, że powinno dawać sobie radę z tak dużymi stertami, jak na przykład nawet jeden terabajt. To jest ciekawe. No twórcy twierdzą, że to działa dobrze i właśnie to ograniczenie 10 milisekund zostanie utrzymane.
Więc wtedy wpływ na działania aplikacji powinien być znikomy. No i faktycznie w moim doświadczeniu, w jednym miejscu mieliśmy taki przypadek, gdzie mieliśmy aplikację, która faktycznie zużywała ponad 100 gigabajtów sterty, no i wtedy przejście na ten nowy garbeczko-lektor dał dużo, w sensie aplikacja zaczęła być responsywna, a wcześniej były problemy właśnie przez to, że ten starszej klasy, powiedzmy, garbeczko-lektor zatrzymywał aplikację nawet na jedną sekundę. No to to już jest zauważalne dla użytkownika systemu, prawda?
Ok, czyli tutaj mamy jakieś możliwości wyboru, a w kwestii samej maszyny wirtualnej, czy jakichś ustawień, czy właśnie kwestii pamięci, tutaj mówimy o stercie, o cashach, no to jak rozumiem, też wszystko podlega optymalizacji i doboru do sytuacji, w której się znajdujemy, tak?
Więc teraz tak, jeżeli używamy w aplikacji jakby liczb tych opakowanych, musimy z nich korzystać, bo niestety na przykład mamy je narzucone przez model bazodanowy, najczęściej tak to wygląda, no to wtedy możemy zoptymalizować typ integer stosując cashowanie, tak? Domyślnie typ integer na przykład jest cashowany w obrębie minus 127 do 128, czyli jeżeli liczba pochodzi z tego zakresu, to ona zawsze jest wyciągana z casha. Natomiast możemy to rozszerzyć, to prawą stronę, niestety lewej nie możemy, więc jak mamy pojemne liczby, nie wiem, z czego to wynika, może to ma jakieś znaczenie, ale zawsze możemy prawą stronę rozszerzyć nawet do samego krańca tego integera, pewnie to nie ma sensu dla typowych przypadków, ale to już pozwala na przykład właśnie zoptymalizować wykorzystanie sterty.
Jeżeli chodzi o stringi, to mówiłem o tym, że mamy internowanie albo możemy włączyć w garbage kolektorze opcję wyszukiwania powtarzających się ciągów znaków, a tu jeszcze powiem, że jest taka ciekawostka, dlaczego warto przejść jawy ósmej już wyż, ponieważ w ogóle ciągi znaków zostały zoptymalizowane pod względem tego, że obecnie już nie są definiowane przez tablice typu char czy char, jak kto woli, tylko przez tablice bajtów. I o co chodzi?
Chodzi o to, że większość napisów to jednak mieści się w tak zwanym łacińskim alfabecie, prawda? Więc po co trzymać te napisy po dwa bajty, bo typ char ma, zajmuje dwa bajty, skoro można to zapisać po jednym bajcie. No i teraz przechodząc już w ogóle na nowszą wersję jawy, możemy zauważyć, że jeżeli faktycznie w naszej aplikacji te ciągi znaków głównie stanowią ten alfabet łaciński, no to w takim przypadku pewnie można na 20-30% pamięci zaoszczędzić, tak się przynajmniej mówi, więc no warto z tego skorzystać.
Jeżeli chodzi o samego garbicz kolektora, to trzeba teraz zwrócić uwagę na to zwykle, ile dajemy przestrzeni na startę, bo oczywiście możemy optymalizować te inne aspekty, ale najważniejsze jest to, ile ustawimy tak zwanego xmxa, czyli ile maksymalnie java może, wirtualna maszyna jawy może zaalokować na poziomie systemu operacyjnego. No i teraz, jeżeli przypiszemy za mało, to nasza aplikacja będzie musiała bardzo często odśmiecać pamięć, więc to może mieć negatywny wpływ. Więc zawsze trzeba wybrać coś, co jest jakimś kompromisem.
Jeżeli przesadzimy znowu, no to java to zauważy, w sensie ten garbicz kolektor to zauważy, no i nie będzie odśmiecał pamięci, bo w sumie, po co ma to robić, skoro jeszcze wolnego jest, nie wiem, 60% tego ustawienia, które tam skonfigurowaliśmy. Więc co do zasady, to nie jest proste, trzeba obserwować właśnie na przykład przy użyciu profilera, czy ogólnie metryk, bo to można robić na poziomie systemu operacyjnego bardziej i wtedy zobaczyć, czy faktycznie nie przesadzamy z tym ustawieniem, albo czy ono nie jest za małe. Zwykle jest tak, że jeżeli jest za małe, to albo jak dojdzie do katastrofy, to mamy out of memory exception, a jeżeli jednak garbicz kolektor sobie z tym radzi, to będziemy mieli zmniejszoną wydajność aplikacji.
No i wtedy trzeba znowu profilera zastosować. W profilerze mamy statystyki dostępne dotyczące tego, ile czasu garbicz kolektor zabiera nam w trakcie działania naszej aplikacji.
Powiedzieliśmy, że przez te lata powstało wiele też alternatywnych garbicz kolektorów, że tutaj jest jakaś tam właśnie swoboda w wyborze, a czy w kwestii maszyny wirtualnej też coś się zmieniło, że tutaj są jakieś opcje do wyboru, z których możemy skorzystać?
Tak, tutaj myślę, że warto powiedzieć o tym, że firma Oracle wcale nie jest taka straszna pod względem tego, że oni za wszystko chcą brać pieniądze, powiedzmy. I tutaj faktycznie JDK i wirtualna maszyna Javy jest open source’owa i tak będzie na zawsze. I obecnie mamy do wyboru, do koloru pod tym względem, czyli liczba tych opcji w zależności od firmy, w zależności, nie wiem, od clouda, z którego może korzystamy, jest dosyć duża, czyli nawet Microsoft, który walczył z Javą, obecnie wypuszcza swoją własną wersję wirtualnej maszyny Javy, bo co ciekawe na poziomie kodu zwykle to się niczym nie różni, ale o tym może za chwilę powiemy.
Tylko raczej chodzi o to, że te korporacje zwykle wprowadzają optymalizację pod swoje właśnie te chmurowe serwery i zastosowania. Natomiast tutaj jeszcze warto właśnie w takim razie rozszerzyć to, że czasami możemy wybrać inną wirtualną maszynę Javy, żeby mieć jakiś bonus na wydajności. No i po pierwsze na przykład możemy zastosować tak zwany mechanizm Crack, to się pisze CYRY-AC i to wykorzystuje linuksowe tak naprawdę pod spodem opcje do tego, żeby wprowadzić aplikację w stan hibernacji, czyli po prostu tak naprawdę cała pamięć, która dotyczy tego kodu i tych obiektów jest w danym momencie persystowana gdzieś na dysku i następnie możemy błyskawicznie taką aplikację uruchomić ponownie. I to jest przydatne, jeżeli właśnie chcemy ograniczyć czas, tak, na start aplikacji, a jednocześnie nasza aplikacja no jest sporych rozmiarów, czyli na przykład uruchamia się tam kilka minut, bo nawet nie chodzi o to, że sama Java wtedy się uruchamia kilka minut, tylko inicjalizacja wszystkich rzeczy tyle zajmuje. Więc na tym możemy zaoszczędzić, ale też mamy inne dostępne mechanizmy takie, jak na przykład GraalVM, o którym mówiłem.
To jest też taka alternatywna wirtualna maszyna Javy, tylko tu się koncentrujemy na tym, że od razu cały kod w Javie sprowadzamy do kodu natywnego. I tam trzeba wtedy na to niestety uważać, czy nie używamy gdzieś refleksji javowej, bo wtedy trzeba takie rzeczy zwykle przepisać albo nauczyć tego przez GraalVM, jak ma się zachowywać, jak napotyka taki kod, bo standardowo on po prostu nie będzie wiedział, co tam powinno się wydarzyć. Dlaczego?
Dlatego, że jak już schodzimy na poziom kodu natywnego, no to tam nie mamy takiej plagi nowości, powiedzmy, że nagle możemy dodać jakąś nową klasę, załadować ją do pamięci i wtedy co? Trzeba byłoby ją skompilować znowu na tym poziomie natywnym, prawda? Więc to nie ma sensu i właśnie jeżeli faktycznie nasza aplikacja dość mocno korzysta z tej refleksji, to pewnie tego się nie da wtedy zastosować.
Ale jeszcze oprócz tego mamy też wirtualne maszyny Javy, które optymalizują wykorzystanie np. kart graficznych właśnie do wykonania obliczeń. Zwykle tutaj mówimy właśnie o jakichś zaawansowanych obliczeniach na matrycach.
Pewnie chodzi tutaj nam zwykle o jakieś algorytmy z reguły dotyczące sztucznej inteligencji, ale nie tylko, ponieważ chodzi po prostu o wyszukiwanie, nie wiem, podobieństwa, czyli tak zwane wektorowe, wtedy porównywanie tam jakichś obiektów. No i w takim przypadku możemy zoptymalizować naszą aplikację poprzez zastosowanie np. Tornado VM.
To jest jeden z takich typowych przykładów, gdzie mamy dostępne specjalne dodatkowe opcje nad wirtualną maszynę Javy, które spowodują, że ten kod piszemy co prawda w Javie, ale on zostanie wykonany w obrębie karty graficznej, a nie CPU. No i wtedy w ogóle możemy oczywiście wybierać, że tylko ten kawałek wykonujemy na karcie graficznej, a reszta oczywiście normalnie dalej na CPU działa. Ale tutaj myślę, że niestety pewnie ten przypadek Tornado VM w przyszłości zostanie zastąpiony przez standard w Javie, bo o tym już wspominałem, że trwają prace nad tak zwanym Project Babylon.
Bez powodu tak się nie nazywa, bo chodzi o to, żeby kod mógł się wykonać w dowolnym miejscu, na dowolnej jednostce powiedzmy, która potrafi wykonać kod. Nawet tu mówimy o FPGA, nie tylko o kartach graficznych, czy o standardowych CPU, tak?
Okej, to ciekawa perspektywa. No to myślę, że tutaj przeszliśmy te poszczególne obszary, które jakąś tam mogą podlegać optymalizacji i powiedzieliśmy sporo o tych kwestiach wydajnościowych. To może jeszcze pod koniec, bo powiedziałeś, że tam czasem właśnie też korzystasz z tych innych języków na maszynie wirtualnej Javy, czy masz jakiś, nie wiem, ulubiony, czy jest jakiś taki element tych języków, który byś chciał, żeby się znalazł w Javie, że jest tam coś takiego, co faktycznie się przydaje w takiej codziennej pracy?
No więc tak od początku tak naprawdę mojej kariery, doświadczenia z programowaniem w Javie, obok miałem Clojure, no bo pracowałem w firmie, gdzie był Clojure używany, obok Javy. No i co do zasady dla juniora to było straszne, powiem tak. Clojure jest językiem, który bazuje na lispowej składni, czyli mamy tak zwane odwrócono polską notację, chyba tak to się nazywa.
Więc to jest w ogóle inne podejście do programowania. No i właśnie zwłaszcza dla osób, które dopiero wchodzą w świat programistyczny, to nie polecałbym zaczynać na pewno od tego języka, no chyba, że na studiach macie jakieś już przedmioty, które wprowadzają w te aspekty. Dla mnie żadnych benefitów stosowania tego języka za bardzo nie ma.
Natomiast oczywiście oprócz tego w międzyczasie mieliśmy język Groovy, który bazował na tym, że on nie próbował być totalną alternatywą nad Javą, tylko dodawał nowości. I co ciekawe był też, no nie był aż tak statycznie kompilowany jak Java, chociaż obecnie Java też już nie jest, bo możemy na przykład stosować vary w ramach funkcji, zamiast zawsze pisać, nie wiem, string, string, string, tak. No a Groovy już to robił wcześniej, dawał taką możliwość.
I przez pewien czas ten język faktycznie był alternatywą, ale obecnie można powiedzieć, że to umarło, ponieważ pojawiła, pojawił się Kotlin, który no zastąpił powiedzmy w wielu obszarach pewnie Grooviego. Jeżeli chodzi o skalę, no to też jest to język, którego używam i używam, ale w mniejszym zakresie, bo to się sprawdza w aspektach big data, czyli tam, gdzie mamy przetwarzanie, procesowanie danych. Tam skala się sprawdza, ponieważ ma wiele takich funkcyjnych gotowców, które bardzo sprawnie pozwalają przeprocesować dane.
Więc w tym miejscu skala pewnie jest okej, chociaż trzeba powiedzieć, że Java właśnie coraz więcej rzeczy przejmuje ze skali też, więc coraz mniej ma to sens używać, chociaż skala trójka totalnie zmienia podejście do tego, co było poprzednio, ponieważ ja bym powiedział, że ona stała się bliska stylowi znanemu z Pythona, czyli mamy możliwość zapisywania kodu w taki sam sposób jak w Pythonie, czyli nie używamy w ogóle żadnych klamer, tylko piszemy po prostu kod, który ma jaki begin-end, tego typu rzeczy. No i kto co lubi, tak? Ale to jest kwestia bardziej syntaksu niż czegoś, co ma wpływ na wydajność, ponieważ zwykle jednak możemy powiedzieć, że kod napisany bezpośrednio w Javie będzie najlepszy pod względem wydajności.
No ale kiedyś tam ja tak podchodziłem do tego, że faktycznie fajnie było mieć możliwość korzystania z tych języków, bo dawały coś na plus. Obecnie tych rzeczy na plus jest coraz mniej i nawet w przypadku Kotlina, który modny zaczął być chyba w 2017 roku, obecnie bym powiedział, że nie ma za bardzo sensu z niego korzystać jako zamiennika Javy. Jeżeli mamy jakieś przypadki, że faktycznie Kotlin nam się przyda, ponieważ musimy projektować DSL, czyli takie własne podjęzyki, no to to jest idealny przypadek zastosowania Kotlina, bo wtedy możemy w ramach Kotlina stworzyć swój własny, biznesowy jakiś na przykład język.
No i na przykład osoba nietechniczna będzie mogła bardzo szybko się tego nauczyć i zacząć z tego korzystać, czego przy Javie byłoby pewnie trudniej. No i teraz dlaczego ja mówię o tym Kotlinie tak źle, powiedzmy, bo ja się trochę na tym zawiodłem pod tym względem, że oni twórcy nie trzymają niestety zasad wstępnej kompatybilności. W Kotlinie 2.0 postanowiono zastosować w ogóle inny kompilator, ponieważ w ogóle Kotlin chorował na problem czasu kompilacji kodu, bo tak naprawdę kod z Kotlina jest skompilowany do Javy, a potem z Javy dopiero do tego kodu bajtowego, więc mamy dwa poziomy, więc niestety to działało wolno, a teraz mamy niby szybciej, ale za to praktycznie nie można przejść takich typowych przypadków na nową wersję Kotlina bez totalnych zmian w projekcie, ponieważ większość bibliotek czy tam frameworków bazuje na tej starszej wersji kompilatora, więc jesteś zablokowany, tak naprawdę.
Coś, co w Javie jest nie do pomyślenia, ponieważ w Javie zawsze możesz skompilować ten sam kod na nowszej wersji Javy, a tu nie. Tu niestety mamy duże zmiany i w ogóle w samym języku też jest takie podejście, że można pewne struktury językowe usunąć w pewnym momencie, to się już działo w Kotlinie. Skala jest pod tym względem podobna i właśnie dlatego dla mnie Java, mimo że przez pewien czas, możemy powiedzieć, była, odstawała, jeżeli chodzi o te aspekty dotyczące właśnie syntax sugar, to obecnie już to nadrobili, a z drugiej strony te inne języki właśnie nie dają takiej stabilności, której ja bym oczekiwał jako już architekt, a nie programista, który się hype’uje na tym, że coś fajnie można zapisać, prawda?
Także to jest ciekawy aspekt, ale mówię, w jakimś minimalnym zakresie w projektach można z tych języków korzystać. Także tam, gdzie to akurat pasuje, jak najbardziej jest to na plus. Oczywiście ja tu nie mówię o świecie Androida, bo tam Kotlin jest podstawą, ale to wynika z polityki, że Google się pokłócił z Oracle’em, a nie z tego, że Java tam nie mogła być dalej stosowana.
Ok, dzięki za ten przegląd tego co się na tej maszynie wirtualnej. Myślę, że zrobiliśmy fajny przegląd możliwości języka. Dzięki za rozmowę Mateusz i do zobaczenia następnym razem, kiedy pojawią się kolejne funkcje, które będziemy mogli omówić.
Komentarze