Duboko razumevanje strukture okvira steka JVM-a
Kada smo obrađivali stek mašine i registarske mašine, spomenuli smo strukturu okvira steka; u obradi instrukcija bytecode-a pomenuli smo i operand stek unutar okvira steka. Dakle, danas ćemo detaljno obraditi strukturu okvira steka JVM-a, kako biste stekli jasniju predstavu o okviru steka.
Krenimo od sledeće slike.

Java izvorni fajl se nakon prevođenja kompilatorom pretvara u bytecode fajl, koji zatim učita class loader JVM-a, a zatim ga predaje izvršnom engine-u. Tokom izvršenja, JVM izdvaja memorijski prostor za čuvanje podataka potrebnih u radu programa; taj prostor se opšte poznato naziva runtime oblast podataka.
Okvir steka (Stack Frame) jeste struktura podataka u runtime oblasti podataka koja podržava virtuelnu mašinu pri pozivu i izvršenju metoda. Svaki metod, od početka poziva do završetka izvršenja, odgovara jednom okviru steka u procesu ulaska/izlaska sa steka virtuelnog mašinskog / steka nativnih metoda.
Nativni metod, odnosno native metod, detaljno smo ranije obradili; realizovan je u C/C++.
Svaki okvir steka obuhvata tabelu lokalnih promenljivih, operand stek, dinamičko povezivanje, adresu povratka metode i neke dodatne prateće informacije.
Pri kompilaciji koda, kolika tabela lokalnih promenljivih je potrebna u okviru steka i koje dubine operand stek, već su potpuno određeni i upisani u Code atribut tabele metoda.
Tabelu metoda i tabelu lokalnih promenljivih smo obrađivali u poglavlju o bytecode-u; možete se klikom na link vratiti i ponovo pročitati; ovaj tekst će ih takođe dodatno obraditi.
Lanac poziva metoda u jednoj niti može biti vrlo dugačak, a mnogo metoda se nalazi u stanju izvršenja. U tekućoj niti, okvir steka koji se nalazi na vrhu steka naziva se tekući okvir steka (Current Stack Frame), a metod povezan sa tim okvirom steka naziva se tekućim metodom. Sve instrukcije bytecode-a koje izvršni engine izvršava deluju na tekući okvir steka. U konceptualnom modelu struktura okvira steka prikazana je na slici ispod:

Tabela lokalnih promenljivih
Tabela lokalnih promenljivih (Local Variables Table) služi za čuvanje lokalnih promenljivih u metodu, kao i argumenata metoda. Kada se Java izvorni fajl prevede u class fajl, maksimalni kapacitet tabele lokalnih promenljivih već je utvrđen.
Pogledajmo sledeći kod.
public class LocalVaraiablesTable {
private void write(int age) {
String name = "Chenmo Wang Er";
}
}Metod write() ima jedan argument age i jednu lokalnu promenljivu name.
Zatim u IntelliJ IDEA-i, pomoću jclasslib-a, pregledajmo prevedeni bytecode fajl LocalVaraiablesTable.class. U Code atributu metoda write() možemo videti da je vrednost polja Maximum local variables (maksimalni kapacitet tabele lokalnih promenljivih) jednaka 3.

Prema logici stvari, maksimalni kapacitet tabele lokalnih promenljivih trebalo bi da bude 2 — jedan za age, jedan za name — zašto je onda 3?
Kada se pozove članski metod (nestatički metod), promenljiva sa indeksom 0 zapravo je referenca na objekat koji poziva taj članski metod — dakle onaj slavni this. Poziv write(18) zapravo je poziv write(this, 18).
Otvorimo Code atribut i u LocalVariableTable možemo videti detaljne podatke.

Indeks 0 je this, tipa LocalVaraiablesTable objekat; indeks 1 je argument metode age, tipa int; indeks 2 je lokalna promenljiva unutar metode, name, tipa String.
Naravno, veličina tabele lokalnih promenljivih nije zbir svih lokalnih promenljivih u metodu; ona zavisi od tipa promenljive i njenog opsega važenja. Kada se opseg važenja neke lokalne promenljive završi, mesto koje ona zauzima u tabeli lokalnih promenljivih preuzima sledeća lokalna promenljiva.
Pogledajmo sledeći kod.
public static void method() {
// ①
if (true) {
// ②
String name = "Chenmo Wang Er";
}
// ③
if(true) {
// ④
int age = 18;
}
// ⑤
}- Veličina tabele lokalnih promenljivih metoda
method()je 1, jer je u pitanju statički metod, pa this ne mora da se doda kao prvi element tabele lokalnih promenljivih; - u ② postoji jedna lokalna promenljiva name, pa veličina tabele lokalnih promenljivih postaje 1;
- u ③ opseg važenja promenljive name se završava;
- u ④ postoji jedna lokalna promenljiva age, pa veličina tabele lokalnih promenljivih iznosi 1;
- u ⑤ opseg važenja promenljive age se završava;
U vezi sa opsegom važenja lokalnih promenljivih, u knjizi „Effective Java" (stavka 57) stoji preporuka:
Sužavanje opsega lokalnih promenljivih poboljšava čitljivost i održivost koda i smanjuje verovatnoću grešaka.
Ovde imam još jednu napomenu. Da bi se memorijski prostor okvira steka štedio koliko god je moguće, slotovi u tabeli lokalnih promenljivih mogu se ponovo koristiti — baš kao što demonstrira metod method() — što znači da razuman opseg važenja doprinosi performansama programa. Nije li to zanimljivo?
Kapacitet tabele lokalnih promenljivih ima slot (slot) kao najmanju jedinicu; jedan slot može primiti tip podataka od 32 bita (npr. int; doduše, „Specifikacija Java virtuelne mašine" ne određuje izričito veličinu memorijskog prostora koju jedan slot zauzima, ali smatram da je ovako lakše za razumevanje). Tipovi podataka koji izričito zauzimaju 64 bita, poput float i double, zauzeće dva susedna slota.
Pogledajmo sledeći kod.
public void solt() {
double d = 1.0;
int i = 1;
}Pomoću jclasslib-a možemo videti da je vrednost polja Maximum local variables metoda solt() jednaka 4.

Zašto baš 4? Sa this ih ukupno 3?

Pogledamo li LocalVariableTable postaje jasno — indeks promenljive i je 3, što znači da promenljiva d zauzima dva slota.

Operand stek
Kao i tabela lokalnih promenljivih, maksimalna dubina operand steka (Operand Stack) utvrđuje se u vreme kompilacije i upisuje u maximum stack size Code atributa. Kada metod tek započne izvršenje, operand stek je prazan; tokom izvršenja metoda razne instrukcije bytecode-a upisuju i uzimaju podatke sa operand steka, odnosno vrše operacije ulaska (push) i izlaska (pop).
Pogledajmo sledeći kod.
public class OperandStack {
public void test() {
add(1,2);
}
private int add(int a, int b) {
return a + b;
}
}Klasa OperandStack ukupno ima 2 metoda; u metodu test() poziva se metod add() sa 2 prosleđena argumenta. Pomoću jclasslib-a se vidi da je vrednost maximum stack size metoda test() jednaka 3.

To je zato što pri pozivu članskog metoda this i svi argumenti bivaju gurnuti na stek, a nakon završetka poziva this i argumenti se jedan po jedan skidaju sa steka. Preko panela „Bytecode” možemo videti odgovarajuće instrukcije bytecode-a.

- aload_0 služi da sa indeksa 0 u tabeli lokalnih promenljivih učita referentnu promenljivu, odnosno this, na operand stek;
- iconst_1 služi da učita celi broj 1 na operand stek;
- iconst_2 služi da učita celi broj 2 na operand stek;
- invokevirtual služi za poziv članskog metoda objekta;
- pop služi da skine vrednost sa vrha steka;
- return je instrukcija povratka za void metod.
Instrukcije bytecode-a smo već ranije obradili; oni koji su zaboravili mogu ponovo prelistati. Pogledajmo sada instrukcije bytecode-a metoda add().

- iload_1 služi da sa indeksa 1 u tabeli lokalnih promenljivih učita promenljivu tipa int na operand stek (na indeksu 0 je this);
- iload_2 služi da sa indeksa 2 u tabeli lokalnih promenljivih učita promenljivu tipa int na operand stek;
- iadd služi za sabiranje tipa int;
- ireturn je instrukcija povratka za metod čija je povratna vrednost tipa int.

Tipovi podataka u operand steku moraju se podudarati sa instrukcijama bytecode-a; za instrukciju iadd, na primer, ona se može koristiti samo za sabiranje celobrojnih podataka — pri njenom izvršenju dve vrednosti na vrhu steka moraju biti tipa int, i ne može se desiti da se long i double sabiraju komandom iadd.
Dinamičko povezivanje
Svaki okvir steka sadrži referencu na metod kom taj okvir steka pripada u runtime constant pool-u; tu referencu drži radi podrške dinamičkom povezivanju (Dynamic Linking) u procesu poziva metoda.

①, ranije smo već spomenuli da je method area jedna runtime memorijska oblast JVM-a, logička definicija koja u različitim verzijama JDK-a ima različite implementacije, ali čija je glavna uloga čuvanje informacija o učitanim klasama, konstanti, statičkim promenljivama, kao i koda dobijenog JIT kompilacijom itd.
②, runtime constant pool (Runtime Constant Pool) deo je method area i služi za čuvanje raznih literala i simboličkih referenci nastalih u vreme kompilacije — posle učitavanja klase ulaze u runtime constant pool. O method area-i ćemo takođe govoriti detaljnije kasnije.
Pogledajmo sledeći kod.
public class DynamicLinking {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("Muškarac, plači, plači, to nije greh");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("Žena pod planinom je tigar");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}Ukoliko razumete Java redefinisanje metoda (override), trebalo bi da vam je jasan značaj ovog koda. Klase Man i Woman nasleđuju klasu Human i redefinišu metod sayHello(). Pogledajmo rezultat izvršenja:
Muškarac, plači, plači, to nije greh
Žena pod planinom je tigar
Žena pod planinom je tigarOvaj rezultat se lako razume: referentni tip promenljive man jeste Human, ali upućuje na objekat Man; referentni tip promenljive woman takođe je Human, ali upućuje na objekat Woman; nakon toga man upućuje na novi objekat Woman.
Sa stanovišta objektno orijentisanog programiranja, odnosno sa stanovišta polimorfizma, rezultat lako razumemo; ali sa stanovišta Java virtuelne mašine — kako ona zaključuje koji metod man i woman treba da pozovu?
Pogledajmo pomoću jclasslib-a instrukcije bytecode-a metoda main.

- Red 1: instrukcija new kreira jedan objekat Man i gura memorijsku adresu objekta na stek.
- Red 2: instrukcija dup duplira vrednost sa vrha steka i gura je na vrh. Pošto naredna instrukcija invokespecial troši jednu referencu na tekuću klasu, potrebno je napraviti kopiju.
- Red 3: instrukcija invokespecial služi za poziv konstruktora radi inicijalizacije.
- Red 4: astore_1 — Java virtuelna mašina skida referencu na objekat Man sa vrha steka i smešta je u lokalnu promenljivu man sa indeksom 1.
- Redovi 5, 6, 7 i 8 slični su redovima 1, 2, 3 i 4, s tom razlikom što je u pitanju objekat Woman.
- Red 9: instrukcija aload_1 gura lokalnu promenljivu man na operand stek.
- Red 10: instrukcija invokevirtual poziva članski metod objekta
sayHello(); obratite pažnju da je tip objekta ovdecom/itwanger/jvm/DynamicLinking$Human. - Red 11: instrukcija aload_2 gura lokalnu promenljivu woman na operand stek.
- Red 12 isti kao red 10.
Obratite pažnju: sa stanovišta bytecode-a, man.sayHello() (red 10) i woman.sayHello() (red 12) potpuno su isti, ali — kao što znamo — ciljni metodi koje te dve instrukcije na kraju izvršavaju nisu isti.
Šta se zapravo desilo?
Mora se krenuti od instrukcije invokevirtual i sagledati kako ona ostvaruje polimorfizam. Prema „Specifikaciji Java virtuelne mašine", proces razrešavanja instrukcije invokevirtual u toku rada može se podeliti na sledeće korake:
- ①, Pronađe se stvarni tip objekta na koji ukazuje element na vrhu operand steka i označi kao C.
- ②, Ukoliko se u tipu C pronađe metod koji se podudara sa deskriptorom u constant pool-u, vrši se provera prava pristupa; ukoliko prođe, vraća se direktna referenca tog metoda i pretraga se završava; u suprotnom baca se izuzetak
java.lang.IllegalAccessError. - ③, U suprotnom, po relaciji nasleđivanja, odozdo nagore se za sve roditeljske klase tipa C ponavlja korak 2 (pretraga i provera).
- ④, Ukoliko se metod nikada ne pronađe, baca se izuzetak
java.lang.AbstractMethodError.
Dakle, instrukcija invokevirtual već u prvom koraku utvrđuje stvarni tip u toku rada, pa pri oba poziva instrukcija invokevirtual ne završava time što simboličku referencu metode iz constant pool-a razreši na direktnu referencu, već na osnovu stvarnog tipa primaoca metoda bira verziju metoda — to je suština Java redefine (override). Proces u kojem se u toku rada, prema stvarnom tipu, utvrđuje verzija metoda za izvršenje nazivamo dinamičko povezivanje (dynamic linking).
Adresa povratka metoda
Kada neki metod započne izvršenje, postoje samo dva načina za izlazak iz tog metoda:
- Normalan izlazak, pri čemu se eventualna povratna vrednost može proslediti pozivaocu iz gornjeg nivoa; da li metod ima povratnu vrednost i koji joj je tip određeno je instrukcijom povratka metoda — kao što je ireturn za tip int (koji smo pominjali), odnosno return za void metode; postoje i druge: lreturn za long, freturn za float, dreturn za double i areturn za referentni tip.
- Izlazak zbog izuzetka — metod je tokom izvršenja naišao na izuzetak koji nije propisno obrađen; u tom slučaju se pozivaocu iz gornjeg nivoa ne vraća nikakva vrednost.
Nije važno koji je način izlaska u pitanju — nakon izlaska iz metoda obavezno se mora vratiti na poziciju na kojoj je metod prvobitno pozvan, kako bi program mogao da nastavi izvršenje. Uglavnom, pri normalnom izlasku iz metoda vrednost PC brojača služi kao adresa povratka; okvir steka vrlo verovatno čuva tu vrednost brojača, dok pri izlasku zbog izuzetka to nije slučaj.
PC brojač: deo runtime oblasti podataka JVM-a; prati poziciju bytecode-a koji tekuća nit izvršava.
Proces izlaska iz metoda zapravo je jednak skidanju tekućeg okvira steka sa steka, pa sledeće moguće operacije obuhvataju: vraćanje tabele lokalnih promenljivih i operand steka metoda iz gornjeg nivoa, guranje povratne vrednosti (ukoliko postoji) na operand stek okvira steka pozivaoca, podešavanje vrednosti PC brojača, pronalaženje sledeće instrukcije koju treba izvršiti itd.
Dodatne informacije
Specifikacija virtuelne mašine dozvoljava da konkretna realizacija virtuelne mašine u okvir steka doda i neke informacije koje specifikacija ne opisuje, kao što su podaci vezani za debugovanje; te informacije u potpunosti zavise od konkretne realizacije virtuelne mašine. U praksi se dinamičko povezivanje, adresa povratka metoda i ostale dodatne informacije obično svrstavaju u jednu kategoriju koja se naziva informacije okvira steka.
StackOverflowError
Sledeći kod će pri izvršenju baciti izuzetak StackOverflowError.
public class StackOverflowErrorTest {
public static void main(String[] args) {
StackOverflowErrorTest test = new StackOverflowErrorTest();
test.testStackOverflowError();
}
public void testStackOverflowError() {
testStackOverflowError();
}
}Pogledajmo podatke steka ovog izuzetka.

Razlog bacanja izuzetka StackOverflowError jeste što pri izvršenju metoda testStackOverflowError() nastaje jedan okvir steka, zatim se poziva metod testStackOverflowError() pa nastaje još jedan okvir steka, pa se ponovo poziva testStackOverflowError() i nastaje još jedan okvir steka... i tako ukrug, sve dok memorija steka ne preplavi.
Hajde da jednostavno izmenimo kod i pogledamo stog izuzetka.
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();
}
}Izuzetak StackOverflowError baca se na 10924. poziv. Možete isprobati u svom lokalnom okruženju i videti posle koliko poziva se on javlja.

Kratak pregled
Okvir steka je struktura podataka u JVM-u koja služi za izvršenje metoda; kad god se metod pozove, JVM za njega kreira okvir steka, a nakon završetka izvršenja metoda ga uništava.
- Tabela lokalnih promenljivih: čuva argumente i lokalne promenljive metoda; sastoji se od osnovnih tipova podataka ili referenci na objekte.
- Operand stek: LIFO (last-in, first-out) struktura steka; služi za čuvanje operanada i međurezultata proračuna.
- Dinamičko povezivanje: povezuje okvir steka sa constant poolom klase kojoj metod pripada, podržavajući dinamičke pozive metoda.
- Adresa povratka metoda: beleži poziciju na koju tok upravljanja treba da se vrati nakon završetka metoda.
Okvir steka privatno je vlasništvo niti; svaka nit ima svoj JVM stek. Pri pozivu metoda novi okvir steka potiskuje se na vrh steka; po završetku metoda, okvir steka se skida sa steka.
Veličina tabele lokalnih promenljivih okvira steka i maksimalna dubina operand steka utvrđuju se već u vreme kompilacije. Kada prostora na steku ponestane, može nastati StackOverflowError. Razumevanje okvira steka od suštinske je važnosti za duboko razumevanje mehanizma rada Java programa.
