Lako razumevanje bytecode-a iz ugla javap-a
Računari su pomalo „glupi", prepoznaju samo 0 i 1, što znači da kod koji napišemo na kraju mora biti preveden u mašinski kod da bi ga računar izvršio. Java je već pri svom nastanku istakla veoma poznatu parolu: „jednom napiši, svuda izvršavaj".
Write Once, Run Anywhere.
Zbog te parole, Java-in tvorac, kompanija Sun, kao i drugi dobavljači virtuelnih mašina, objavili su mnoge Java virtuelne mašine koje rade na različitim platformama, a sve one imaju jednu zajedničku sposobnost — mogu učitati i izvršiti istu vrstu platformski nezavisnog bytecode-a (Byte Code).
(I ovome smo zapravo već ranije posvetili pažnju, ali radi celovitosti ovog teksta kratko ćemo proći; fokus ovog odeljka jeste da pomoću komande javap upoznamo bytecode)
Zahvaljujući Java virtuelnoj mašini, naš Java izvorni kod više ne mora da se za svaku platformu prevodi u odgovarajući mašinski kod — dovoljno je generisati jedan bytecode, a zatim taj bytecode fajl predati Java virtuelnim mašinama koje rade na različitim platformama da ga pročitaju i izvrše.
Današnje Java virtuelne mašine izuzetno su moćne: pored Java jezika podržavaju i mnoge druge programske jezike, poput Groovy, Scala, Kotlin itd.

Pogledajmo jedan kod.
public class Main {
private int age = 18;
public int getAge() {
return age;
}
}Nakon prevođenja i generisanja fajla Main.class, možemo u komandnoj liniji komandom xxd Main.class otvoriti class fajl (ovo smo već ranije obrađivali; oni koji ne znaju kako mogu pogledati unazad).

Za ove heksadecimalne vrednosti, osim zaglavlja cafe babe, ostatak se grubo može prevesti kao: šta je ovo bre......
Ali nakon krštenja u prethodnom odeljku o strukturi class fajla, verujem da vam je sadržina ovog fajla već dobro poznata.
javap
Java u sebi ugrađuje komandu za dekompilaciju, javap; komandom javap -help možete saznati osnovnu upotrebu javap-a.

Naravno, preduslov za izvršenje ove komande jeste da dobro konfigurišete Java okruženje; ukoliko ga niste, pogledajte ovaj članak.
javap je komandni alat koji dolazi uz JDK, namenjen prvenstveno dekompilaciji class fajlova (.class fajlova). Ja sam na svom macOS računaru koristio jenv za upravljanje verzijama JDK-a, pa je putanja koju vidim kao na slici ispod.
Korisnici Windowsa, kao i macOS korisnici koji ne koriste jenv, mogu na ovoj objavi saznati više o jenv-u — zaista je dobar.

javap uglavnom služi za dekompilaciju Java class fajlova, odnosno pretvara prevedeni .class fajl u oblik razumljiviji čoveku. Iako ne generiše originalni Java izvorni kod, može prikazati strukturu klase, uključujući konstruktore, metode, polja itd., čime nam pomaže da bolje razumemo Java bytecode i mehanizam rada Java programa.
Ranije smo već napisali jednostavnu klasu koju biste, nadam se, još pamtili:
public class Main {
private int age = 18;
public int getAge() {
return age;
}
}Naravno, nadam se da kod pišete u IntelliJ IDEA-i, a ne u Notepadu — time se štedi postupak ručnog prevođenja, jer class fajl možete pronaći direktno u target direktorijumu; sve ovo smo već ranije obradili.
OK, u direktorijumu u kojem se nalazi class fajl unesimo komandu javap -v -p Main.class kako bismo pregledali izlaz (-v prikazuje dodatne podatke, kao što su tabela lokalnih promenljivih, opcodovi itd.; -p prikazuje sve klase i članove, uključujući privatne — oni koji ne razumeju mogu se vratiti i ponovo pogledati izlaz javap -help 😁).
Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.class
Last modified 15. april 2021.; size 385 bytes
SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c6
Compiled from "Main.java"
public class com.itwanger.jvm.Main
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // com/itwanger/jvm/Main
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:I
#3 = Class #20 // com/itwanger/jvm/Main
#4 = Class #21 // java/lang/Object
#5 = Utf8 age
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/itwanger/jvm/Main;
#14 = Utf8 getAge
#15 = Utf8 ()I
#16 = Utf8 SourceFile
#17 = Utf8 Main.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #5:#6 // age:I
#20 = Utf8 com/itwanger/jvm/Main
#21 = Utf8 java/lang/Object
{
private int age;
descriptor: I
flags: (0x0002) ACC_PRIVATE
public com.itwanger.jvm.Main();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 18
7: putfield #2 // Field age:I
10: return
LineNumberTable:
line 6: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/itwanger/jvm/Main;
public int getAge();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field age:I
4: ireturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/itwanger/jvm/Main;
}
SourceFile: "Main.java"Otvorimo oči dobro — sadržaja ima prilično. Ne žurite, analiziraćemo red po red.
Osnovne informacije bytecode-a
Red 1:
Classfile /Users/maweiqing/Documents/GitHub/TechSisterLearnJava/codes/TechSister/target/classes/com/itwanger/jvm/Main.classKao što ime kaže, ovaj red označava putanju bytecode fajla.
Red 2:
Last modified 15. april 2021.; size 385 bytesDatum izmene bytecode fajla (zajednički sam ga delimio u javnoj poruci 2021. godine; ne znam koliko ga čitalaca još pamti 😄), a veličina fajla je 385 bajtova.
Red 3:
SHA-256 checksum 6688843e4f70ae8d83040dc7c8e2dd3694bf10ba7c518a6ea9b88b318a8967c6SHA-256 vrednost bytecode fajla, koristi se za proveru integriteta fajla.
SHA-256 je algoritam heširanja koji proizvoljno dugačak ulazni podatak prevodi u izlazni podatak fiksne dužine (256 bita, odnosno 32 bajta), pri čemu se iz heš-vrednosti matematički veoma teško dobiti originalni podatak, pa se često koristi za proveru integriteta podataka.
Red 4:
Compiled from "Main.java"Označava da je bytecode fajl preveden iz izvornog fajla Main.java.
Red 5:
public class com.itwanger.jvm.MainModifikator pristupa i tip klase — ukazuje na to da je u pitanju javna klasa imena com.itwanger.jvm.Main.
Red 6 minor version: 0, sporedna verzija.
Red 7 major version: 55, glavna verzija (preveo Java 11; obrađivali smo u prethodnom odeljku).
Red 8:
flags: (0x0021) ACC_PUBLIC, ACC_SUPERAccess flags klase; ukupno ih ima 8, što smo već spominjali u prethodnom odeljku.

Ukazuje na to da je tekuća klasa ACC_PUBLIC | ACC_SUPER (klasa je public i koristi ključnu reč super).
Bitni operator | znači: ako na odgovarajućem mestu stoji 0, rezultat je 0, inače je 1, pa je 0x0001 | 0x0020 rezultat 0x0021 (potrebno je preći u binarni oblik da bi se izračunalo).
Red 9:
this_class: #3 // com/itwanger/jvm/MainIndeks tekuće klase; upućuje na konstantu sa indeksom 3 u constant pool-u (objašnjeno u prethodnom odeljku) — vidi se da je tekuća klasa Main.
Red 10:
super_class: #4 // java/lang/ObjectIndeks roditeljske klase; upućuje na konstantu sa indeksom 4 u constant pool-u — vidi se da je roditeljska klasa tekuće klase Object (klase bez izričitog roditelja podrazumevano nasleđuju tu superklasu, što je važan razlog za načelo sve je objekat).
Red 11:
interfaces: 0, fields: 1, methods: 2, attributes: 1Tekuća klasa ima 0 interfejsa, 1 polje (age), 2 metoda (metod write() i podrazumevani konstruktor, o čemu smo govorili u poglavlju „Objektno orijentisano programiranje") i 1 atribut (jedini atribut te klase je SourceFile, koji sadrži podatke o izvornom fajlu — o tome je već bilo reči u prvom redu).
Constant pool
Sledeći je Constant pool — constant pool, najvažniji deo bytecode fajla. Constant pool možemo shvatiti kao skladište resursa u bytecode fajlu; uglavnom čuva dve velike kategorije podataka.
U prethodnom odeljku već smo govorili o literalima i simboličkim referencama; ovde ćemo ponovo — treći put — jer je zaista teže razumljivo, pa ćemo ga obraditi više puta, sve dok ga svi ne razumeju (😁).
1) Literal (Literal), pomalo slično pojmu konstante u Javi, npr. tekstualni string, final konstanta itd.
2) Simbolička referenca (Symbolic References), pojam iz oblasti kompilatorskih teorija, obuhvata tri vrste:
- Potpuno kvalifikovano ime (Fully Qualified Name) klase i interfejsa
- Ime polja i deskriptor (Descriptor)
- Ime metode i deskriptor
Java virtuelna mašina dinamičko povezivanje obavlja tek pri učitavanju bytecode fajla, odnosno simboličke reference polja i metode dobijaju pravu memorijsku adresu tek nakon razrešavanja tokom rada.
Kada Java virtuelna mašina radi, iz constant pool-a uzima odgovarajuću simboličku referencu, a zatim je pri kreiranju klase ili tokom rada razrešava i prevodi na konkretnu memorijsku adresu.
U tekućem bytecode fajlu ukupno je 21 konstanti; međusobno su povezane, pa bi njihova analiza jedna po jedna bila konfuzna, pa ćemo ih pratiti metodom „prateći lozu": odozgo nadole; one konstante koje su referencirane samo ćemo kratko okrzati.
Napomena:
- Iza znaka
#stoji indeks; indeksiranje kreće od 1, a ne od 0, jer su autori smatrali da „kada želimo da izrazimo da ne referenciramo nijednu konstantu, indeks možemo postaviti na 0" (opisano u knjizi „Duboko razumevanje Java virtuelne mašine" profesora Zhou Zhiming-a). - Iza znaka
=stoji tip konstante, bez prefiksaCONSTANT_i sufiksa_info. - Indeks i podudarajući pojam podložni su u tekstu — radi fleksibilnog opisa nisu ujednačeni.
Hajde da krenemo.
- konstanta:
#1 = Methodref #4.#18 // java/lang/Object."<init>":()VTip Methodref, označava definiciju metode; upućuje na konstante sa indeksima 4 i 18 u constant pool-u.
- konstanta:
#4 = Class #21 // java/lang/ObjectTip Class, označava definiciju klase (ili interfejsa); upućuje na konstantu sa indeksom 21 u constant pool-u.
- konstanta:
#21 = Utf8 java/lang/ObjectTip Utf8, string u UTF-8 kodiranju, vrednost java/lang/Object.
- konstanta:
#18 = NameAndType #7:#8 // "<init>":()VTip NameAndType, označava delimičnu simboličku referencu polja ili metode; upućuje na konstante sa indeksima 7 i 8 u constant pool-u.
- konstanta:
#7 = Utf8 <init>Tip Utf8, string u UTF-8 kodiranju, vrednost <init>, ukazuje na konstruktor.
- konstanta:
#8 = Utf8 ()VTip Utf8, string u UTF-8 kodiranju, vrednost ()V, ukazuje na to da metod nema povratnu vrednost (void).
Time je 1. konstanta potpuno raspletena. Značenje spojenog je da klasa Main koristi podrazumevani konstruktor koji vodi poreklo iz klase Object. #4 upućuje na Class #21 (odnosno java/lang/Object), a #18 upućuje na NameAndType #7:#8 (odnosno <init>:()V).
- konstanta:
#2 = Fieldref #3.#19 // com/itwanger/jvm/Main.age:ITip Fieldref, označava definiciju polja; upućuje na konstante sa indeksima 3 i 19 u constant pool-u.
- konstanta:
#3 = Class #20 // com/itwanger/jvm/MainTip Class, označava definiciju klase (ili interfejsa); upućuje na konstantu sa indeksom 20 u constant pool-u.
- konstanta:
#19 = NameAndType #5:#6 // age:ITip NameAndType, označava delimičnu simboličku referencu polja ili metode; upućuje na konstante sa indeksima 5 i 6 u constant pool-u.
- konstanta:
#5 = Utf8 ageTip Utf8, string u UTF-8 kodiranju, vrednost age, ukazuje na to da je ime polja age.
- konstanta:
#6 = Utf8 ITip Utf8, string u UTF-8 kodiranju, vrednost I, ukazuje na to da je tip polja int.
Mapiranje deskriptora tipova polja prikazano je na slici; u prethodnom odeljku smo to već obradili, ali iz ugla heksadecimalnog zapisa, dok ga ovde posmatramo iz ugla javap-a.

Time je i 2. konstanta raspletena. Značenje spojenog jeste: deklarisano je polje age tipa int. #3 upućuje na Class #20 (odnosno com/itwanger/jvm/Main), a #19 upućuje na NameAndType #5:#6 (odnosno age:I).
Skup tabele polja
Tabela polja opisuje promenljive deklarisane u interfejsu ili klasi, uključujući promenljive klase i članske promenljive, ali ne obuhvata lokalne promenljive deklarisane unutar metoda.
Sve sa linkom ranije smo obrađivali — da li primećujete kako se sve znanje međusobno povezuje? Upravo zato učimo javap i bytecode: uporedo sa upoznavanjem bytecode-a, produbljujemo i razumevanje samog Java znanja.
Modifikatori polja obično su:
- Modifikator pristupa, npr. public, private, protected
- Modifikator statičke promenljive, npr. static
- final modifikator
- Modifikator vidljivosti među nitima, npr. volatile
- Modifikator serializacije, npr. transient
Zatim tip polja (to može biti osnovni tip podataka, niz ili objekat) i ime.
U bytecode fajlu Main.class informacija tabele polja izgleda ovako.
private int age;
descriptor: I
flags: (0x0002) ACC_PRIVATEOznačava da je modifikator pristupa polja private, da je tip int i da je ime age. Access flags polja veoma su slični access flags klase.

Skup tabele metoda
Tabela metoda opisuje metode deklarisane u interfejsu ili klasi, uključujući metode klase, metode članove i konstruktore. Modifikatori metode se donekle razlikuju od modifikatora polja — tako se volatile i transient ne mogu primenjivati na metode, a metode imaju i dodatne modifikatore poput synchronized, native, strictfp i abstract.

Konstruktor
Ovaj deo je konstruktor; povratni tip je void, access flag je public.
public com.itwanger.jvm.Main();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC- Deklaracija:
public com.itwanger.jvm.Main();— to je konstruktor klase Main, služi za kreiranje instance klase Main; javan je (public). - Deskriptor:
descriptor: ()VOvo označava da konstruktor nema argumenata (()) i nema povratnu vrednost (V, označavavoid). - Access flag:
flags: (0x0001) ACC_PUBLIC, što znači da je ovaj konstruktor javan i da mu se može pristupiti iz drugih klasa.
Pogledajmo detaljnije njegov Code atribut.
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 18
7: putfield #2 // Field age:I
10: return
LineNumberTable:
line 6: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/itwanger/jvm/Main;①, stack je najveći operand stek; Java virtuelna mašina prilikom rada na osnovu te vrednosti alocira dubinu operand steka okvira steka (o operand steku i okviru steka detaljno ćemo govoriti u sledećem odeljku). Ovde je vrednost 2, što znači da je dubina operand steka 2.
Operand stek je LIFO (last-in, first-out) stek koji služi za čuvanje privremenih promenljivih i međurezultata. U konstruktoru instrukcije bipush i aload_0 mogu istovremeno zahtevati prostor na steku, pa su potrebne 2 dubine operand steka.
②, locals je prostor za skladištenje lokalnih promenljivih, u jedinicama slota (slot); argumenti metode i lokalne promenljive unutar metode čuvaju se u tabeli lokalnih promenljivih.
Kapacitet tabele lokalnih promenljivih kao najmanju jedinicu koristi varijabilni slot; jedan slot može primiti tip podataka unutar 32 bita, poput boolean, bajt, char, short, int, float, reference i returnAddress.
Potrebna veličina tabele lokalnih promenljivih izračunava se tokom prevođenja i zavisi od kompilatora, te bytecode različitih kompilatora može biti različit.
locals=1 znači da u tabeli lokalnih promenljivih ima prostora za 1 promenljivu. Za metode instance (poput konstruktora) prvo mesto u tabeli lokalnih promenljivih (indeks 0) uvek služi za čuvanje reference this.
③, args_size je broj argumenata metode.
Zašto je vrednost stack 2, locals 1, args_size 1? Pa podrazumevani konstruktor ipak nema argumenata i lokalnih promenljivih?
To je zato što postoji skrivena promenljiva this; sve dok metod nije statički, uvek postoji objekat tekuće klase, this, koji se krišom krije iza uvida.
To objašnjava zašto su locals i args_size jednaki 1.
A zašto je stack 2? Zato što instrukcija bytecode-a invokespecial (koja poziva konstruktor roditeljske klase radi inicijalizacije) troši jednu referencu na tekuću klasu, pa se aload_0 izvršava 2 puta, što znači da je veličina operand steka 2.
O instrukcijama bytecode-a govorićemo detaljnije kasnije; ovde ih samo nakratko pominjemo.
④, LineNumberTable; uloga ovog atributa jeste da opiše odgovarajuću vezu između brojeva linija izvornog koda i brojeva linija bytecode-a (ofseta bytecode-a). To je za debugovanje od izuzetnog značaja jer omogućava debuggeru da instrukciju bytecode-a koja se izvršava precizno poveže sa određenim retkom izvornog koda.
LineNumberTable:
line 6: 0
line 7: 4Značenje je: 6. redu odgovara red bytecode-a 0, a 7. redu odgovara red bytecode-a 4.
Tokom debugovanja, kada se aktivira breakpoint ili nastane izuzetak, pomoću LineNumberTable možemo saznati koja linija izvornog koda je dovela do toga.
④, LocalVariableTable; uloga ovog atributa jeste da opiše odnos između lokalnih promenljivih u okviru steka i promenljivih definisanih u izvornom kodu. Pažljivo pogledajte i uočićete prisustvo this.
- Start i Length: definišu obim važenja promenljive u metodi. Start je ofset bytecode-a od kog promenljiva počinje da važi, a Length je dužina tokom koje ostaje aktivna.
- Slot: indeks promenljive u nizu lokalnih promenljivih.
- Name: ime promenljive, onako kako je definisano u izvornom kodu.
- Signature: deskriptor tipa promenljive.
Ovde postoji samo jedna lokalna promenljiva, this, koja označava objekat koji konstruktor trenutno inicijalizuje. Njen obim važenja počinje od ofseta 0 i proteže se kroz celu dužinu metode (dužina 11), a dodeljena je u prvi slot tabele lokalnih promenljivih (indeks 0). Lcom/itwanger/jvm/Main; ukazuje na to da je tip ove promenljive com.itwanger.jvm.Main.
Metod član
Ovaj deo je metod član getAge(); povratni tip int, access flag public.
public int getAge();
descriptor: ()I
flags: (0x0001) ACC_PUBLICKada se razume Code atribut konstruktora, Code atribut metoda getAge() lako se razume.
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field age:I
4: ireturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/itwanger/jvm/Main;Najveći operand stek je 1, potreban prostor za lokalne promenljive 1, broj argumenata metode 1 — to je zato što je jedina lokalna promenljiva skriveni this, a u instrukcijama bytecode-a aload_0 se izvršava samo jednom.
①, instrukcije bytecode-a
- aload_0: učitava referencu this na vrh steka, kako bi se pristupilo polju instance age.
getfield #2: uzima vrednost polja. Ova instrukcija čita vrednost polja age objekta this i gura je na vrh steka.#2je referenca na polje u constant pool-u.- ireturn: vraća celu vrednost sa vrha steka. Ovde vraća vrednost polja age.
②, dodatni podaci
LineNumberTable i LocalVariableTable takođe pružaju informacije o rednim brojevima izvornog koda i lokalnim promenljivama, što pomaže pri debugovanju i razumevanju toka izvršenja koda.
Kratak pregled
Učenje može ići u širinu i u dubinu. Kada tek počinjemo da programiramo, želimo da naučimo što više — to je širenje u širinu; kada steknemo izvesno programersko iskustvo i želimo korak više, potrebno je širenje u dubinu, neprestano i temeljno učenje, sve dok ne izgradimo sopstveni sistem znanja.
Bilo da posmatramo iz ugla heksadecimalnog bytecode-a, grafičkog pregleda dekompilirani podataka kroz jclasslib, ili — kao danas — iz ugla izlaza javap dekompilacije, uvek se može iščitatati nešto novo!
Početnicima bytecode isprva deluje zbunjujuće; nije problem — i meni je bilo tako. Vremenom, uz nakupljanje iskustva, postepeno postaje lakše; što dublje ulazite, sve više osećate onaj „tehnologiju imam, svetom vladam"~
