12.000 reči, 30 ručno crtanih ilustracija — sveobuhvatan pregled ključnih tačaka JVM znanja
1. Osnovni pojmovi
1.1 OpenJDK
Od izdanja JDK 1.0 1996. godine, kompanija Sun je u velikim verzijama objavila JDK 1.1, JDK 1.2, JDK 1.3, JDK 1.4, JDK 5 i JDK 6; svi ti JDK-ovi se mogu zajednički nazvati SunJDK.
Zatim, na konferenciji JavaOne 2006. godine, kompanija Sun je objavila da će Java biti open source, te je u narednoj godini i više postepeno otvarala različite delove JDK-a pod licencom GPL v2 (GNU General Public License, verzija 2) i osnovala OpenJDK organizaciju radi nezavisnog upravljanja tim kodom. To je poreklo OpenJDK-a; tada je OpenJDK posedovao gotovo sav kod tadašnjeg sunJDK 7.
1.2 OracleJDK
Tokom razvoja JDK 7, zbog uticaja različitih razloga, tržišna vrednost kompanije Sun je neprestano padala i nije više bila u stanju da unapređuje razvoj JDK 7, pa je izdanje JDK 7 stalno odlagano.
Nakon toga, 2009. godine, kompaniju Sun je kupila kompanija Oracle; da bi rešila problem dugog odlaganja JDK 7, Oracle je veći deo nezavršenih projekata iz JDK 7 odložio u JDK 8, te je 2011. godine objavila JDK 7. Nakon toga, JDK verzije koje Oracle redovno objavljuje preimenovane su iz SunJDK u Oracle JDK.
Nakon izdanja JDK 9 2017. godine, kompanija Oracle je objavila: ubuduće će JDK objavljivati po jednu veliku verziju u martu i septembru svake godine, odnosno po jednu veliku verziju na pola godine, s ciljem izbegavanja rizika od neprepoznaje do kojeg bi došlo vezivanjem brojnih funkcija za jednu verziju JDK-a.
Nakon izdanja JDK 11, Oracle je sinhronizovano prilagodio komercijalnu licencu JDK-a i objavio da će, počevši od JDK 11, sve prethodne komercijalne karakteristike biti open source ustupljene OpenJDK-u, čime su kod i funkcionalnost OpenJDK 11 i OracleJDK 11 u suštini postali potpuno isti.
Istovremeno je objavljeno i da će se ubuduće objavljivati dve verzije JDK-a:
- jedna je OpenJDK koji je Oracle otvorio pod licencom GPLv2 + CE;
- druga je OracleJDK koji se redovno objavljuje pod OTN licencom.
Oba dele većinu izvornog koda i gotovo su istovetna po funkciji. Jedina razlika jeste u tome što se Oracle OpenJDK može koristiti u razvojnim, testnim ili produkcionim okruženjima, ali ima samo pola godine ažurirajuće podrške; dok je OracleJDK besplatan za pojedince, ali se u produkcionim okruženjima naplaćuje za komercijalnu upotrebu i može imati ažurirajuću podršku u trajanju od tri godine.
Trenutno najnoviji JDK sa dugoročnom podrškom (LTS) jeste JDK 21; detalje možete pogledati u postu prijatelja why technology.
1.3 HotSpot VM
On je podrazumevani virtuelna mašina u Sun/Oracle JDK-u i OpenJDK-u, a i trenutno najšire korišćena virtuelna mašina.
Prvobitno su ga dizajnirali i izmislili u kompaniji Longview Technologies, koju je kompanija Sun kupila 1997. godine; zatim je kompanija Sun, prilikom otvaranja SunJDK-a 2006. godine, zajedno otvorila i HotSpot virtuelnu mašinu.
Nakon što je Oracle kupio Sun, osnovan je HotRockit projekat, te su odlične karakteristike JRockit virtuelne mašine kompanije BEA (koju je Oracle takođe kupio) integrisane u HotSpot.
HotSpot je u tom procesu uklonio trajnu generaciju i apsorbovao funkcije poput alata za nadzor Java Mission Control iz JRockit-a.
Do izdanja JDK 8, u upotrebi je HotSpot VM koji objedinjuje prednosti oba.
Na svom računaru možemo koristiti java -version da dobijemo informacije o JDK-u:

2. Java memorijske oblasti
Java memorijske oblasti smo već obradili; ovde ih ponovo pređimo.

2.1 Programski brojač
Programski brojač (Program Counter Register) jeste manji memorijski prostor; on se može posmatrati kao indikator broja linije bytecoda koji trenutno izvršava data nit.
Interpretator bytecoda menja vrednost programskog brojača radi odabira sledeće bytecode instrukcije koju treba izvršiti; osnovne funkcije poput grananja, petlji, skokova, obrade izuzetaka, oporavka niti itd. zavise od ovog brojača.
Svaka nit poseduje svoj nezavisan programski brojač; brojači različitih niti se ne međusobno utiču i čuvaju se nezavisno.
2.2 Stek virtuelne mašine
Stek virtuelne mašine (Java Virtual Machine Stack) takođe je privatno vlasništvo niti; on opisuje memorijski model niti za izvršavanje Java metoda: prilikom izvršavanja svake metode, Java virtuelna mašina sinhronizovano kreira jedan okvir steka, koji služi za čuvanje tabele lokalnih promenljivih, steka operanada, dinamičkih veza, izlaza iz metode i sl.
Od poziva metode do njenog završetka odgovara procesu okvira steka od guranja na stek do skidanja sa steka. U "Specifikaciji Java virtuelne mašine" za ovu memorijsku oblast definisane su dve kategorije izuzetaka:
- Ako nit zahteva dubinu steka veću od dubine koju dozvoljava virtuelna mašina, biće bačen izuzetak
StackOverflowError; - Ako kapacitet steka virtuelne mašine dozvoljava dinamičko proširenje, a pri proširenju ne može da se prijavi dovoljno memorije, biće bačen izuzetak
OutOfMemoryError.
2.3 Stek nativnih metoda
Stek nativnih metoda (Native Method Stacks) sličan je steku virtuelne mašine; razlika je u tome što je stek virtuelne mašine namenjen izvršavanju Java metoda (odnosno bytecoda) od strane virtuelne mašine, dok je stek nativnih metoda namenjen nativnim (Native) metodama koje JVM koristi.
2.4 Heap
Heap (Java Heap) jeste najveći memorijski prostor kojim upravlja virtuelna mašina; njega dele sve niti i služi za smeštanje instanci objekata.
Java heap se može nalaziti u fizički neprekidnom memorijskom prostoru, ali se logički treba posmatrati kao neprekidan. Java heap može biti implementiran kao fiksne veličine, a može biti i proširiv.
Trenutno većina glavnih virtuelnih mašina implementira heap kao proširiv, odnosno može se podesiti parametrom maksimalne vrednosti -Xmx i parametrom minimalne vrednosti -Xms.
Ako u Java heap-u nema dovoljno memorije za završetak dodele instance objekta, a heap se više ne može proširiti, Java virtuelna mašina će baciti izuzetak OutOfMemoryError.
2.5 Oblast metoda
Oblast metoda (Method Area) takođe je memorijska oblast koju dele sve niti; služi za čuvanje podataka poput informacija o klasama koje je virtuelna mašina učitala, konstanti, statičkih promenljivih, keša koda nakon JIT kompajliranja itd.
Oblast metoda se naziva i "ne-heap", s ciljem da se razlikuje od Java heap-a. "Specifikacija Java virtuelne mašine" propisuje da, ako oblast metoda ne može da zadovolji novi zahtev za dodelom memorije, biće bačen izuzetak OutOfMemoryError.
Implementacija oblasti metoda nakon JDK 8 više nije trajna generacija (Permanent Generation), već se koristi metaprostor (Metaspace).
Bazen konstanti u toku rada (Runtime Constant Pool) takođe je deo oblasti metoda; služi za čuvanje tabele bazena konstanti (Constant Pool Table), u kojoj se nalaze razni simbolički literali i simboličke reference generisani u vreme kompajliranja.
Bazeni konstanti u toku rada nakon JDK 8 nalaze se u metaprostoru.

3. Objekti
3.1 Kreiranje objekata
Kada u kodu koristimo ključnu reč new da kreiramo objekat, u JVM-u je potrebno proći kroz sledeće korake:
1. Proces učitavanja klase
Kada virtuelna mašina naiđe na jednu bytecode instrukciju new, prvo će proveriti da li argument te instrukcije može u bazenu konstanti locirati jednu simboličku referencu i da li je klasa koju ta simbolička referenca predstavlja već učitana, razrešena i inicijalizovana. Ako nije, mora se prvo izvršiti odgovarajući proces učitavanja klase.
2. Dodela memorije
Nakon što provera učitavanja klase prođe, virtuelna mašina novom objektu mora dodeliti memorijski prostor. U zavisnosti od toga da li je Java heap uredan, postoje dva načina dodele:
① Sudaranje pokazivača: pretpostavimo da je memorija u Java heap-u apsolutno uredna, sva iskorišćena memorija na jednoj strani, sva neiskorišćena na drugoj, sa pokazivačem na sredini kao graničnim indikatorom.
U tom slučaju dodela memorije svodi se na pomeranje pokazivača u slobodnom pravcu za veličinu objekta; ovaj način se naziva sudaranje pokazivača.

② Lista slobodnih blokova: ako Java heap nije uredan, virtuelna mašina u tom slučaju mora održavati listu koja beleži koji su memorijski blokovi upotrebljivi, a koji nisu. Pri dodeli memorije dovoljno je iz te liste izabrati jedan dovoljno veliki memorijski prostor i dodeliti ga instanci objekta.
Napomena: Da li je Java heap uredan zavisi od toga da li sakupljač smeća koji se koristi ima sposobnost prostorne kompresije i sređivanja, o čemu je već bilo reči.
Pored načina dodele, pošto je kreiranje objekata u virtuelnoj mašini veoma česta aktivnost, potrebno je obezbediti bezbednost niti u konkurentnom okruženju: ako jedna nit dodeli memorijski prostor objektu A, ali pokazivač još uvek nije izmenjen, može se desiti da druga nit iskoristi stari pokazivač da dodeli memorijski prostor objektu B.
Za rešavanje ovog problema postoje dva načina:
① Način jedan: upotreba sinhronizacije zaključavanja ili upotreba CAS uz mehanizam ponovnog pokušaja pri neuspehu, radi obezbeđenja atomičnosti operacije ažuriranja.
② Način dva: svakoj niti se unapred dodeljuje po jedan blok memorije u Java heapu, koji se naziva lokalni bafer za dodelu niti (Thread Local Allocation Buffer, TLAB).
Nit pri dodeli memorije prvo koristi lokalni bafer; kada se lokalni bafer potroši, tek onda podnosi zahtev Java heap-u za dodelu, pri čemu Java heap koristi sinhronizovano zaključavanje da obezbedi bezbednost niti pri dodeli.
3. Podešavanje zaglavlja objekta
Metapodaci vezani za objekat, haš-kod objekta, starosna generacija itd. smeštaju se u zaglavlje objekta.
Može se povezati sa sadržajem poglavlja o JIT-u.
4. Inicijalizacija objekta
Poziva se konstrukcijska metoda objekta, odnosno <init>() iz Class fajla, radi inicijalizacije objekta i dodeljivanja vrednosti relevantnim poljima.
3.2 Memorijski raspored objekta
U HotSpot-u, raspored skladištenja objekata u heap memoriji može se podeliti na sledeća tri dela:
1. Zaglavlje objekta (Header)
Zaglavlje objekta sadrži dve vrste informacija:
- Mark Word: podaci o samom objektu u toku rada, kao što su haš-kod, GC starosna generacija, oznaka stanja brave, brava koju drži nit, ID nagnute niti, vremenska oznaka naginjanja itd. — zvanično se zajednički nazivaju Mark Word; o njima smo govorili u četiri stanja brave synchronized.
- Pokazivač na tip: pokazivač kojim objekat ukazuje na metapodatke svog tipa; Java virtuelna mašina pomoću tog pokazivača utvrđuje koje je klase instanci taj objekat. Treba napomenuti da ne moraju sve virtuelne mašine obavezno čuvati pokazivač na tip u podacima objekta; to zavisi od načina pozicioniranja pristupa objektu.
2. Podaci instance (Instance Data)
To su razni tipovi polja koje definišemo u kodu — bilo da su nasleđena od roditeljske klase ili definisana u potklasi, sva se moraju zabeležiti.
3. Poravnanje popunom (Padding)
Uglavnom služi kao mesto za popunu. HotSpot zahteva da početna adresa objekta bude umnožak od 8 bajtova, što indirektno zahteva da veličina svakog objekta bude umnožak od 8 bajtova. Zaglavlje objekta po dizajnu jeste umnožak od 8 bajtova; ako podaci instance objekta nisu umnožak od 8 bajtova, poravnanje popunom nadoknađuje razliku.
3.3 Pozicioniranje pristupa objektu
Nakon kreiranja objekta, Java program može preko reference na steku (odnosno reference) da upravlja konkretnim objektom na heap-u.
"Specifikacija Java virtuelne mašine" propisuje da je reference referenca na objekat, ali ne propisuje konkretan način implementacije. Glavni načini su sledeća dva:
- Pristup preko ručki: u Java heap-u izdvojiće se memorija za bazen ručki;
referencečuva adresu ručke objekta, a ručka sadrži adrese podataka instance i podataka o tipu. - Pristup preko pokazivača:
referencedirektno čuva adresu objekta, dok se podaci o tipu objekta određuju preko pokazivača na tip u zaglavlju objekta, koji je gore opisan.
Pristup objektu preko ručki:

Pristup objektu preko direktnog pokazivača:

Prednost pristupa preko ručki jeste u tome što prilikom pomeranja objekta (sakupljanje smeća često pomera objekte) treba promeniti samo pokazivač podataka instance u ručki, dok se samu reference ne mora menjati;
Pristup preko pokazivača je obrnut: pošto reference direktno čuva adresu objekta, kada se objekat pomeri, reference mora biti izmenjen. Ali za scenarije u kojima je potrebno pristupiti samo samom objektu, pristup preko pokazivača smanjuje jedan trošak pozicioniranja. Pošto je pristup objektu veoma česta operacija, efekat ovakvog smanjenja je veoma značajan; iz tog razloga, HotSpot uglavnom koristi pristup preko pokazivača.
4. Mehanizam sakupljanja smeća
U JVM modelu memorije, tri oblasti — programski brojač, stek virtuelne mašine i stek nativnih metoda — privatne su niti i uništavaju se završetkom niti, pa u tim trima oblastima ne treba previše razmišljati o pitanju sakupljanja smeća. Problem sakupljanja smeća uglavnom se javlja na Java heap-u.
Na Java heap-u, glavni sadržaj sakupljanja smeća su mrtvi objekti (objekti koji nikada više ne mogu biti korišćeni bilo kojim putem).
Za utvrđivanje da li je objekat mrtav postoje dve metode:
1. Metoda brojanja referenci
U objekat se dodaje brojač referenci; svaki put kada se objekat referencira, brojač se povećava za jedan; kada referenca prestane da važi, vrednost brojača se smanjuje za jedan; čim vrednost brojača postane nula, to znači da odgovarajući objekat više ne može biti korišćen. Mana ove metode jeste u tome što ne može da izbegne problem međusobnog referenciranja:
objA.instance = objB
objB.instance = objA
objA = null;
objB = null;
System.gc();Kao što je gore prikazano, u tom trenutku dva objekta više ne mogu biti pristupana, ali međusobno drže reference jedan na drugog; ako se primeni metoda brojanja referenci, nijedan od ta dva objekta ne može biti recikliran.
2. Analiza dostižnosti
Ali gorenavedeni kod u većini virtuelnih mašina može biti ispravno recikliran, jer većina glavnih virtuelnih mašina koristi metodu analize dostižnosti da utvrdi da li je objekat mrtav.
Analiza dostižnosti polazi od skupa korenskih objekata poznatih kao GC Roots kao početnog skupa čvorova; iz tih čvorova, prema odnosima referenci, vrši pretragu nadole. Putanja kojom pretraga prolazi naziva se lanac referenci (Reference Chain); ako između nekog objekta i GC Roots ne postoji nijedan povezani lanac referenci, to znači da je iz GC Roots do tog objekta nemoguće doći — tada se dokazuje da taj objekat više ne može biti korišćen.

U Java jeziku, objekti koji fiksno mogu poslužiti kao GC Roots obuhvataju sledeće:
- Objekti referencirani u steku virtuelne mašine (u tabeli lokalnih promenljivih okvira steka), npr. argumenti, lokalne promenljive, privremene promenljive koje se koriste u steku poziva metoda različitih niti;
- Objekti referencirani statitčkim promenljivama klase u oblasti metoda (metaprostoru), npr. statitčke promenljive referentnog tipa u Java klasama;
- Objekti referencirani konstantama u oblasti metoda (metaprostoru), npr. reference u bazenu string konstanti (String Table);
- Objekti referencirani u JNI-ju (Native metode) u steku nativnih metoda;
- Interne reference same Java virtuelne mašine, kao što su Class objekti koji odgovaraju osnovnim tipovima podataka, neki stalni objekti izuzetaka (npr. NullPointException, OutOfMemoryError itd.) i sistemski class loader;
- Svi objekti koje drže sinhronizacione brave (ključna reč synchronized);
Pored tog fiksnog skupa GC Roots, u zavisnosti od sakupljača smeća koji je korisnik odabrao i memorijske oblasti koja se trenutno reciklira, mogu se i drugi objekti "privremeno" pridružiti i zajednički sačiniti potpuni skup GC Roots.
3. Reference objekata
Analiza dostižnosti zasniva se na lancu referenci; nakon JDK 1.2, Java deli odnose referenci na sledeće četiri kategorije:
Jaka referenca (Strongly Reference)
Najtradicionalnija referenca, kao Object obj = new Object(). Bez obzira na okolnosti, sve dok postoji odnos jake reference, sakupljač smeća nikada neće reciklirati referencirani objekat.
Meka referenca (Soft Reference)
Služi za opisivanje nekih još uvek korisnih, ali ne i neophodnih objekata. Objekti na koje ukazuje samo meka referenca, pre nego što dođe do prekoračenja memorije, biće stavljeni u opseg recikliranja radi drugog kruga recikliranja; ako ni nakon tog recikliranja nema dovoljno memorije, tek tada se baca izuzetak prekoračenja memorije.
U nastavku je primer koda koji koristi Java klasu SoftReference:
class SoftReferenceExample {
public static void main(String[] args) {
// kreiranje objekta na koji ukazuje jaka referenca
String strongReference = new String("Erge, ja sam jaka referenca");
// kreiranje meke reference koja ukazuje na gorenavedeni objekat
SoftReference<String> softReference = new SoftReference<>(strongReference);
// uklanjanje jake reference
strongReference = null;
// sada samo meka referenca ukazuje na objekat "Erge, ja sam jaka referenca"
// pokušaj preuzimanja objekta preko meke reference
String retrievedString = softReference.get();
System.out.println(retrievedString); // ispisuje "Erge, ja sam jaka referenca"
// prinudno sakupljanje smeća, može očistiti objekat meke reference
System.gc();
// ponovni pokušaj preuzimanja objekta preko meke reference
retrievedString = softReference.get();
if (retrievedString != null) {
System.out.println(retrievedString);
} else {
System.out.println("Objekat meke reference je sakupljen kao smeće");
}
}
}U ovom primeru prvo smo kreirali jaku referencu na objekat niske, a zatim preko SoftReference kreirali meku referencu na taj objekat. Nakon ukidanja jake reference, na taj objekat ukazuje samo meka referenca. Kada pokušamo da preko meke reference preuzmemo objekat, ako objekat još uvek postoji, meka referenca će ga vratiti; ako je objekat već sakupljen, vratiće null.
Treba napomenuti da se pri drugom recikliranju, ako ni nakon tog recikliranja nema dovoljno memorije, tek tada baca izuzetak prekoračenja memorije. "Dovoljno" ovde znači da će sistem, pre bacanja izuzetka prekoračenja memorije, izvršiti još jedan pokušaj; ako ni nakon tog recikliranja nema dovoljno memorije, tek tada se baca izuzetak prekoračenja memorije.
Uobičajeno je da gorenavedeni kod nakon gc neće reciklirati meku referencu, jer je u tom trenutku memorija još uvek dovoljna.
Erge, ja sam jaka referenca
Erge, ja sam jaka referencaSlaba referenca (Weak Reference)
Služi za opisivanje neobaveznih objekata; jačina je slabija od meke reference. Objekti na koje ukazuje slaba referenca mogu preživeti samo do sledećeg sakupljanja smeća; bez obzira na to da li u tom trenutku ima dovoljno memorije, objekti slabe reference biće reciklirani.
Pogledajmo ovaj kod:
class WeakReferenceExample {
public static void main(String[] args) {
// kreiranje objekta na koji ukazuje jaka referenca
String strongReference = new String("Erge, ja sam jaka referenca");
// kreiranje slabe reference koja ukazuje na gorenavedeni objekat
WeakReference<String> weakReference = new WeakReference<>(strongReference);
// ukidanje jake reference
strongReference = null;
// prinudno sakupljanje smeća
System.gc();
// pokušaj preuzimanja objekta preko slabe reference
String retrievedString = weakReference.get();
if (retrievedString != null) {
System.out.println(retrievedString);
} else {
System.out.println("Objekat slabe reference je sakupljen kao smeće");
}
}
}U ovom primeru prvo smo kreirali jaku referencu na objekat niske, a zatim preko WeakReference kreirali slabu referencu na taj objekat. Nakon ukidanja jake reference, na taj objekat ukazuje samo slaba referenca. Kada pokušamo da preko slabe reference preuzmemo objekat, ako objekat još uvek postoji, slaba referenca će ga vratiti; ako je objekat već sakupljen, vratiće null.
Rezultat rada razlikuje se od meke reference — nakon gc slaba referenca je reciklirana.
Objekat slabe reference je sakupljen kao smećeFantomska referenca (Phantom Reference)
Najslabiji odnos reference. Jedina svrha postavljanja fantomske reference na objekat jeste mogućnost primanja sistemske notifikacije kada taj objekat bude recikliran.
Fantomska referenca mora se koristiti zajedno sa redom referenci (ReferenceQueue). Kada sakupljač smeća priprema recikliranje nekog objekta, a otkrije da on ima fantomsku referencu, nakon recikliranja objekta tu fantomsku referencu dodaće u red referenci s kojom je povezana.
Pogledajmo ovaj primer:
class PhantomReferenceExample {
public static void main(String[] args) {
// kreiranje objekta na koji ukazuje jaka referenca
String strongReference = new String("Erge, ja sam jaka referenca");
// kreiranje reda referenci
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
// kreiranje fantomske reference koja ukazuje na gorenavedeni objekat i povezivanje sa redom referenci
PhantomReference<String> phantomReference = new PhantomReference<>(strongReference, referenceQueue);
// ukidanje jake reference
strongReference = null;
// prinudno sakupljanje smeća
System.gc();
// provera reda referenci, da li postoji notifikacija
if (referenceQueue.poll() != null) {
System.out.println("Objekat fantomske reference je sakupljen kao smeće i notifikacija je primljena");
} else {
System.out.println("Objekat fantomske reference nije sakupljen kao smeće ili notifikacija nije primljena");
}
}
}U ovom primeru prvo smo kreirali jaku referencu na objekat niske, a zatim preko PhantomReference kreirali fantomsku referencu na taj objekat i povezali je sa redom referenci. Nakon ukidanja jake reference, na taj objekat ukazuje samo fantomska referenca. Kada prinudno izvršimo sakupljanje smeća, ako objekat još uvek postoji, fantomska referenca ga neće vratiti; ako je objekat već sakupljen, vratiće null. Istovremeno, ako je objekat recikliran, fantomska referenca će biti dodata u red referenci.
Rezultat rada je sledeći:
Objekat fantomske reference je sakupljen kao smeće i notifikacija je primljena4. Stvarna smrt objekta
Da bi se objekat zaista proglasio mrtvim, potrebno je proći kroz najmanje dva procesa označavanja:
① Ako se nakon analize dostižnosti utvrdi da objekat nije dostižan iz GC Roots, izvršiće se prvo označavanje;
② Zatim sledi jedno prosejavanje; uslov za prosejavanje jeste da li je neophodno izvršiti metod finalize() nad tim objektom.
Ako objekat nije preklopio metod finalize(), ili je finalize() već bio pozvan od strane virtuelne mašine, u oba slučaja se smatra da ga nije neophodno izvršiti.
Ako je rezultat procene da ga je neophodno izvršiti, objekat će biti stavljen u red po imenu F-Queue, gde će Finalizer nit izvršiti njegov metod finalize().
U tom procesu, sakupljač će izvršiti drugo, manje obimno označavanje; ako objekat u metodi finalize() ponovo uspostavi vezu sa bilo kojim objektom na lancu referenci — na primer, dodeli sebe (ključna reč this) nekoj promenljivoj klase ili članu objekta — u tom trenutku je ostvario samospasavanje, pa će ga drugo označavanje ukloniti iz skupa "predstojećeg recikliranja"; u suprotnom će taj objekat zaista biti recikliran i otići u smrt.
4.3 Algoritmi sakupljanja smeća
1. Teorija generacionog sakupljanja
Većina savremenih virtuelnih mašina prati teoriju "generacionog sakupljanja" u dizajnu; ona se zasniva na dve hipoteze — slaboj i jakoj generacionoj:
- Slaba generaciona hipoteza (Weak Generational Hypothesis): ogromna većina objekata je kratkog veka.
- Jaka generaciona hipoteza (Strong Generational Hypothesis): što je objekat preživeo više procesa sakupljanja smeća, teže ga je uništiti.
- Hipoteza o unakrsnoj referenci (Intergenerational Reference Hypothesis): na osnovu gore dve hipoteze može se izvesti i jedan implicitan zaključak: dva objekta između kojih postoji odnos međusobnog referenciranja treba da teže istovremenom preživljavanju ili istovremenoj smrti.
Slaba i jaka generaciona hipoteza postavile su princip dizajna sakupljača smeća: sakupljač treba da podeli Java heap u različite oblasti, a zatim da objekte koji podležu recikliranju, na osnovu njihove starosti (starost je broj koliko je puta objekat prošao kroz sakupljanje smeća), rasporedi u različite oblasti radi skladištenja.
Nakon toga, ako su u jednoj oblasti svi objekti kratkog veka, sakupljač treba da obrati pažnju samo na mali broj preživelih objekata, umesto da označava onaj veliki broj objekata koji tek treba da budu reciklirani; tada se uz manji trošak može dobiti veliki prostor.
Na kraju se teško uništivi objekti okupe na jednom mestu; prema jakoj generacionoj hipotezi, njih je teško uništiti, pa virtuelna mašina može koristiti nižu učestalost recikliranja, čime se istovremeno vodi računa i o potrošnji vremena i memorijskog prostora.
2. Tipovi recikliranja
Na osnovu teorije generacionog sakupljanja, obim sakupljanja se može podeliti u sledeće tipove:
① Delimično sakupljanje (Partial GC); konkretno se deli na:
- Sakupljanje mlade generacije (Minor GC / Young GC): sakupljanje smeća samo nad mladom generacijom;
- Sakupljanje stare generacije (Major GC / Old GC): sakupljanje smeća samo nad starom generacijom. Treba napomenuti da se izraz Major GC u nekim kontekstima koristi i za sakupljanje celog heap-a;
- Mešovito sakupljanje (Mixed GC): sakupljanje smeća nad celom mladom generacijom i delom stare generacije.
- Sakupljanje celog heap-a (Full GC): sakupljanje celog Java heap-a i oblasti metoda.
3. Algoritam označavanje-brisanje
To je najosnovniji algoritam sakupljanja smeća; proces sakupljanja deli se u dve faze: prvo se označe svi objekti koje treba reciklirati, a nakon završetka označavanja, objedinjeno se recikliraju svi označeni objekti; može i obrnuto — označe se preživeli objekti, pa objedinjeno recikliraju svi neoznačeni objekti.

Ima uglavnom dve mane:
- Nestabilna efikasnost izvršavanja: ako Java heap sadrži veliki broj objekata koje treba reciklirati, potrebno je izvesti mnogo radnji označavanja i brisanja;
- Fragmentacija memorijskog prostora: nakon označavanja i brisanja nastaje mnogo neprekidnih prostora, zbog čega se ne može obezbediti dovoljno neprekidne memorije za velike objekte.
4. Algoritam označavanje-kopiranje
Algoritam označavanje-kopiranje zasnovan je na algoritmu "kopiranje po polovinama": raspoloživu memoriju po kapacitetu deli na dva jednaka dela; svaki put se koristi samo jedna polovina; kada se memorija u toj polovini potroši, preživele objekte treba kopirati u onu drugu polovinu, a zatim se već iskorišćen memorijski prostor odjednom očisti. Njegova prednost jeste izbegavanje fragmentacije memorijskog prostora, a mane su sledeće:
- Ako je u memoriji većina objekata preživela, ovaj algoritam stvoriće veliki trošak kopiranja;
- Rasipanje memorijskog prostora — memorijski prostor se svodi na polovinu prvobitnog.

Na osnovu osobine mlade generacije da su objekti "kratkog veka", većina virtuelnih mašina ne vrši podelu memorije u odnosu 1:1; na primer, HotSpot deli memorijski prostor na jedan veći Eden i dva manja Survivor prostora, čiji je međusobni odnos 8:1:1.
Pri svakoj dodeli koristiće se samo Eden i jedan od Survivor prostora; kada dođe do sakupljanja smeća, preživele objekte dovoljno je jednom kopirati na onaj drugi Survivor, čime se rasipa samo 10% memorijskog prostora.
Kada Survivor prostor nije dovoljan da primi jedan Minor GC, tada druga memorijska oblast (obično stara generacija) pruža garant za dodelu.
5. Algoritam označavanje-sređivanje
Algoritam označavanje-sređivanje, nakon završetka označavanja, pomeri sve preživele objekte ka jednom kraju memorije, a zatim direktno očisti memoriju izvan granice.
Njegova prednost jeste u tome što izbegava problem fragmentacije memorijskog prostora i može u potpunosti iskoristiti memorijski prostor; mana jeste u tome da, u zavisnosti od sakupljača koji se koristi, pri pomeranju preživelih objekata možda mora da se pauzira korisnički program tokom celog procesa:

5. Sakupljači smeća
Paralelnost i konkurentnost su termini iz konkurentnog programiranja; u kontekstu razgovora o sakupljačima smeća, njihova značenja su sledeća:
① Paralelno (Parallel): paralelnost opisuje odnos između više niti sakupljača smeća; ukazuje na to da u istom trenutku više takvih niti radi zajednički, pri čemu se obično podrazumeva da su korisničke niti u stanju čekanja.
② Konkurentno (Concurrent): konkurentnost opisuje odnos između niti sakupljača smeća i korisničkih niti; ukazuje na to da u istom trenutku i niti sakupljača smeća i korisničke niti rade. Ali pošto niti sakupljača smeća zauzimaju deo sistemskih resursa, propusni opseg programa i dalje će biti izvesno pogođen.
U HotSpot-u postoji ukupno sedam klasičnih sakupljača smeća:

Napomena: linije koje povezuju sakupljače označavaju da se mogu koristiti zajedno.
5.1 Serial sakupljač
Serial sakupljač je najosnovniji i istorijski najstariji sakupljač; to je jednonitni sakupljač i pri sakupljanju smeća mora pauzirati sve druge radne niti do završetka sakupljanja — to mu je glavna mana.
Njegova prednost jeste u tome što jednonitnost izbegava složeno prebacivanje konteksta više niti, pa je efikasnost sakupljanja u jednonitnom okruženju veoma visoka; zahvaljujući toj prednosti, do danas ostaje podrazumevani sakupljač mlade generacije za HotSpot virtuelnu mašinu u klijentskom režimu:

5.2 ParNew sakupljač
To je višenitna verzija Serial sakupljača; za sakupljanje smeća može koristiti više niti:

5.3 Parallel Scavenge sakupljač
Parallel Scavenge je takođe sakupljač mlade generacije, zasnovan na algoritmu označavanje-kopiranje; njegov cilj jeste da postigne kontrolisani propusni opseg. Propusni opseg ovde označava odnos vremena izvršavanja korisničkog koda procesa i ukupnog vremena potrošenog od strane procesa:
propusni opseg = vreme rada korisničkog koda \ (vreme rada korisničkog koda + vreme rada sakupljanja smeća)Parallel Scavenge sakupljač nudi dva parametra za preciznu kontrolu propusnog opsega:
① -XX:MaxGCPauseMillis: kontroliše maksimalno vreme sakupljanja smeća; pod pretpostavkom da je ukupna količina smeća koju treba reciklirati nepromenjena, smanjenje vremena sakupljanja smeća dovešće do veće učestalosti sakupljanja, pa zato treba postaviti odgovarajuću vrednost i ne smanjivati je jednostrano.
② -XX:MaxGCTimeRatio: direktno služi za podešavanje propusnog opsega; to je ceo broj veći od 0 a manji od 100. Ako se postavi na 19, to znači da u tom trenutku maksimalno dozvoljeno vreme sakupljanja smeća iznosi 5% ukupnog vremena (odnosno 1/(1+19)); podrazumevana vrednost je 99, odnosno dozvoljeno je najviše 1% (1/(1+99)) vremena za sakupljanje smeća.
5.4 Serial Old sakupljač
Iz imena se može videti da je to verzija Serial sakupljača za staru generaciju; takođe je jednonitni sakupljač, koristi algoritam označavanje-sređivanje i uglavnom služi za HotSpot u klijentskom režimu:

5.5 Parallel Old sakupljač
Parallel Old je verzija Parallel Scavenge sakupljača za staru generaciju; podržava višenitno konkurentno sakupljanje i implementiran je na osnovu algoritma označavanje-sređivanje:

5.6 CMS sakupljač
CMS (Concurrent Mark Sweep) sakupljač jeste sakupljač čiji je cilj postizanje najkraćeg vremena pauze pri recikliranju; zasnovan je na algoritmu označavanje-brisanje, a ceo proces sakupljanja deli se u sledeće četiri faze:
- Početno označavanje (initial mark): označava objekte do kojih
GC Rootsmože direktno doći; kratko traje, ali zahteva pauziranje korisničkih niti; - Konkurentno označavanje (concurrent mark): polazeći od objekata do kojih
GC Rootsmože direktno doći, obilazi čitav graf objekata; dugo traje, ali ne zahteva pauziranje korisničkih niti; - Ponovno označavanje (remark): koristi algoritam inkrementalnog ažuriranja da ponovo označi one objekte koji su se promenili u fazi konkurentnog označavanja zbog rada korisničkih niti; traje nešto duže od početnog označavanja i zahteva pauziranje korisničkih niti;
- Konkurentno čišćenje (concurrent sweep): konkurentno čisti objekte koji su već mrtvi; dugo traje, ali ne zahteva pauziranje korisničkih niti.

Njegova prednost jeste u tome što dugotrajne faze konkurentnog označavanja i konkurentnog čišćenja ne zahtevaju pauziranje korisničkih niti, pa je vreme pauze kraće; glavne mane su sledeće:
- Pošto obuhvata konkurentne operacije, veoma je osetljiv na resurse procesora.
- Pošto je zasnovan na algoritmu označavanje-brisanje, stvoriće mnogo fragmenata prostora.
- Ne može da obradi plutajuće smeće (Floating Garbage): pošto se tokom konkurentnog čišćenja korisničke niti nastavljaju sa radom, u tom trenutku se i dalje stvara smeće; to smeće se naziva plutajućim smećem i može biti očišćeno tek pri sledećem sakupljanju smeća.
5.7 Garbage First sakupljač
Garbage First (kraće G1) jeste sakupljač smeća orijentisan na servere, a i podrazumevani sakupljač smeća u JDK 9 u server režimu; njegov nastanak ima epohalni značaj.
Iako G1 takođe prati teoriju generacionog sakupljanja, ne vrši podelu generacionih oblasti po fiksnoj veličini i fiksnom broju, već neprekidan Java heap deli na više nezavisnih oblasti jednake veličine (Region). Svaki Region, u zavisnosti od različitih potreba, može igrati ulogu Eden prostora, Survivor prostora ili prostora stare generacije mlade generacije; sakupljač će, u zavisnosti od uloge koju igra, koristiti različite strategije sakupljanja.

Na gornjoj slici neki Region-i nose oznaku H, koja predstavlja Humongous i označava da se ti Region-i koriste za skladištenje velikih objekata (humongous object, H-obj), odnosno objekata čija je veličina veća ili jednaka polovini Region-a.
Rad G1 sakupljača grubo se može podeliti na sledeća četiri koraka:
① Početno označavanje (Initial Marking): označava objekte do kojih GC Roots može direktno doći i izmenjuje vrednost TAMS (Top at Mark Start) pokazivača, tako da kad korisničke niti u sledećoj fazi rade konkurentno, mogu ispravno da dodeljuju nove objekte u Region-u.
G1 za svaki Region projektuje dva pokazivača pod imenom TAMS; novo dodeljeni objekti moraju se nalaziti iznad pozicija ta dva pokazivača, a objekti koji se nalaze iznad pozicija ta dva pokazivača podrazumevano se implicitno označavaju kao preživeli i ne ulaze u opseg recikliranja;
② Konkurentno označavanje (Concurrent Marking): polazeći od objekata do kojih GC Roots može direktno doći, obilazi čitav graf objekata. Nakon obilaska, potrebno je obraditi i objekte koji su se promenili u SATB zapisima.
SATB (snapshot-at-the-beginning, snimak na početku) efikasno rešava promene objekata u fazi konkurentnog označavanja usled rada korisničkih niti; njegova efikasnost veća je od efikasnosti algoritma inkrementalnog ažuriranja koji koristi CMS u fazi ponovnog označavanja;
③ Konačno označavanje (Final Marking): kratko pauzira korisničke niti radi obrade preostalog malog broja STAB zapisa nakon završetka konkurentne faze. Iako konkurentna faza označavanja obrađuje SATB zapise, pošto su prilikom obrade korisničke niti i dalje u radu, i dalje ostaje mali broj promena, pa je potrebno konačno označavanje koje će ih obraditi;
④ Selektivno recikliranje (Live Data Counting and Evacuation): odgovorno za ažuriranje statistike Region-a, sortira Region-e prema vrednosti i troškovima recikliranja, a zatim na osnovu vremena pauze koje korisnik očekuje sastavlja plan recikliranja; može se izabrati bilo koji broj Region-a koji čine skup za recikliranje.
Zatim se preživeli objekti iz Region-a u skupu za recikliranje kopiraju u prazne Region-e, a zatim čitav stari Region čisti. U ovom trenutku, pošto je u pitanju premeštanje preživelih objekata, potrebno je pauzirati korisničke niti, a izvršavanje obavljaju paralelno multiple sakupljačke niti.

5.8 Principi dodele memorije
1. Objekti se prvo dodeljuju u Eden-u
U većini slučajeva objekti se dodeljuju u Eden oblasti mlade generacije; kada Eden oblast više nema dovoljno prostora, virtuelna mašina će izvesti jedan Minor GC.
2. Veliki objekti odmah ulaze u staru generaciju
Veliki objekti se odnose na Java objekte kojima je potrebno mnogo neprekidnog memorijskog prostora; najtipičniji su vrlo duge niske ili nizovi sa velikim brojem elemenata — oni će direktno ući u staru generaciju.
Glavni razlog je to što bi, u slučaju dodele u mladoj generaciji, zbog potrebe za mnogo neprekidnog memorijskog prostora, moglo prerano doći do sakupljanja smeća; a s obzirom na to da je sakupljanje smeća u mladoj generaciji samo po sebi veoma učestalo, kopiranje velikih objekata u tom trenutku zahteva i dodatne performansne troškove.
3. Dugovečni objekti će preći u staru generaciju
Virtuelna mašina će u zaglavlju svakog objekta definisati brojač starosti. Objekti se obično rađaju u Eden oblasti; ako prežive prvi Minor GC i mogu stati u Survivor, biće premešteni u Survivor i njihova starost biće povećana za 1.
Svaki put kada objekat u Survivor-u preživi jedan Minor GC, starost mu se povećava za 1; kada starost dostigne izvestan stepen (podešen parametrom -XX:MaxTenuringThreshold, podrazumevana vrednost 15), ući će u staru generaciju.
4. Dinamička procena starosti
Ako u Survivor prostoru zbir veličina svih objekata iste starosti premašuje polovinu Survivor prostora, onda objekti čija je starost veća ili jednaka toj starosti mogu direktno preći u staru generaciju, bez čekanja da starost dostigne vrednost podešenu parametrom -XX:MaxTenuringThreshold.
5. Garantovana dodela prostora
Pre nego što dođe do Minor GC-a, virtuelna mašina mora prvo proveriti da li je maksimalni raspoloživi neprekidni prostor u staroj generaciji veći od ukupnog prostora svih objekata u mladoj generaciji; ako jeste, tada se ovaj Minor GC može smatrati bezbednim.
Ako nije, virtuelna mašina proverava da li -XX:HandlePromotionFailure dozvoljava neuspeh garantovanja; ako dozvoljava, nastaviće sa proverom da li je maksimalni raspoloživi neprekidni prostor u staroj generaciji veći od prosečne veličine objekata koji su prethodno unapređivani u staru generaciju; ako je veći, pokušaće da izvrši jedan Minor GC; ako je manji ili -XX:HandlePromotionFailure ne dozvoljava rizik, mora se preći na izvršenje jednog Full GC-a.
6. Mehanizam učitavanja klasa
Java virtuelna mašina iz Class fajla u memoriju učitava podatke koji opisuju klasu, vrši proveru podataka, konverziju i razrešavanje i inicijalizaciju, te konačno oblikuje Java tip koji virtuelna mašina može direktno koristiti; ovaj proces se naziva mehanizam učitavanja klasa virtuelne mašine.
6.1 Vreme učitavanja klase
Od trenutka kada klasa počne da se učitava u memoriju virtuelne mašine, pa do istovara iz memorije, njen čitav životni cikl prolazi kroz sedam faza: učitavanje, provera, priprema, razrešavanje, inicijalizacija, korišćenje i istovar; pri čemu se provera, priprema i razrešavanje zajednički nazivaju povezivanje:

"Specifikacija Java virtuelne mašine" strogo propisuje šest situacija u kojima klasa mora biti odmah inicijalizovana:
① Naiđe se na četiri bytecode instrukcije new, getstatic, putstatic, invokestatic; tipične Java kodne scene koje mogu generisati te čiri instrukcije su:
- instanciranje objekta pomoću ključne reči
new; - čitanje ili postavljanje statičkog polja tipa (osim statičkih polja modifikovanih sa final, čiji je rezultat već u vreme kompajliranja stavljen u bazen konstanti);
- poziv statičke metode klase.
② Korišćenje metoda iz paketa java.lang.reflect za refleksivni poziv nad Class-om; ako tip nije bio inicijalizovan, potrebno je okinuti njegovu inicijalizaciju;
③ Prilikom inicijalizacije klase, ako se utvrdi da njena roditeljska klas nije bila inicijalizovana, potrebno je okinuti inicijalizaciju njene roditeljske klase;
④ Prilikom pokretanja virtuelne mašine, korisnik mora navesti glavnu klasu koja će se izvršiti (onu klasu koja sadrži metod main()); virtuelna mašina će prvo inicijalizovati tu glavnu klasu;
⑤ Prilikom korišćenja podrške za dinamičke jezike uvedene u JDK 7, ako konačni rezultat razrešavanja instance java.lang.invoke.MethodHandle jeste metod handle tipa REF_getStatic, REF_putStatic, REF_invokeStatic ili REF_newInvokeSpecial, a klasa koja odgovara tom metodu handle-a nije bila inicijalizovana, potrebno je prvo okinuti njenu inicijalizaciju;
⑥ Kada interfejs definiše podrazumevanu metodu uvedenu u JDK 8 (metod interfejsa modifikovan ključnom rečju default), ako se desi inicijalizacija neke klase koja implementira taj interfejs, taj interfejs mora biti inicijalizovan pre nje.
6.2 Proces učitavanja klase
1. Učitavanje
U fazi učitavanja, virtuelna mašina mora završiti sledeće tri stvari:
- Pomoću potpuno kvalifikovanog imena klase dobiti binarni tok bajtova koji definiše tu klasu;
- Pretvoriti statičku strukturu skladištenja koju taj tok bajtova predstavlja u strukturu podataka u toku rada;
- U memoriji generisati jedan
java.lang.Classobjekat koji predstavlja tu klasu, kao ulaznu tačku za pristup raznim podacima te klase.
"Specifikacija Java virtuelne mašine" ne ograničava odakle se binarni tok može dobiti, pa se može dobijati iz JAR paketa, WAR paketa, kao i iz Class fajla koji generiše JSP itd.
2. Provera
Cilj ove faze jeste da se osigura da tok bajtova u Class fajlu sadrži informacije koje ispunjavaju sve zahteve ograničenja "Specifikacije Java virtuelne mašine", čime se garantuje da će te informacije, pošto se pokrenu kao kod, ugroziti bezbednost same virtuelne mašine.
Faza provere grubo će završiti sledeće četiri provere:
- Provera formata fajla: proverava da li tok bajtova ispunjava specifikaciju formata Class fajla;
- Provera metapodataka: vrši semantičku analizu informacija opisanih bytecode-om, kako bi se osiguralo da ispunjavaju zahteve "Specifikacije Java jezika" (npr. osim
java.lang.Object, sve klase treba da imaju roditeljsku klasu); - Provera bytecoda: kroz analizu toka podataka i toka kontrole, utvrđuje da li je semantika programa legalna i logična (npr. dozvoljeno je dodeliti objekat potklase tipu podatka roditeljske klase, ali ne i objekat roditeljske klase tipu podatka potklase);
- Provera simboličkih referenci: proverava da klasi ne nedostaju ili da joj nije zabranjen pristup spoljnim resursima od kojih zavisi — pojedinim klasama, metodama, poljima itd. Ako provera ne prođe, biće bačen izuzetak koji je podklasa od
java.lang.IncompatibleClassChangeError, kao što sujava.lang.NoSuchFieldError,java.lang.NoSuchMethodErroritd.
3. Priprema
Faza pripreme jeste faza u kojoj se zvanično dodeljuje memorija za promenljive definisane u klasi (odnosno statičke promenljive, promenljive modifikovane sa static) i postavljaju početne vrednosti promenljivih klase.
4. Razrešavanje
Razrešavanje je proces u kojem Java virtuelna mašina zamenjuje simboličke reference u bazenu konstanti direktnim referencama:
- Simbolička referenca: simbolička referenca se koristi za opisivanje cilja na koji se upućuje pomoću skupa simbola; simboli mogu biti bilo kakvi literali, pod uslovom da pri upotrebi cilj mogu nedvosmisleno locirati.
- Direktna referenca: direktna referenca je pokazivač, relativni pomeraj ili ručka koja direktno ukazuju na cilj, a pomoću koje se cilj može indirektno locirati.
Celokupna akcija razrešavanja uglavnom se odnosi na sedam vrsta simboličkih referenci: klasa ili interfejs, polje, metod klase, metod interfejsa, tip metoda, metod handle i kvalifikator pozivne tačke.
5. Inicijalizacija
Faza inicijalizacije proces je izvršavanja metoda <clinit>() konstruktori klase; taj metod ima sledeće karakteristike:
- Metod
<clinit>()nastaje tako što kompajler automatski prikupi sve akcije dodeljivanja vrednosti promenljivima klase i naredbe iz statičkih blokova naredbi; redosled prikupljanja određuje redosled pojavljivanja naredbi u izvornom fajlu. - Metod
<clinit>()razlikuje se od konstrukcijske metode klase (odnosno konstruktora instance<init>()iz ugla virtuelne mašine) po tome što ne zahteva izričit poziv konstruktoru roditeljske klase; Java virtuelna mašina će garantovati da će, pre izvršavanja metoda<clinit>()potklase, metod<clinit>()roditeljske klase već biti završen. - Pošto se metod
<clinit>()roditeljske klase izvršava prvi, to znači da će statički blokovi naredbi definisani u roditeljskoj klasi imati prioritet nad akcijama dodeljivanja vrednosti promenljivama potklase. - Metod
<clinit>()nije obavezan za klasu ili interfejs; ako klasa nema statičke blokove naredbi niti akcije dodeljivanja vrednosti promenljivama, kompajler može da ne generiše metod<clinit>()za tu klasu. - U interfejsu se ne mogu koristiti statički blokovi naredbi, ali i dalje postoje akcije dodeljivanja vrednosti pri inicijalizaciji promenljivih, pa interfejs, kao i klasa, generiše metod
<clinit>(). - Java virtuelna mašina mora garantovati da će metod
<clinit>()jedne klase, u višenitnom okruženju, biti ispravno zaključan i sinhronizovan; ako više niti istovremeno pokuša da inicijalizuje jednu klasu, samo će jedna od njih izvršiti metod<clinit>()te klase, dok će ostale niti morati da čekaju blokirane.
6.3 Class loader
Alat koji na osnovu potpuno kvalifikovanog imena klase može dobiti binarni tok bajtova koji opisuje tu klasu naziva se class loader.
Svaki class loader poseduje nezavisan prostor imena klasa, pa za bilo koju klasu moraju biti zajednički utvrđeni njen class loader i ona klasa sama, radi utvrđivanja njene jedinstvenosti u Java virtuelnoj mašini.
To znači da, da bi se uporedile dve klase, to mora biti pod pretpostavkom da ih učitava isti class loader; ako class loaderi dveju klasa su različiti, one sigurno nisu jednake.
6.4 Model delegiranja roditelju
Iz ugla Java virtuelne mašine, class loaderi se mogu podeliti u dve kategorije:
- Bootstrap class loader: bootstrap class loader (Bootstrap ClassLoader) implementiran je u C++-u (na primeru HotSpot) i deo je same virtuelne mašine;
- Class loaderi svih ostalih klasa: implementirani su u Java jeziku, postoje nezavisno izvan virtuelne mašine i nasleđuju se od
java.lang.ClassLoader.
Iz ugla programera, class loaderi se mogu podeliti u tri kategorije:
- Bootstrap class loader (Boostrap Class Loader): odgovoran za učitavanje u memoriju virtuelne mašine biblioteka smeštenih u direktorijumu
<JAVA_HOME>\libili na putanji navedenoj parametrom-Xbootclasspath, a koje Java virtuelna mašina prepoznaje; - Extension class loader (Extension Class Loader): odgovoran za učitavanje svih biblioteka smeštenih u direktorijumu
<JAVA_HOME>\lib\extili na putanji navedenoj sistemskom promenljivomjava.ext.dirs. - Application class loader (Application Class Loader): odgovoran za učitavanje svih biblioteka na korisničkoj putanji klasa (ClassPath).
Java aplikacije pre JDK 9 oslanjale su se na te tri vrste class loadera, u međusobnoj saradnji, radi završetka učitavanja:

Hijerarhijski odnos između različitih class loadera prikazan na gornjoj slici naziva se "model delegiranja roditelju" class loadera. Model delegiranja roditelju zahteva da, osim bootstrap class loadera na vrhu, svi ostali class loaderi treba da imaju svoj roditeljski class loader. Treba napomenuti da odnos roditelj-dete između loadera ovde se obično ne ostvaruje nasleđivanjem, već kompozicijom, radi ponovnog korišćenja koda roditeljskog class loadera.
Proces rada modela delegiranja roditelju je sledeći: kada class loader primi zahtev za učitavanje klase, on prvo neće pokušati sam da učita tu klasu, već taj zahtev delegira svom roditeljskom class loaderu na izvršenje; to važi za svaki nivo class loadera, pa će tako svi zahtevi za učitavanje konačno biti prosleđeni bootstrap class loaderu na vrhu; tek kada roditeljski loader javi da ne može da ispuni taj zahtev (u njegovom opsegu pretrage nije pronađena tražena klasa), podređeni loader će pokušati sam da završi učitavanje.
Na osnovu modela delegiranja roditelju može se garantovati da će klasa u programu, u različitim okruženjima class loadera, biti ista klasa; u suprotnom bi moglo doći do situacije da u jednom programu postoje dva različita java.lang.Object.
6.5 Class loaderi u uslovima modularizacije
Nakon JDK 9, radi prilagođavanja razvoju modularizacije, class loaderi su doživeli sledeće promene:
- I dalje se održava arhitektura sa tri sloja class loadera i modelom delegiranja roditelju, ali je extension class loader zamenjen platform class loaderom;
- Kada platformski i aplikacijski class loader prime zahtev za učitavanje klase, prvo moraju proceniti da li ta klasa može pripadati nekom sistemskom modulu; ako se pronađe takav odnos pripadnosti, prioritetno se delegira loaderu zaduženom za taj modul radi završetka učitavanja;
- Bootstrap class loader, platform class loader i application class loader svi nasleđuju se od
java.internal.loader.BuiltinClassLoader; u BuiltinClassLoader implementirana je logika učitavanja klasa iz modula u novoj modularnoj arhitekturi, kao i obrana pristupačnosti resursa u modulu.

7. Kompajliranje programa
7.1 Klasifikacija kompajlera
- Frontend kompajler: proces pretvaranja
*.javafajla u.classfajl; npr. Javac iz JDK-a, inkrementalni kompajler u Eclipse JDT-u. - Just-in-time kompajler: često se naziva JIT kompajler (Just In Time Compiler); proces pretvaranja bytecoda u nativni mašinski kod u toku rada; npr. C1 i C2 kompajler u HotSpot virtuelnoj mašini, Graal kompajler.
- Ahead-of-time kompajler: proces direktnog kompajliranja programa u binarni kod vezan za skup instrukcija ciljne mašine; npr. jaotc iz JDK-a, GUN Compiler for the Java (GCJ), Excelsior JET.
7.2 Interpretator i kompajler
U HotSpot-u, Java program inicijalno interpretirano izvršava interpretator (Interpreter); prednost toga jeste u tome što se može uštedeti vreme kompajliranja i omogućiti brzo pokretanje programa.
Kada se program pokrene, ako virtuelna mašina otkrije da se neka metoda ili blok koda izvršava posebno često, koristiće kompajler da ih prevede u nativni mašinski kod i primeniti različita sredstva optimizacije, čime se poboljšava efikasnost izvršavanja — to je just-in-time kompajler.
HotSpot u sebi ima dva (ili tri) just-in-time kompajlera:
- Klijentski kompajler (Client Compiler): kraće C1;
- Serverski kompajler (Server Compiler): kraće C2; u nekim materijalima i u JDK izvornom kodu naziva se i Opto kompajler;
- Graal kompajler: pojavio se tek u JDK 10; dugoročni cilj mu je da zameni C2.
Pre pojave režima rada sa slojevitim kompajliranjem, izbor između klijentskog ili serverskog kompajlera u potpunosti je zavisio od toga da li virtuelna mašina radi u klijentskom ili serverskom režimu; to se moglo navesti pri pokretanju parametrima -client ili -server, a može se prepustiti i virtuelnoj mašini da na osnovu sopstvene verzije i performansi domaćina samostalno odabere.
7.3 Slojevito kompajliranje
Za kompajliranje koda višeg stepena optimizacije obično je potrebno duže vreme kompajliranja; radi postizanja najbolje ravnoteže između brzine pokretanja programa i efikasnosti rada, HotSpot je u podsistem za kompajliranje uveo slojevito kompajliranje (Tiered Compilation):
- Sloj 0: program se izvršava čisto interpretirano, a interpretator ne uključuje funkciju nadzora performansi;
- Sloj 1: klijentski kompajler prevodi bytecode u nativni kod radi izvršavanja, vrši jednostavne i pouzdane stabilne optimizacije i ne uključuje funkciju nadzora performansi;
- Sloj 2: i dalje se koristi klijentsko kompajliranje, ali se uključuje samo ograničeni nadzor performansi, poput brojanja poziva metoda i broja povratnih ivica;
- Sloj 3: i dalje se koristi klijentsko kompajliranje, ali se uključuje potpuni nadzor performansi;
- Sloj 4: serverski kompajler prevodi bytecode u nativni kod; traje duže i, na osnovu informacija nadzora performansi, vrši neke nepouzdane agresivne optimizacije.
Gorenavedeni slojevi nisu fiksni; u zavisnosti od različitih parametara rada i verzija, virtuelna mašina može prilagoditi broj slojeva. Interakcija i prelazi između slojeva kompajliranja prikazani su na slici:

Nakon uvođenja slojevitog kompajliranja, interpretator, klijentski i serverski kompajler radiće istovremeno; klijentski kompajler može se koristiti za postizanje veće brzine kompajliranja, a serverski za postizanje boljeg kvaliteta kompajliranja.
7.4 Detekcija vrućih tačaka
Cilj just-in-time kompajliranja jeste "vrući kod", koji se uglavnom deli u dve kategorije:
- Metode koje su više puta pozivane.
- Tela petlji koja su više puta izvršavana. Ovde se misli na metodu koja je pozivana samo nekoliko puta, ali u telu metode postoji petlja sa velikim brojem iteracija; u tom trenutku se i ona smatra vrućim kodom. Ali kompajler i dalje kompajlira metodu u kojoj se nalazi petlja, a ne izdvojeno telo petlje.
Delovanje kojim se utvrđuje da li je neki deo koda vrući kod naziva se "detekcija vrućih tačaka" (Hot Spot Code Detection); glavne metode detekcije vrućih tačaka su sledeće dve:
- Detekcija vrućih tačaka zasnovana na uzorkovanju (Sample Based Hot Spot Code Detection): virtuelne mašine koje koriste ovu metodu periodično proveravaju vrhove steka poziva različitih niti; ako otkriju da se neka (ili neke) metoda često pojavljuje na vrhu steka, smatraju je "vrućom metodom".
- Detekcija vrućih tačaka zasnovana na brojačima (Counter Based Hot Spot Code Detection): virtuelne mašine koje koriste ovu metodu za svaku metodu (čak i blok koda) uspostavljaju brojač, koji beleži broj izvršavanja metode; ako broj izvršavanja premaši izvestan prag, metoda se smatra "vrućom metodom".
8. Optimizacija koda
Pored prevđenja bytecoda u nativni mašinski kod, just-in-time kompajler vrši i izvesnu optimizaciju koda; on obuhvata i do nekoliko desetina tehnika optimizacije; ovde biramo četiri reprezentativne radi predstavljanja:
8.1 Umetanje metoda
Najvažnije sredstvo optimizacije — kod ciljne metode se "kopira" netaknut u metodu koja je poziva, čime se izbegava stvarni poziv metode; za rešavanje problema umetanja virtuelnih metoda (u Java jeziku podrazumevane metode instance su virtuelne metode) koristi se tehnika poznata kao analiza hijerarhije klasa (Class Hierarchy Analysis, CHA).
8.2 Analiza bekstva
Ponašanja bekstva uglavnom se dele u dve kategorije:
- Bekstvo iz metode: kada se objekat definiše unutar metode, on može biti referenciran spoljnim metodama — na primer, prosleđivanjem kao argument poziva u druge metode; to se naziva bekstvom iz metode;
- Bekstvo iz niti: kada se objekat definiše unutar metode, on može biti pristupan spoljnim nitima — na primer, dodelom promenljivoj instance kojoj se može pristupiti iz drugih niti; to se naziva bekstvom iz niti, a stepen bekstva je viši od bekstva iz metode.
public static StringBuilder concat(String... strings) {
StringBuilder sb = new StringBuilder();
for (String string : strings) {
sb.append(string);
}
return sb; // došlo je do bekstva iz metode
}
public static String concat(String... strings) {
StringBuilder sb = new StringBuilder();
for (String string : strings) {
sb.append(string);
}
return sb.toString(); // nije došlo do bekstva iz metode
}Ako se može dokazati da objekat neće pobeći izvan metode ili niti, ili da je stepen bekstva relativno nizak (bekstvo samo iz metode, ali ne i iz niti), nad tom instancom objekta mogu se preduzeti različiti stepeni optimizacije:
- Dodela na steku (Stack Allocations): ako objekat neće pobeći izvan niti, memorija za kreiranje tog objekta biće dodeljena na steku, a ne na Java heap-u; u tom trenutku memorijski prostor koji objekat zauzima uništiće se sa skidanjem okvira steka sa steka, čime se smanjuje pritisak sakupljanja smeća.
- Zamena skalara (Scalar Replacement): ako podatak više ne može biti razložen na manje tipove podataka, takvi podaci se nazivaju skalari (npr. numerički tipovi poput int, long i referentni tip itd.); nasuprot tome, ako se podatak može dalje razložiti, naziva se agregat (npr. objekat). Ako objekat ne može pobeći izvan metode, može se zameniti direktnim kreiranjem nekoliko promenljivih članica koje ta metoda koristi, čime se smanjuje zauzeće memorije.
- Eliminacija sinhronizacije (Synchronization Elimination): ako promenljiva ne može pobeći izvan niti, mere sinhronizacije primenjene na nju mogu se eliminisati.
8.3 Eliminacija zajedničkih podizraza
Ako je izraz E već bio izračunat ranije, a od tog prethodnog izračunavanja do sada vrednosti svih promenljivih u E nisu se promenile, ovaj novi izraz E naziva se zajedničkim podizrazom. Za takav izraz nema potrebe ponovo ga računati — dovoljno je direktno iskoristiti prethodni rezultat izračunavanja.
8.4 Eliminacija provera granica niza
Za podsistem za izvršavanje virtuelne mašine, svako čitanje i pisanje elementa niza nosi jednu implicitnu proveru konteksta radi izbegavanja pristupa izvan opsega. Ako pristup nizu dolazi unutar petlje i za pristup podacima se koristi promenljiva petlje — odnosno vrednost promenljive petlje je uvek u opsegu [0, list.length) — tada se može eliminisati provera granica podataka za celu petlju, čime se izbegava višestruko beskorisno proveravanje.
Rezime
U ovom članku sistematski smo sumirali najvažnije tačke JVM znanja, kao što su memorijska struktura JVM-a, algoritmi sakupljanja smeća, sakupljači smeća, mehanizam učitavanja klasa, class loaderi, kompajliranje programa, optimizacija koda itd. Nadamo se da će vam pomoći pri učenju JVM-a.
- Referentni link: https://github.com/heibaiying/Full-Stack-Notes
- Priredio: Chenmo Wang Er
