Jedan tekst kojim temeljno savladavaš mehanizam učitavanja klasa u Javi
U prethodnom poglavlju, gde smo objašnjavali kako JVM izvršava Java kod, pomenuo sam da JVM mora da učita kompajlirani fajl bajtkoda u svoju runtime podatkovnu oblast kako bi ga izvršio. Ovaj proces obuhvata mehanizam učitavanja klasa u Javi (znanje koje se često pita na intervjuima), pa ćemo ga detaljno obraditi.

Bajtkod smo već spomenuli u prethodnom poglavlju; usko je povezan sa mehanizmom učitavanja klasa, pa verujem da ti je još uvek u sećanju.
Ovde ću ti pokazati i jedan mali trik: bajtkod fajl možeš pregledati komandom xxd. Pogledaj najpre sledeći kod.
public class Test {
public static void main(String[] args) {
System.out.println("Chenmo Wang Er");
}
}Nakon što se kod uspešno kompajlira, u komandnoj liniji izvrši xxd Test.class (korisnici macOS-a mogu pokrenuti komandu direktno, dok Windows korisnici mogu preko ove veze naći zamenu) i tako brzo videti heksadecimalni sadržaj bajtkoda.
xxd je alatka koja u terminalu kreira heksadecimalni dump (hex dump) ili pretvara heksadecimalni prikaz nazad u binarni. Više informacija možeš naći na Vikipediji.
00000000: cafe babe 0000 0034 0022 0700 0201 0019 .......4."......
00000010: 636f 6d2f 636d 6f77 6572 2f6a 6176 615f com/cmower/java_
00000020: 6465 6d6f 2f54 6573 7407 0004 0100 106a demo/Test......j
00000030: 6176 612f 6c616e 672f4f 626a 6563 7401 ava/lang/Object.
00000040: 0006 3c69 6e69 743e 0100 0328 2956 0100 ..<init>...()V..
00000050: 0443 6f64 650a 0003 0009 0c00 0500 0601 .Code...........
00000060: 000f 4c69 6e65 4e75 6d62 6572 5461 626c ..LineNumberTablOvde ću reći samo jedno: cafe babe u ovom bajtkodu se naziva „magični broj“ i predstavlja oznaku po kojoj JVM prepoznaje .class fajl (fajl bajtkoda). Svima je poznato da je logo Jave šoljica kafe iz koje se diže para — vidiš kako se to povezuje?

Kreator formata fajla slobodno bira magični broj (samo da već nije iskorišćen); na primer, magični broj
.pngfajla je8950 4e47.
Što se tiče ostatka sadržaja u bajtkod fajlu, time se za sada nećemo baviti — detaljno ćemo ga obraditi kasnije.
Proces učitavanja klasa
Sada kada znamo šta je Java bajtkod, razgovarajmo o procesu učitavanja klasa u Javi.

Od trenutka kada se klasa učita u JVM pa do istovara iz memorije, njen životni ciklus prolazi kroz sedam faza: učitavanje, provera, priprema, razrešavanje, inicijalizacija, korišćenje i istovar. Od toga se faze provera, priprema i razrešavanje zajednički nazivaju povezivanje.
Ako izuzmemo korišćenje i istovar, ostaje proces učitavanja klasa u Javi. Ovih 5 faza se najčešće odvija redom, ali u slučaju dinamičkog povezivanja faza razrešavanja nastupa posle faze inicijalizacije (objasnićemo to ubrzo).
1) Loading (učitavanje)
Cilj JVM-a u ovoj fazi jeste da bajtkod iz različitih izvora podataka (to može biti .class fajl, .jar paket, pa čak i mreža) pretvori u binarni tok bajtova, učita ga u memoriju i generiše objekat java.lang.Class koji predstavlja tu klasu (o tome smo govorili pri učenju refleksije).
2) Verification (provera)
U ovoj fazi JVM proverava binarni tok bajtova; samo ono što ispunjava specifikaciju JVM bajtkoda može biti ispravno izvršeno od strane JVM-a. Ova faza je važna barijera koja garantuje bezbednost JVM-a; evo glavnih provera.
Predlog izmene koji je 22. februara 2025. dostavio čitalac Fei.
- Osigurava da format binarnog toka bajtova odgovara očekivanom (npr. da počinje sa
cafe babe, što smo već spomenuli). - Da li sve metode poštuju ograničenja ključnih reči za kontrolu pristupa —
protected,privatei slično. - Da li su broj i tip argumenata poziva metode ispravni.
- Osigurava da su promenljive pravilno inicijalizovane pre upotrebe.
- Proverava da li su promenljivama dodeljene vrednosti odgovarajućeg tipa.
- I još mnogo toga.
3) Preparation (priprema)
U ovoj fazi JVM klasnim promenljivama (tzv. statičkim promenljivama, onima izmenjenim ključnom reči static) dodeljuje memoriju i inicijalizuje ih na podrazumevanu početnu vrednost odgovarajućeg tipa podataka, npr. 0, 0L, null, false itd.
Drugim rečima, ako imamo sledeći kod:
public String chenmo = "Chenmo";
public static String wanger = "Wang Er";
public static final String cmower = "Chenmo Wang Er";chenmo neće dobiti memoriju, dok će wanger dobiti; ali početna vrednost promenljive wanger neće biti „Wang Er“ već null.
Treba imati na umu da se promenljive izmenjene sa static final nazivaju konstantama i razlikuju se od klasnih promenljivih (o tome smo već govorili kod ključne reči static). Konstanta, jednom kada joj se dodeli vrednost, više se ne menja, pa vrednost cmower u fazi pripreme iznosi „Chenmo Wang Er“, a ne null.
4) Resolution (razrešavanje)
Ova faza u konstantnom bazenu pretvara simboličke reference u direktne reference.
Šta? Simboličke reference, direktne reference?
Simbolička referenca opisuje cilj reference pomoću skupa simbola (bilo kakvih literala, važno je samo da pri upotrebi nedvosmisleno lociraju cilj).
U trenutku kompajliranja Java klasa ne zna stvarnu adresu klase koju referencira, pa je može zameniti samo simboličkom referencom. Na primer, klasa com.Wanger referencira klasu com.Chenmo; prilikom kompajliranja klasa Wanger ne zna stvarnu memorijsku adresu klase Chenmo, pa mora koristiti simbol com.Chenmo.
Direktna referenca nastaje razrešavanjem simboličke reference, čime se nalazi stvarna memorijska adresa na koju se referencira. Uporedimo ih još jednom.
Simbolička referenca
- Definicija: sadrži potpuno kvalifikovano ime klasa, polja, metode, interfejsa i drugih simbola.
- Karakteristika: generiše se u trenutku kompajliranja i čuva u konstantnom bazenu kompajliranog fajla bajtkoda.
- Nezavisnost: ne zavisi od konkretne memorijske adrese, što daje veću fleksibilnost.
Direktna referenca
- Definicija: pokazivač koji direktno ukazuje na cilj, relativni pomeraj ili ručka koja indirektno locira cilj.
- Karakteristika: generiše se u toku izvršavanja i zavisi od konkretnog rasporeda memorije.
- Efikasnost: pošto direktno ukazuje na memorijsku adresu ili pomeraj, pristup objektu preko direktne reference je efikasniji.
Sledeća pojednostavljena slika opisuje razliku:

U gornjem primeru:
class Areferenciraclass B.- Prilikom kompajliranja ta referenca postaje simbolička referenca i čuva se u konstantnom bazenu
.classfajla. - U toku izvršavanja, kada
class Atreba da koristiclass B, JVM će simboličku referencu razrešiti na direktnu referencu koja ukazuje na objekatclass Bu memoriji ili na njegove metapodatke.
Na ovaj način Java program u trenutku kompajliranja i u toku izvršavanja ima veću fleksibilnost i labavu spregu, a u toku izvršavanja i bolje performanse.
Java je po prirodi statički jezik, ali su joj kasnije dodate osobine dinamičkog učitavanja, pa fazu razrešavanja moramo posmatrati sa ta dva aspekta.
Ako ne učestvuje dinamičko učitavanje, rezultat razrešavanja jednog simbola se može keširati, čime se izbegava višestruko razrešavanje istog simbola — jer, ako prvo razrešavanje uspe, i svako naredno će uspeti, a ako prvo ne uspe, i ponovno razrešavanje će dati isti rezultat.
Ako se dinamičko učitavanje ipak koristi, rezultat ponovnog razrešavanja simbola koji je ranije razrešavan putem dinamičkog učitavanja može biti drugačiji. Pri dinamičkom učitavanju proces razrešavanja se dešava u trenutku kada program dođe do te instrukcije — upravo zato se, kao što smo napomenuli, razrešavanje pri dinamičkom učitavanju obavlja posle inicijalizacije.
Celokupna faza razrešavanja obavlja sledeće poslove:
- Razrešavanje klasa i interfejsa
- Razrešavanje metoda klase
- Razrešavanje metoda interfejsa
- Razrešavanje polja
5) Initialization (inicijalizacija)
Ovo je poslednji korak procesa učitavanja klasa. U fazi pripreme klasnim promenljivama već bile dodeljene podrazumevane početne vrednosti, dok će u fazi inicijalizacije klasne promenljive dobiti vrednost koju kod želi da im dodeli. Drugim rečima, faza inicijalizacije jeste proces izvršavanja metode konstruktor klase (metoda <clinit>() koju vidimo u javap).
Gornji tekst može zvučati apstraktno i teško razumljivo, pa ću navesti primer.
String cmower = new String("Chenmo Wang Er");Ovaj kod koristi ključnu reč new da instancira objekat stringa, pri čemu će biti pozvan konstruktor klase String radi instanciranja cmower-a.
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}Trenuci inicijalizacije obuhvataju sledeće situacije:
- Pri kreiranju instance klase.
- Pri pristupu statičkoj metodi ili statičkom polju klase (osim
finalkonstanti, koje se u fazi kompajliranja već stavljaju u konstantni bazen). - Pri refleksivnom pozivu klase pomoću metoda iz paketa
java.lang.reflect. - Pri inicijalizaciji podklase neke klase (prvo se inicijalizuje nadklasa).
- Pri pokretanju JVM-a, glavna klasa koju korisnik navede (klasa koja sadrži metodu
main) biće inicijalizovana.
Učitač klasa
Kada smo završili proces učitavanja klasa, moramo progovoriti i o učitaču klasa.

U principu, Java programeru retko je potrebno da direktno komunicira sa učitačem klasa. Podrazumevano ponašanje JVM-a je dovoljno za većinu situacija. Ipak, ako naiđeš na situaciju u kojoj moraš da komuniciraš sa učitačem klasa, a ne razumeš dobro njegov mehanizam, moraćeš da potrošiš mnogo vremena na otklanjanje izuzetaka poput ClassNotFoundException i NoClassDefFoundError (o kojima smo već govorili).
Za bilo koju klasu, njen učitač klasa i ona sama zajedno određuju njenu jedinstvenost u okviru JVM-a. Drugim rečima, ako dve klase imaju različite učitače, čak i ako potiču iz istog fajla bajtkoda, te dve klase nužno nisu jednake (npr. njihovi Class objekti nisu equals).
Hajde da to pokažemo na jednostavnom kodu.
/**
* @author Erge
*/
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader);
loader = loader.getParent();
}
}
}Svaka Java klasa čuva referencu na učitač klasa koji ju je definisao; tu referencu možeš dobiti preko ImeKlase.class.getClassLoader(), a zatim preko loader.getParent() dobiti njen nadređeni učitač klasa.
Izlaz gornjeg koda je sledeći:
jdk.internal.loader.ClassLoaders$AppClassLoader@512ddf17
jdk.internal.loader.ClassLoaders$PlatformClassLoader@2d209079Prvi red ispisuje učitač klase Test, odnosno aplikacioni učitač klasa, koji je instanca klase jdk.internal.loader.ClassLoaders$AppClassLoader; drugi red ispisuje platformski učitač klasa, koji je instanca klase jdk.internal.loader.ClassLoaders$PlatformClassLoader. A šta je sa bootstrap učitačem klasa?
Po logici stvari, nadređeni učitač proširujućeg učitača klasa treba da bude bootstrap učitač klasa, ali je on ugrađeni učitač klasa virtuelne mašine i najčešće se prikazuje kao null.
Dakle, učitače klasa možemo podeliti u četiri tipa:
① Bootstrap ClassLoader (bootstrap učitač klasa): odgovoran za učitavanje osnovnih jezgrenih biblioteka JVM-a, npr. klasa iz rt.jar i sa putanje sun.boot.class.path.
② Extension ClassLoader (proširujući učitač klasa): odgovoran za učitavanje klasa iz biblioteka proširenja Jave, na primer klasa iz direktorijuma jre/lib/ext ili sa lokacija određenih sistemskim svojstvom java.ext.dirs.
③ System (Application) ClassLoader (sistemski/aplikacioni učitač klasa): odgovoran za učitavanje biblioteka navedenih na sistemskoj putanji klasa java.class.path, najčešće tvojih aplikacionih klasa i biblioteka trećih strana.
④ Korisnički definisani učitači klasa: Java dozvoljava korisnicima da kreiraju sopstvene učitače klasa nasleđivanjem klase java.lang.ClassLoader. To je vrlo korisno kada je potrebno dinamički učitavati resurse, implementirati modularne okvire ili primeniti posebne strategije učitavanja klasa.
import java.io.*;
public class CustomClassLoader extends ClassLoader {
private String pathToBin;
public CustomClassLoader(String pathToBin) {
this.pathToBin = pathToBin;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classData = loadClassData(name);
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException("Class " + name + " not found", e);
}
}
private byte[] loadClassData(String name) throws IOException {
String file = pathToBin + name.replace('.', File.separatorChar) + ".class";
InputStream is = new FileInputStream(file);
ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
int len = 0;
while ((len = is.read()) != -1) {
byteSt.write(len);
}
return byteSt.toByteArray();
}
}Ovaj korisnički definisani učitač klasa radi sledeće:
- Konstruktor: prihvata jedan string argument koji određuje putanju na kojoj se čuvaju fajlovi klasa.
- Preklapanje metode
findClass: kada nadređeni učitač klasa ne može da učita klasu, poziva se metodafindClass. U njoj se prvo pomoću metodeloadClassDatačita bajtkod fajla klase, a zatim poziva metodadefineClasskoja taj bajtkod pretvara uClassobjekat. - Metoda
loadClassData: čita sadržaj fajla klase na zadatoj putanji i vraća ga kao niz bajtova.
Model delegiranja roditelju
Model delegiranja roditelju (Parent Delegation Model) jeste mehanizam koji koriste Java učitači klasa, a njime se obezbeđuje stabilnost i bezbednost Java programa. U ovom modelu, kada učitač klasa pokuša da učita neku klasu, on taj zadatak najpre delegira svom nadređenom učitaču; tek ako nadređeni učitač ne može da učita tu klasu, podređeni učitač pokušava sam da je učita.
Delegiranje nadređenom učitaču: kada učitač klasa primi zahtev za učitavanje klase, on najpre ne pokušava sam da je učita, već taj zahtev delegira svom nadređenom učitaču.
Rekurzivno delegiranje: ovaj proces se rekurzivno odvija naviše, počevši od bootstrap učitača klasa (Bootstrap ClassLoader), zatim proširujućeg učitača (Extension ClassLoader), i na kraju sistemskog učitača (System ClassLoader).
Učitavanje klase: ako nadređeni učitač može da učita klasu, koristi se njegov rezultat. Ako nadređeni učitač ne može da učita tu klasu (jer je nije pronašao), podređeni učitač pokušava sam da je učita.
Bezbednost i izbegavanje višestrukog učitavanja: ovaj mehanizam osigurava da se klasa ne učita više puta i štiti klase jezgrenog API-ja Jave od zlonamerne zamene.
Hijerarhijska struktura učitača klasa prikazana je na sledećoj slici:
Bootstrap ClassLoader
↑
│
Extension ClassLoader
↑
│
System/Application ClassLoader
↑
│
Custom ClassLoaderOvaj hijerarhijski odnos naziva se model delegiranja roditelju: ako učitač klasa dobije zahtev za učitavanje klase, on će zahtev najpre proslediti nadređenom učitaču na izvršenje, nadređeni će proslediti svom nadređenom, i tako sve do učitača na samom vrhu; tek ako nadređeni učitač ne uspe da izvrši učitavanje klase, trenutni učitač pokušava sam da je učita.
Usput rečeno, model delegiranja roditelju me podseća na Džu Juanžanga — kada je postao car, čak je i funkciju kancelara ukinuo i sve je radio lično; samo ono što sam nije stizao ili imao vremena prepustio je ministrima.
Korišćenje modela delegiranja roditelju donosi jednu očiglednu prednost: Java klasa zajedno sa svojim učitačem klasa dobija hijerarhijski odnos sa prvenstvom, što je veoma važno za stabilan rad Java programa.
Gore smo spomenuli da, ako dve klase imaju različite učitače, čak i ako potiču iz istog fajla bajtkoda, one nužno nisu jednake — model delegiranja roditelju garantuje da će istu klasu na kraju učitati upravo određeni učitač klasa.
Kratak pregled
Mehanizam učitavanja klasa u Javi, kroz saradnju učitača klasa i procesa učitavanja, obezbeđuje dinamičko učitavanje, fleksibilnost i bezbednost Java programa. Model delegiranja roditelju dodatno jača bezbednost ovog mehanizma i usklađenost među klasama.
Tako se uči — samo ako se usudiš da izazoveš sebe, steći ćeš znanje; baš kao što planina stoji tu, samo ako si voljan da penješ, stići ćeš do vrha.
