Kako zapravo JVM izvršava Java kod? Sa 3000 reči i 10 ručno nacrtanih ilustracija temeljno savladavamo to.
„Erge, pošto sam pogledala kod Hello World, zaista me zanima kako se u Run panelu IDE-ja ispisalo ‘Sanmej, manje gledaj u telefon, manje igraj igrice, uči lepo, lepa budi’?” — rekla mi je Sanmej posle jednog gutljaja mlečnog čaja od žitarica i kakaa.
„Sanmej, proces izvršavanja Java koda obično delimo na fazu kompajliranja i fazu izvršavanja; kad razjasnimo te dve faze, znaćemo i razlog”, rekao sam joj sa osmehom. „Za Java programera koji je toliko dugo pisao kod, vreme je da shvati kako se njegov Java kod zapravo izvršava. To pitanje se često postavlja i na intervjuima.”
Hajde da pogledamo zajedno.
Faza kompajliranja
Nalepnimo ovaj HelloWorld kod:
/**
* @author Erge
*/
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Sanmej, manje gledaj u telefon, manje igraj igrice, uči lepo, lepa budi.");
}
}Klikni na ikonicu čekića u toolbaru IDE-ja (Build Project, kompajlira ceo projekat; obično nije potrebno ručno kompajlirati, jer IDEA to radi automatski), kao na slici ispod.

Tada se u direktorijumu target, koji se nalazi uz src, može pronaći fajl pod imenom HelloWorld.class.

Ako ga ne vidiš, desni klik na direktorijum i izaberi „Reload from Disk, ponovo učitaj sa diska”, kao na slici ispod:

Možeš ga otvoriti dvoklikom i videti sledeći sadržaj.
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.itwanger.five;
public class HelloWorld {
public HelloWorld() {
}
public static void main(String[] args) {
System.out.println("Sanmej, manje gledaj u telefon, manje igraj igrice, uči lepo, lepa budi.");
}
}IDEA podrazumevano koristi alatku Fernflower za dekompajliranje fajla bajtkoda (fajl sa nastavkom .class, odnosno fajl dobijen kompajliranjem Java izvornog koda) nazad u Java izvorni kod razumljiv nama.
Ali fajl bajtkoda zapravo ne izgleda ovako — on sadrži instrukcije koje JVM izvršava, kao i metapodatke klase, poput imena klase, metoda i atributa. Ako fajl bajtkoda otvorimo preko „show bytecode”, izgleda ovako:
// class version 58.0 (58)
// access flags 0x21
public class com/itwanger/five/HelloWorld {
// compiled from: HelloWorld.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 6 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/itwanger/five/HelloWorld; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 8 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "\u4e09\u59b9\uff0c\u5c11\u770b\u624b\u673a\u5c11\u6253\u6e38\u620f\uff0c\u597d\u597d\u5b66\uff0c\u7f8e\u7f8e\u54d2\u3002"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 9 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}Zar nije malo teže za razumevanje? I mene je prvi put kad sam video ovaj sadržaj uhvatila jeza, ali ne brini — kasnije ćemo zajedno ući u dubinu; ovde samo unapred oseti šarm bajtkoda.
Kako pregledati bajtkod?
Možeš preko stavki menija „View” → „Show Bytecode” u IDE-ji, kao na slici ispod.

Ovaj bajtkod može se doslovno prevesti kao „bytecode”.
Bajtkod nije mašinski kod; operativni sistem ga ne može direktno prepoznati — potrebno je instalirati odgovarajuću verziju JVM-a na operativni sistem da bi ga prepoznao.
Obično je dovoljno instalirati odgovarajuću verziju JDK-a (Java Development Kit, Java razvojni kit), koji sadrži JRE (Java Runtime Environment, Java okruženje za izvršavanje), a JRE opet sadrži JVM.

Operativni sistemi Windows, Linux, MacOS i drugi imaju svoje JDK verzije; dovoljno je instalirati JDK i time se dobija Java okruženje za izvršavanje, pa se Java izvorni kod može kompajlirati u bajtkod, a zatim se taj bajtkod može izvršavati na različitim operativnim sistemima.
build once, run anywhere.
jclasslib
Ovde preporučujem dodatak jclasslib za pregled fajlova bajtkoda, koji se može instalirati iz IDEA prodavnice dodataka.

Nakon instalacije, klikni na View -> Show Bytecode With jclasslib i možeš pregledati fajl bajtkoda (pre klika kursor mora biti na odgovarajućem fajlu klase), kao na slici ispod.

Sa jclasslib-om možeš ne samo vizuelno pregledati bajtkod klase, već i osnovne informacije o klasi, konstantni bazen, interfejse, polja, metode itd., kao na slici ispod; detaljno ćemo obraditi i to kasnije.

Dakle, u fazi kompajliranja Java prevodi izvorni kod u fajl bajtkoda.

Ako fajl bajtkoda otvorimo heksadecimalnim editorom (biće reči o tome u sledećem poglavlju), sadržaj izgleda ovako (u suštini je binarni niz 0 i 1); heksadecimalni prikaz je lakši za razumevanje (iako je i njega teško pročitati golim okom).
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 4d61 696e 2e6a 6176 610c
0007 0008 0c00 0500 0601 0010 636f 6d2f
7268 7974 686d 372f 4d61 696e 0100 106a
6176 612f 6c616e 672f4f 626a 6563 7400
2100 0300 0400 0000 0100 0200 0500 0600
0000 0200 0100 0700 0800 0100 0900 0000
1d00 0100 0100 0000 052a b700 01b1 0000
0001 000a 0000 0006 0001 0000 0003 0001
000b 000c 0001 0009 0000 001f 0002 0001
0000 0007 2ab4 0002 0460 ac00 0000 0100
0a00 0000 0600 0100 0000 0800 0100 0d00
0000 0200 0eFaza izvršavanja
Kada imamo .class fajl, odnosno fajl bajtkoda, moramo pokrenuti JVM da izvrši taj fajl bajtkoda — to je run faza; pre toga je bila build faza.
JVM će najpre preko učitača klasa učitati fajl bajtkoda, zatim učitati bajtkod u runtime podatkovne oblasti JVM-a, a onda kroz izvršni motor pretvoriti ga u mašinski kod i na kraju predati operativnom sistemu na izvršavanje.

Pogledajmo sekvencu instrukcija bajtkoda za HelloWorld pomoću javap (koji ćemo detaljno obraditi kasnije).
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <Hello World>
5 invokevirtual #4 <java/io/PrintStream.println>
8 returnSekvenca instrukcija bajtkoda obično se sastoji od više instrukcija; svaka instrukcija čini jedan opkod i nekoliko operanada.
- Opkod: instrukcija veličine jednog bajta, koja predstavlja konkretnu operaciju.
- Operand: prati opkod i pruža dodatne informacije.
Ova sekvenca bajtkoda poziva metodu System.out.println da ispiše string „Hello World”. Detaljno objašnjenje:
①,0: getstatic #2 <java/lang/System.out>:
- Opkod: getstatic
- Operand: #2
- Opis: ova instrukcija služi da dobavi statičko polje; ovde dobavlja statičko polje
outklasejava.lang.System, što je izlazni tok tipaPrintStream.#2je indeks koji ukazuje u konstantni bazen; o tome će biti reči kasnije, pri obradi strukture fajla klasa.
②,3: ldc #3 <Hello World>:
- Opkod: ldc
- Operand: #3
- Opis: ova instrukcija iz konstantnog bazena učitava konstantnu vrednost (string „Hello World”) na vrh steka operanada.
#3je indeks koji ukazuje u konstantni bazen, u kojem je smešta referenca na string „Hello World”.
③,5: invokevirtual #4 <java/io/PrintStream.println>:
- Opkod: invokevirtual
- Operand: #4
- Opis: ova instrukcija poziva metodu; ovde poziva metod
printlnklasePrintStreamradi ispisa stringa.#4je indeks u konstantni bazen, u kojem je smeštena referentna informacija o metodijava/io/PrintStream.println.
④,8: return:
- Opkod: return
- Opis: ova instrukcija služi da se vrati iz trenutne metode.
Gore navedeni getstatic, ldc, invokevirtual, return i ostalo su opkodovi instrukcija bajtkoda.
Možeš koristiti hexdump, alatku uobičajenu na Unix i Linux sistemima koja prikazuje sadržaj fajla u heksadecimalnom obliku, da pogledaš binarni sadržaj bajtkoda.
b2 00 02 12 03 b6 00 04 b1Napomena: ovo je heksadecimalni prikaz binarnog fajla, odnosno hex; analiza binarnih fajlova obično se radi preko hex-a. Veza između instrukcija bajtkoda i binarnog oblika, sa odgovarajućom semantikom, data je u nastavku:
0xb2 getstatic dobavlja vrednost statičkog polja
0x12 ldc konstantu iz konstantnog bazena gura na stek
0xb6 invokevirtual u toku izvršavanja povezuje i poziva metod
0xb1 return povratak iz void metodaJVM upravo dovršava izvršavanje programa parse-om ovih instrukcija bajtkoda. Postoje dva uobičajena načina izvršavanja: jedan je interpretirano izvršavanje, koje instrukcije bajtkoda interpretira i izvršava jednu po jednu; drugi je JIT, odnosno just-in-time kompilacija, koja u toku izvršavanja vrući kod optimizuje i kešira, pa se pri sledećem izvršavanju direktno koristi keširani mašinski kod, bez ponovnog interpretiranja.

Na taj način se povećava efikasnost izvršavanja programa.
Napomena: kada učitač klasa završi učitavanje bajtkoda, JVM je izdvojio posebne memorijske oblasti za smeštanje tih podataka bajtkoda i runtime međupodataka.

Od toga PC registar, virtuelni mašinski stek i stek native metoda pripadaju privatnom delu niti, dok heap i metaspace (implementacija oblasti metoda) pripadaju oblasti deljenih podataka, pa te podatke u memoriji različite niti dele.
Ako trenutna nit virtuelne mašine izvršava Java uobičajenu metodu, u PC registru je smeštena prva instrukcija metode; kada metod počne sa izvršavanjem, u PC registru se nalazi adresa sledeće instrukcije bajtkoda.
Ako trenutna nit virtuelne mašine izvršava native metod, vrednost u PC registru je undefined.
Ako se naiđe na grane, petlje ili izuzetke — različite kontrole toka — PC registar se postavlja na adresu ciljne instrukcije bajtkoda.
Pored toga, pri promeni konteksta između niti, virtuelna mašina pamti PC registar tekuće niti; kada se nit vrati, prethodno zabeležena vrednost se obnavlja u PC registar, čime se nastavlja izvršavanje narednih instrukcija bajtkoda.
Osim PC registra, u toku izvršavanja instrukcija bajtkoda učestvuje i virtuelni mašinski stek. Pogledajmo najpre grubu strukturu virtuelnog mašinskog steka, kao na slici ispod.

Osnovni element kojim operiše virtuelni mašinski stek jeste okvir steka (stack frame); okvir steka uglavnom sadrži tabelu lokalnih promenljivih, stek operanada, dinamičko povezivanje i adresu povratka metode. Okvir steka jeste struktura podataka principa „prvi ušao, poslednji izašao”; svakom metodu, od poziva do završetka izvršavanja, odgovara po jedan okvir steka koji se gura i skida sa virtuelnog mašinskog steka.
Kada smo savladali strukturu virtuelnog mašinskog steka, hajde da vidimo kako izgleda tok izvršavanja metoda.
Uzmimo za primer sledeći kod: klasa Test, u metodi main kreira se novi objekat User, pa se age tog objekta prosleđuje statičkom metodu calculate radi jednostavne sabiračke operacije, a rezultat se na kraju ispisuje na konzolu.
public class Test {
public static void main(String[] args) {
User user = new User();
Integer result = calculate(user.getAge());
System.out.println(result);
}
private static Integer calculate(Integer age) {
Integer data = age + 3;
return data;
}
}Nakon što JVM završi učitavanje
.classfajla, kreira nit pod imenom „main”, koja automatski poziva statički metod pod imenom „main” — to je ulazna tačka Java programa;Kada nit main izvršava metod main, JVM će na virtuelni mašinski stek gurnuti okvir steka koji odgovara metodi main;

Stek operanada u okviru steka čuva podatke nad kojima se radi; kada JVM izvršava instrukcije bajtkoda, uzima podatke sa steka operanada, a nakon izvršenja računske operacije rezultat ponovo gura na stek operanada;
Pri pozivu metode calculate, na virtuelni mašinski stek se nastavlja gurati okvir steka koji odgovara metodi calculate.

- Za instrukciju sabiranja
age + 3, pre nego što se izvrši, JVM će skinuti prva dva elementa sa vrha steka operanada, sabrati ih i rezultat gurnuti na stek operanada.
U ovom primeru opkod instrukcije je „add”, što označava operaciju sabiranja; operand 0 označava da se prvi operand uzima sa vrha steka operanada; operand 1 označava da se drugi operand uzima sa drugog mesta od vrha steka operanada.
U PC registru smeštena je adresa sledeće instrukcije bajtkoda koja treba da se izvrši.
Kada se izvršavanje metode calculate završi, njoj odgovarajući okvir steka se skida sa virtuelnog mašinskog steka; rezultat izvršavanja metode gura se na stek operanada u okviru steka main, a adresa povratka metode resetuje se u PC registar niti main, kako bi motor za izvršavanje bajtkoda nakon toga iz PC registra dobio adresu sledeće instrukcije.
Ako metod nema povratnu vrednost, JVM će na stek operanada okvira steka koji je pozvao metod gurnuti null kao rezervisano mesto, čime se obnavlja stanje steka operanada pozivaoca.
- Interpretator u motoru za izvršavanje iz brojača programa uzima adresu sledeće instrukcije bajtkoda, odnosno odgovarajuću instrukciju bajtkoda u metaspace-u; pošto je dobavi, interpretator je interpretira u odgovarajuću mašinsku instrukciju, koju CPU na kraju i izvršava.
Kratak pregled
„Dobro, Sanmej, za danas ćemo stati ovde; hajde da ukratko sumiramo”, rekao sam uz dah nakon izdahnuća.
Java kod se prvo kompajlerom prevodi u bajtkod, a zatim izvršava na JVM-u. U toku izvršavanja JVM pretvara bajtkod u mašinski kod interpretiranim izvršavanjem ili just-in-time kompilacijom (JIT). Interpretirano izvršavanje direktno radi bajtkod, dok JIT u toku izvršavanja vrući kod kompiluje i optimizuje u mašinski kod kako bi poboljšao performanse.
Pri tome su potrebne runtime podatkovne oblasti koje čuvaju podatke bajtkoda i runtime međupodatke.
Bajtkod je u JVM-u veoma ključan sadržaj; obuhvata mehanizam učitavanja klasa, strukturu fajla bajtkoda, tok izvršavanja instrukcija bajtkoda i slično — o svemu tome ćemo detaljno govoriti kasnije.
Referenca: Kako JVM izvršava Java programe, autor Mengjao Jishu, vrlo dobro napisano.
