Duboko razumevanje runtime oblasti podataka JVM-a
Ranije smo već spomenuli da Java izvorni fajl, nakon prevođenja kompilatorom, generiše bytecode fajl, koji se zatim, pošto ga class loader učita, predaje izvršnom engine-u na izvršenje. Tokom izvršenja, JVM izdvaja prostor za čuvanje podataka potrebnih tokom rada programa; taj prostor se opšte poznato naziva runtime oblast podataka, kao na slici ispod.

Prema specifikaciji Java virtuelne mašine, runtime oblast podataka može se podeliti na sledeće delove:
- Programski brojač (Program Counter Register)
- Java virtuelni mašinski stek (Java Virtual Machine Stacks)
- Stek nativnih metoda (Native Method Stack)
- Hip (Heap)
- Method area (Method Area)

Počevši od JDK 8, PermGen je potpuno uklonjen, a zamenio ga je Metaspace. Metaspace više nije deo memorije JVM-a, već je realizovan preko nativne memorije (Native Memory). Drugim rečima, od JDK 8 implementacija method area upravo je Metaspace.
Programski brojač
Programski brojač (Program Counter Register) zauzima neveliki memorijski prostor, veoma, veoma mali deo, i može se posmatrati kao indikator broja linije instrukcije bytecode-a koju tekuća nit izvršava. Interpreter bytecode-a, tokom rada, menja vrednost ovog brojača kako bi odabrao sledeću instrukciju bytecode-a koju treba izvršiti; grananje, petlje, skokovi, obrada izuzetaka i oporavak niti — sve te funkcije oslanjaju se na taj brojač.
U JVM-u se višenitnost ostvaruje naizmeničnim preklapanjem niti pri dobijanju procesorskog vremena, pa u svakom konkretnom trenutku jedna jezgra CPU-a izvršava instrukcije samo jedne niti. Da bi se nakon promene niti moglo vratiti na pravu poziciju izvršenja, svaka nit mora imati sopstveni programski brojač koji ne sme međusobno smetati, inače bi to uticalo na pravilan redosled izvršenja programa.
Drugim rečima, zahtevamo da programski brojač bude privatno vlasništvo niti.
Prema „Specifikaciji Java virtuelne mašine", ukoliko nit izvršava metod koji nije nativni, u programskom brojaču se čuva adresa instrukcije koja treba da se izvrši; ukoliko nit izvršava nativni metod, vrednost programskog brojača je undefined.
Zašto je vrednost nativnog metoda u programskom brojaču undefined? Zato što se nativni metodi uglavnom realizuju pomoću C/C++ i nisu prevedeni u instrukcije bytecode-a koje treba izvršiti.
Pogledajmo kroz kod i instrukcije bytecode-a koja je uloga programskog brojača.
public static int add(int a, int b) {
return a + b;
}Instrukcije bytecode-a grubo izgledaju ovako:
0: iload_0 // učitava promenljivu a iz tabele lokalnih promenljivih na operand stek
1: iload_1 // učitava promenljivu b iz tabele lokalnih promenljivih na operand stek
2: iadd // sabira dva broja
3: ireturn // vraća rezultatHajde sada da korak po korak analiziramo kako se programski brojač ažurira pri izvršenju ovih instrukcija:
Početno stanje: kada metod počinje sa izvršenjem, PC brojač se postavlja na 0, upućujući na prvu instrukciju
0: iload_0.Izvršenje prve instrukcije:
- Izvršava se instrukcija
iload_0, koja sa indeksa 0 u tabeli lokalnih promenljivih (to je prvi argument metode,a) učitava celi broj na vrh operand steka. - Nakon izvršenja, PC brojač se ažurira na 1, upućujući na sledeću instrukciju
1: iload_1.
- Izvršava se instrukcija
Izvršenje druge instrukcije:
- Izvršava se instrukcija
iload_1, koja sa indeksa 1 u tabeli lokalnih promenljivih (to je drugi argument metode,b) učitava celi broj na vrh operand steka. - Nakon izvršenja, PC brojač se ažurira na 2, upućujući na sledeću instrukciju
2: iadd.
- Izvršava se instrukcija
Izvršenje treće instrukcije:
- Izvršava se instrukcija
iadd, koja skida dva cela broja sa vrha operand steka (to suaib), sabira ih i rezultat gura na vrh operand steka. - Nakon izvršenja, PC brojač se ažurira na 3, upućujući na sledeću instrukciju
3: ireturn.
- Izvršava se instrukcija
Izvršenje poslednje instrukcije:
- Izvršava se instrukcija
ireturn, koja skida ceo broj sa vrha operand steka (rezultata + b) i koristi ga kao povratnu vrednost metode. - Izvršenje metode je završeno; kontrola se vraća pozivaocu metode.
- Izvršava se instrukcija
Java virtuelni mašinski stek
Java virtuelni mašinski stek (JVM stek) sastoji se od okvira steka (stack frame); svaki okvir steka odgovara jednoj pozvanoj metodi. Kada nit izvršava metod, kreira se odgovarajući okvir steka i gura na stek. Kada se izvršenje metode završi, okvir steka se uklanja sa steka.
Okvir steka sadrži sledećih 5 delova, kao na slici. Okvire steka smo već ranije detaljno obradili; oni čitaoci koji su zaboravili mogu se vratiti i ponovo pročitati.

Pretpostavimo da imamo jednostavan metod add, kao što je prikazano:
public int add(int a, int b) {
int result = a + b;
return result;
}Kada se pozove metod add, JVM za taj poziv kreira novi okvir steka. Zatim se izvršavaju instrukcije bytecode-a unutar metode — ovaj deo smo već obradili; možete sami putem javap pregledati bytecode i simulirati proces izvršenja instrukcija bytecode-a.
Kada se metod add završi, odgovarajući okvir steka se skida sa JVM steka.
Karakteristike Java virtuelnog mašinskog steka su:
- Privatno za nit: svaka nit ima sopstveni JVM stek; stekovi se ne dele među nitima.
- Prelivanje steka: ako dubina steka prekorači dubinu koju JVM stek dozvoljava, biće bačen
StackOverflowError, što smo već spominjali kada smo obrađivali okvir steka.
Možete li pogoditi koja je podrazumevana veličina JVM steka?
Iskoristimo ponovo primer iz obrade okvira steka:
public class StackOverflowErrorTest1 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
while (true) {
testStackOverflowError();
}
}
public static void testStackOverflowError() {
System.out.println(count.incrementAndGet());
testStackOverflowError();
}
}U podrazumevanoj konfiguraciji, izuzetak zbog prelivanja steka javlja se nakon 10886 poziva:

Dodajmo -Xss256k i pokušajmo ponovo.

Izuzetak se javlja već nakon 1991 poziva.

Kakva je veza između ovih vrednosti?
Komandom java -XX:+PrintFlagsFinal -version | grep ThreadStackSize možemo saznati podrazumevanu veličinu JVM steka.

Jedinica za ThreadStackSize je bajt, što znači da je podrazumevana veličina JVM steka 1024 KB, odnosno 1M.
Drugim rečima, JVM stek od 1024 KB podrazumevano može izvršiti 10885 poziva metoda testStackOverflowError, dok JVM stek od 256 KB može samo 1990 poziva — otprilike pet puta manje.
Stek nativnih metoda
Stek nativnih metoda (Native Method Stack) sličan je Java virtuelnom mašinskom steku, samo što JVM stek služi virtuelnoj mašini za izvršenje Java metoda, dok stek nativnih metoda služi za nativne metode koje virtuelna mašina koristi.
Hip
Hip je memorijska oblast koju sve niti dele; kreira se pri pokretanju JVM-a i služi za čuvanje objekata (i nizovi su vrsta objekata).
Nekada su se „skoro svi" objekti u Javi alocirali na hipu, ali sa razvojem JIT kompilatora i sazrevanjem tehnike analize bekstva (escape analysis), alociranje svih objekata na hipu postepeno je prestalo da bude „apsolutno". Počevši od JDK 7, Java virtuelna mašina podrazumevano uključuje analizu bekstva, što znači da, ako referenca na objekat u nekim metodama ne biva vraćena niti je spolja koristi (odnosno ne „pobegne"), taj objekat može direktno biti alociran na steku.

Stek je onaj JVM stek koji smo malopre spomenuli (uglavnom čuva lokalne promenljive, argumente metoda, reference na objekte itd.), privatno vlasništvo niti, obično nestaje sa završetkom poziva metode, pa nema potrebe za garbage collection; o hipu smo takođe već govorili — to je memorijska oblast u zajedničkom vlasništvu niti na kojoj se alocira skoro svaki objekat, a životni vek nije određen jednim pozivom metode, te objekat može nastaviti da postoji i nakon završetka poziva metode, sve dok ga neka promenljiva ne prestane da referencira — tada ga prikuplja garbage collector.
Ukratko objasnimo JIT i analizu bekstva (detaljnije u poglavlju o JIT).
Uobičajeni kompilirani jezici poput C++ najčešće prevode kod neposredno u mašinski kod razumljiv CPU-u. Java pak, kako bi ostvarila osobinu „jednom prevedi, svuda izvršavaj", proces prevođenja deli na dva dela: prvo se izvorni kod prevodi (putem javac) u univerzalni intermedijalni oblik — bytecode — a zatim interpreter instrukciju po instrukciju tumači bytecode u mašinski kod i izvršava ga. Stoga u pogledu performansi Java može zaostajati za kompiliranim jezicima poput C++.

Kako bi optimizovala performanse Jave, JVM pored interpretera uvodi i JIT kompilator: pri radu programa prvo deluje interpreter, pa se kod može neposredno izvršavati. Vremenom just-in-time kompilator preuzima sve više uloge, prevodeći i optimizujući sve više koda u nativni kod, čime se postiže veća efikasnost izvršenja. Interpreter tu služi kao degradirani način rada — ako se jave problemi sa nepouzdanim optimizacijama kompilacije, vraća se na interpretirano izvršenje, čime se obezbeđuje normalan rad programa.
Analiza bekstva (Escape Analysis) tehnika je optimizacije u kompilatoru kojom se procenjuje obim važenja i životni vek objekta. Ako kompilator utvrdi da objekat ne može „pobeći" izvan opsega metoda ili niti, može izabrati da ga alocira na steku umesto na hipu. Time se smanjuje pritisak na garbage collection i poboljšavaju performanse.
Napišimo komad koda koji bi mogao da pokrene alokaciju na steku.
public class EscapeAnalysisExample {
private static class Point {
private int x;
private int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
int calculate() {
return x + y;
}
}
public static void main(String[] args) {
int total = 0;
for (int i = 0; i < 1000000; i++) {
total += createAndCalculate();
}
System.out.println(total);
}
private static int createAndCalculate() {
Point p = new Point(1, 2);
return p.calculate();
}
}- Metod createAndCalculate kreira jedan Point objekat i poziva njegov metod calculate.
- Point objekat se kreira u metodu createAndCalculate i ne pobegne van tog metoda.
- Ukoliko analiza bekstva u JVM-u utvrdi da Point objekat ne beži izvan metoda createAndCalculate, može ga alocirati na steku umesto na hipu.
Hip smo već ranije obradili; pored toga što je sabirno mesto objekata, to je i glavna oblast pod upravom Java garbage collectora, pa se naziva i GC hip (Garbage Collected Heap). Sa stanovišta garbage collection-a, pošto skoro svi garbage collectori koriste generacijski algoritam prikupljanja, hip se može dalje deliti na: mladu generaciju i staru generaciju. Mlada generacija se može deliti na: Eden prostor, From Survivor, To Survivor prostor itd. Svrha dalje podela je bolje prikupljanje memorije, odnosno brže alociranje memorije.
Ne brinite, o svemu tome ćemo detaljno govoriti u poglavlju o garbage collection.
Na hipu se najčešće javlja greška OutOfMemoryError, u sledećim oblicima:
OutOfMemoryError: GC Overhead Limit Exceeded: nastaje kada JVM previše vremena provodi na garbage collection-u, a pritom može da prikupi veoma malo prostora na hipu.java.lang.OutOfMemoryError: Java heap space: ako pri kreiranju novog objekta na hipu nema dovoljno prostora za novi objekat, javlja se ova greška. Nema veze sa fizičkom memorijom računara, već sa veličinom memorije koju smo konfigurisali za virtuelnu mašinu!
Hajde da kroz kod simuliramo prelivanje memorije hipa.
public class HeapSpaceErrorGenerator {
public static void main(String[] args) {
List<byte[]> bigObjects = new ArrayList<>();
try {
while (true) {
// kreira niz od oko 10MB
byte[] bigObject = new byte[10 * 1024 * 1024];
bigObjects.add(bigObject);
}
} catch (OutOfMemoryError e) {
System.out.println("OutOfMemoryError nastao nakon " + bigObjects.size() + " objekata");
throw e;
}
}
}VM parametrom veličinu memorije hipa postavimo na -Xmx128M, a zatim pokrenemo program.

Vidi se da je prelivanje memorije hipa nastupilo nakon 11 objekata.

Koja je podrazumevana veličina memorije hipa?
Komandom java -XX:+PrintFlagsFinal -version | grep HeapSize možemo saznati podrazumevanu veličinu JVM hipa.

Isto možemo dobiti i sledećim retkom koda:
System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "MB");Možete gorenavedenim postupkom proveriti veličinu memorije hipa na svom računaru.
Metaspace i method area
Method area je logička oblast u specifikaciji Java virtuelne mašine sa različitim implementacijama u različitim verzijama JDK-a. U JDK 7 method area se nazivala PermGen, a u JDK 8 PermGen je potpuno uklonjen i zamenjen Metaspace-om.
Ukoliko u nekim izvorima i dalje naiđete na PermGen, to je bilo Erge koji vam to objašnjava, ili je pak sadržaj zastareo.
„Specifikacija Java virtuelne mašine" propisuje samo pojam method area i njegovu ulogu, ali ne i način realizacije. Različite implementacije Java virtuelne mašine mogu se razlikovati. PermGen je jedan oblik realizacije method area u HotSpot-u. Drugim rečima, PermGen je implementacija u starim verzijama HotSpot-a, dok je method area definicija u specifikaciji Java virtuelne mašine — jedna specifikacija.
Drugačije rečeno, odnos između method area i PermGen-a poput je odnosa interfejsa i klase u Javi: klasa implementira interfejs, interfejs i dalje ostaje taj isti interfejs, ali je implementacija potpuno unapređena.

Pre JDK 7 postojao je samo pojam constant pool-a, u okviru method area.
U JDK 7 string constant pool je izdvojen iz method area i premešten u hip, dok je runtime constant pool ostao u method area (odnosno u PermGen-u).
U JDK 8 HotSpot uklanja PermGen i zamenjuje ga Metaspace-om. String constant pool ostaje u hipu, dok runtime constant pool prelazi u Metaspace.

Zašto zameniti PermGen Metaspace-om?
Stara verzija HotSpot virtuelne mašine nije imala JIT, dok je Oracle-ova druga virtuelna mašina, JRockit, imala. Da bi bolje prepustila Java carstvo naslednicima, Oracle je želeo da tehnologiju JIT iz starijeg JRockit-a stopi sa naslednikom HotSpot-om.
Pošto u JRockit virtuelnoj mašini nije postojao pojam PermGen-a, novi HotSpot je prosto odbacio PermGen, uzeo deo memorije operativnog sistema i tu memoriju nazvao Metaspace.
Veličina Metaspace-a više nije ograničena maksimalnom veličinom hipa postavljenom pri pokretanju JVM-a, već direktno koristi nativnu memoriju, odnosno memoriju operativnog sistema, čime se efikasno rešava problem OutOfMemoryError greške.
Podrazumevanu veličinu memorije hipa JVM-a možete videti komandom
java -XX:+PrintFlagsFinal -version | grep HeapSize.

Kada podaci u Metaspace-u rastu, JVM od operativnog sistema traži da alokira još memorije. Ukoliko je memorijskog prostira dovoljno, operativni sistem ispuni JVM-ov zahtev. Da li može doći do prelivanja Metaspace-a?
Odgovor je da, a o tome ćemo detaljno govoriti u poglavlju o prelivanju memorije.
Runtime constant pool
Kada smo obrađivali bytecode, detaljno smo govorili o constant pool-u; to je skladište resursa bytecode fajla: prvo veličina constant pool-a, od 1 do n−1, 0 je rezervisani indeks, a zatim skup stavki constant pool-a, uključujući informacije o klasama, poljima, metodama, interfejsima, stringovnim konstantama i sl.

Runtime constant pool, kako mu ime kaže, jest ono što dobijemo kada JVM tokom rada učita constant pool iz bytecode fajla u memoriju i smesti ga u runtime constant pool.
Dakle, constant pool nalazi se u bytecode fajlu, a runtime constant pool u Metaspace-u (od JDK 8 nadalje) — u pitanju je ista stvar, ali u različitom obliku, baš kao da je jedno čvrsto, a drugo tečno; ili kao kalup prema posuđu u kalupu.

String constant pool
String constant pool detaljno smo obradili u poglavlju o stringovima; njegova uloga je da čuva stringovne konstante, odnosno stringove koje napišemo u kodu. I dalje se nalazi u hipu.

OK, method area (bez obzira na to da li je u pitanju realizacija kroz PermGen ili Metaspace), jednako kao i hip, oblast je koju niti dele.

Kratak pregled
Rezimirajmo glavne sastave runtime oblasti podataka:
- PC registar (PC Register), poznat i kao programski brojač (Program Counter Register), manji je memorijski prostor; njegova uloga može se posmatrati kao signalni indikator bytecode-a koji tekuća nit izvršava.
- JVM stek (Java Virtual Machine Stack), kao i PC registar, privatno je vlasništvo niti. Svaka JVM nit ima svoj JVM stek (poznat i kao metodski stek), koji se kreira zajedno sa niti; njegov životni vek jednak je životnom veku niti.
- Stek nativnih metoda (Native Method Stack) — JVM može koristiti tradicionalni stek da podrži izvršenje nativnih metoda; taj stek je stek nativnih metoda.
- Hip (Heap) — u JVM-u to je runtime memorijska oblast koja može da se deli među nitima, ali i oblast u kojoj sve instance klasa i objekti podataka alociraju memoriju.
- Method area (Method area) — od JDK 8 Metaspace zamenjuje PermGen. Method area je logička oblast u JVM-u koja čuva strukturne informacije o klasama: definicije klasa, definicije metoda, definicije polja i instrukcije bytecode-a. Razlika je u tome što Metaspace više nije deo memorije JVM-a, već je realizovan preko nativne memorije (Native Memory).
- Runtime constant pool je runtime oblik konstanti svake klase ili interfejsa; obuhvata numeričke literale poznate u vreme kompilacije, kao i reference na metode ili polja dobijene razrešavanjem tokom rada. Ukratko, kada se pozove metod ili referencira promenljiva, JVM putem runtime constant pool-a traži stvarnu adresu metoda ili promenljive u memoriji.
Pri pokretanju JVM-a veličinu Metaspace-a određuje parametar MaxMetaspaceSize; tokom rada JVM automatski prilagođava veličinu Metaspace-a radi prilagođavanja različitim potrebama programa.

