JavaSvet - otvorena java zajednica

 
glavna stranica arr2javasvet  english version arr2java.net

Osnove ClassLoader-a

Igor Spasić
20 Jul 2004

Class loaderi apstrakuju proces učitavanja klasa pre njihovog prvog instanciranja čime ih čine raspoloživim za upotrebu. Svaki put kada se zahteva instanciranje klase ili se klasa statički poziva, class loader u pozadini, transparentno za sistem, pronalazi i učitava zahtevanu klasu u memoriju JVM.

Razlozi za postojanje class loadera se nalaze u samoj osnovi zahteva Java jezika: da bude nezavisan od platforme i da podržava distribuirane sisteme. Jezik koji je nezavisan od platforme ne sme da se oslanja na specifičan fajl sistem za učitavanje biblioteka. Dalje, pošto se očekuje da Java učitava klase koje se nalaze distribuirane po mreži, korišćenje fajl sistema sasvim prestaje da ima smisla. Zato, kada se u programu zahteva instanciranje neke klase, JVM zahteva od class loadera da je učita. Class loader traži klasu na način na koji je već dizajniran: na fajl sistemu, na mreži, na ROM čipu itd.

Javini class loaderi

Java definiše sledeća 3 class loadera:

Opis rada class loadera

Sledi opis organizacije Javinih class loadera i način rada.

Hijerarhija class loadera

Jasno je da JVM mora nekako da 'zna' kada da koristi neki od navedenih class loadera. Zato su class loaderi organizovani hijerarhijski, pri čemu se koristi model delegiranja (delegate) zahteva kako bi se pronašao pravi class loader.

Dakle, kada JVM dobije zahtev za instaciranjem neke klase, obratiće se prvo System class loaderu. Međutim, System class loader ne pristupa odmah traženju klase. Prvo se primenjuje metod delegiranja: zahtev se prosleđuje sledećem class loaderu u hijerarhiji. Tako zahtev stiže sve do Bootstrap class loadera gde se zaista prvi put i obrađuje, pošto je on poslednji u hijerarhiji.

Ako je klasa pronađena, ona će biti vraćena istim putem nazad. U suportnom, Bootstrap class loader vraća informacija da klasa nije nađena, što je znak Extension class loaderu da on obradi zahtev. Ako i on ne pronađe klasu, zahtev se konačno vraća do System class loadera koji će pokušati da je nađe.

Ako je nakon obrade zahteva klasa pronađena, ona će biti učitana u memoriju JVM. U suprotnom, generiše se ClassNotFoundException.

Postupak učitavanja klasa

Procedura učitavanja nađene klase se može podeliti na sledeće celine:

  1. Učitavanje
    • lociranje bajtkod fajla na fajl sistemu, mreži, ROMu, itd.
    • učitavanje bajtkoda u JVM heap
    • parsiranje bajtkoda na sekcije za konstante, metode, atribute itd.
    • kreiranje instance java.lang.Class na osnovu učitanog bajtkoda
  2. Linkovanje
    • provera - proverava se osnovna strktura i forma klase, uključujući i proveru bajtkoda. Proverava se da li struktura class fajla odgovara Java standardu, semantika, validnost instrukcija i toka bajtkoda, simbolične reference. Provera simboličkih referenci ima veze sa dinamičkim linkovanjem (dynamic binding) jer se linkovanje radi za vreme izvršavanja programa. Verfikacija simboličkih referenci podrazumeva i učitavanje referenci ukoliko nisu do sada učitane, proveru ispravnosti postojećih potpisa metoda itd.
    • priprema - alocira se memorija za podatke klase kao što su, na primer, statičke varijable
    • razrešavanje (resolve) - pretvaranje simboličkih referenci (na pr. imena promenljivih) iz class fajla u direktne reference
  3. Inicijalizacija
    Statičke varijable i konstante se moraju setovati na default ili na zadane vrednosti. Inicijalizacioni kod se ceo nalazi u specijalnom metodu <clinit> koga izvršava JVM.

Ova procedura je okvirna i različite implementacije JVM je mogu nadograđivati i menjati. Na primer, klase se mogu keširati radi bržeg učitavanja, verifikacija za jednu klasu se može raditi samo prvi put kada se čita itd.

Zamena sistemskih biblioteka

Java v1.4 donosi i jednu zanimljivu funkcionalnost, a to je mogućnost zamene nekih sistemskih biblioteka novijim (Endorsed Standard Override Mechanism). Pošto Bootstrap class loader nalazi sistemske biblioteke, novije verzije istih koje su navedene u CLASSPATH-u biće ignorisane, jer se već učitane. Pomenuta funkcionalnost omogućava da se ipak koriste novje verzije nekih sistemskih klasa, tako što će biti smeštene u %JAVA_HOME%/lib/endorsed folder.

Ova funkcionalnost zamene je moguća samo za JAXP i CORBA sistemske biblioteke.

Još neke osobine class loadera

Opisani Javini class loaderi koriste i sledeće važne mehanizme:

Custom class loaderi

Java omogućava jednostavno kreiranje custom class loadera. Za njihovo kreiranje je predviđena apstraktna klasa java.lang.ClassLoader. Ona implementira svu neophodnu logiku koja transformiše niz pročitanih bajtova u Class objekat, čime je njena upotreba značajno olakšana i pojednostavljena. Međutim, ClassLoader ne implementira mehanizam za pronalaženje i učitavanje fajlova klasa.

Java dolazi sa sledećim implementacijama ClassLoader:

Za standardnu upotrebu dovoljne su Javine implementacije class loadera. Kasnije će biti pokazano kako se one koriste, tj. kako se klase dinamički učitavaju. Ipak, custom class loaderi mogu da nađu interesantne primene. Evo nekih ideja:

Primer #1 - učitavanje

Kao što je rečeno, custom class loader se pravi nasleđivanjem klase java.lang.ClassLoader. Ova apstraktna klasa sadrži sve potrebne funkcionalnosti za rad sa bajt kodom i sa njegovim pretvaranjem u java.lang.Class instancu tako da je razvijaoc custom class loadera oslobođen velikog posla. Praktično, ono što treba uraditi je implementirati metod loadClass().

U primeru se nalazi jednostavna implementacija custom class loadera (FooClassLoader). Njegov zadatak je da prvo potraži zahtevanu klasu na standardan način, konsultujući Java class loadere (delegiranje). Ako oni ne nađu klasu, onda je FooClassLoader traži u specijalnom folderu, gde, primera radi, fajlovi klasa imaju ekstenziju '.klasa' umesto standardnog '.class'. Ako se pogleda implementacija FooClassLoader.loadClass() metode, uočava se sledeći postupak:

  1. traži se zahtevana klasa u baferu već nađenih klasa,
  2. ako klasa nije nađena, zahtev se delegira Javinim class loaderima,
  3. ako klasa nije nađena, učitava se klasa sa lokacije koja nije na CLASSPATH putanji,
  4. ako je klasa uspešno pročitana iz fajla, pročitani sadržaj se parsira (klasa se definiše),
  5. radi se resolve klase,
  6. pre nego što se vrati nova Class instanca, smešta se u bafer.

Važno je uočiti postojanje bafera za već nađene klase koje su prethodno bile učitane istom instancom class loadera. Bafer postoji zbog toga što svaki put kada se zahteva jedna klasa class loader mora da vrati referencu na isti Class objekat. U suprotnom, sistem bi svaku novu instancu posmatrao kao različitu klasu što bi dovodilo do Exception-a. Dalje, postojanje bafera (keša) je zgodno i zbog performasi, kako sistem ne bi iznova tražio i proveravao klase koje su već nađene.

Sledeći važan momenat je delegiranje zahteva Javinim class loaderima, prema ranije prikazanoj hijerarhiji. Ovo je jako bitno za sigurnost i stabilnost sistema, kako custom class loader ne bi vratio neke svoje (izmenjene) instance postojećih i/ili sistemskih klasa. Dalje, uočava se da delegiranje nije neophodno i da ne postoji ništa što sprečava custom class loader da ne pozove Javine class loadere. Ovo su važna pitanja, pa će kasnije biti objašnjena sa više detalja.

U ovom primeru treba učitati klasu Ext1 koja se ne nalazi na CLASSPATH-u, već u pomenutom posebnom folderu, sa promenjenom ekstenzijom. Kod koji koristi FooClassLoader je jednostavan:

FooClassLoader fcl = new FooClassLoader();
Class clazz = fcl.loadClass
("Ext1");
Ext ext1 =
(Ext) clazz.newInstance();

Kako se klasa Ext1 ne nalazi na CLASSPATH-u, FooClassLoader će potražiti fajl 'Ext1.klasa' u posebnom folderu i odatle učitati klasu. Poslednji red ovog isečka je zanimljiv jer se kreirana instanca objekta Ext1 kastuje u Ext. Iako deluje ispravno, nije moguće pisati sledeće:

Ext1 ext1 = (Ext1) clazz.newInstance();

Ovako napisan red prouzrokuje java.lang.NoClassDefFoundError. Razlog je očigledan: klasa Ext1 nije dostupna Javinim class loaderima koji se koriste za pokretanje i rad programa primera, tako da oni jednostavno ne vide tu klasu. Odatle se zaključuje da klasa koja se dinamički učitava mora da bude nekog tipa koji je dostupan na mestu korišćenja, da bi mogla da se kastuje iz Object. Zato u primeru klasa Ext1 implementira interfejs Ext koji je 'vidljiv' i na mestu korišćenja, pa kastovanje prolazi bez problema.

Rezultat rada primera je:

+ ext2
Ext2.foo

+ ext1
> load class: Ext1
> custom class
> find class: Ext1
> load class: java.lang.Object
> system class
> load class: Ext
> system class
> class loaded ok
> load class: java.lang.System
> system class
> load class: java.io.PrintStream
> system class
Ext1.foo

Odavde se vidi da se za samo jedan upućen zahtev za instanciranjem FooClassLoader interno poziva čak 5 puta! Ipak nema mesta zabuni: rečeno je već da class loader implicitno učitava i sve druge neophodne klase, što ovde praktično znači učitavanje svih klasa koje se koriste u Ext1: Object, Ext, System i PrintStream. Zahtevi za učitavanjem ovih klasa se upućuju ponovo FooClassLoader koji ih delegira Javinim class loaderima. Učitavanje je uspešno jer se sve zahtevane klase nalaze na CLASSPATH-u.

Primer #2 - različiti namespace

Na prvi pogled se čini da je dinamički kreirana klasa Ext1 iz prethodnog primera istovetna kao i kada bi bila kreirana na standardni način, kao i svaka klasa koje je na CLASSPATH-u (na pr. Ext2). Ipak, razlika postoji: klase se nalaze u različitim namespace-ovima! Svaka klasa 'pamti' referencu na class loader koji ju je učitao i predstavlja deo njegovog namespace-a.

Da bi se ovo proverilo, prethodni primer je izmenjen da bi se foo() metod deklarisao sa default access. Pošto su Ext1 i Ext2 u istom paketu kao i glavni program, čini se da će biti moguće pozvati njihove foo() metode. Rezultat rada programa je:

+ ext2
Ext2.foo

+ ext1
> load class: Ext1
> custom class
> find class: Ext1
> load class: Ext
> system class
> class loaded ok
> load class: java.lang.Object
> system class
Exception in thread "main" java.lang.AbstractMethodError: Ext.foo()V
        at RunMe.main(RunMe.java:16)

Program upravo puca u liniji:

ext1.foo();

Glavni program je učitan samo Javinim class loaderima. Klasu Ext1 učitava FooClassLoader, čime se ona nalazi u drugom namespacu. Iako je u pitanju isti paket, namespace je različit, pa glavni program praktično 'ne vidi' Ext1.foo() metodu. Umesto toga, poziva se metod Ext.foo(), što, jasno, dovodi do greške - metod je apstraktan.

Ova greška se ispravlja ako se Ext.foo() i njene implementacije deklarišu kao public. Rezultat rada glavnog programa je tada istovetan prethodnom primeru.

Ovo je važna osobina class loadera. Znači, iako u istom paketu, klase Ext1 i Ext2 iz primera se nalaze u različitm namespace-ovima jer su učitane različitim class loaderima.

Dinamičko učitavanje klasa

Za dinamičko učitavanje klasa nije neophodno praviti novi custom class loadere. Ukoliko nema potrebe za nekim specijalnim vidom učitavanja, dovoljno je koristiti neki od Javinih ponuđenih mehanizama za dinamičko učitavanjem klasa. Generalno, u Javi postoje 2 načina za učitavanje klasa:


Navedene razlike nemaju uticaja na obične Java programe. Međutim, u dinamičkim okruženjima, gde postoji razgranata hijerarhija class loadera i gde neki ne moraju da delegiraju zahtev standardnim Java class loaderima (na pr.: aplikacioni serveri), ova razlika dolazi do izražaja. Tada Class.forName(String) postaje neupotrebljiv, jer class loader pozivajuće klase može biti izolovan i da ne vidi klasu koja je regularno na CLASSPATH-u. Ovo je bio jedan od razloga zašto Java2 donosi proširenja koja se tiču dinamičkog učitavanja klasa. Sledi njihov opis.

Class.forName(String, boolean, ClassLoader) je nova verzija forName metode. Za razliku od prethodne, korisnik ovde može da utiče na to koji se class loader koristi za učitavanje klasa. Drugi parametar boolean tipa određuje da li se kreirana instanca inicijalizuje odmah prilikom učitavanja ili kasnije, pri prvoj njenoj upotrebi. Između stare i nove verzije postoji sledeća relacija:

Class.forName("Foo") == Class.forName("Foo", true, this.getClass().getClassLoader())

Drugo proširenje je Thread.currentThread().getContextClassLoader(). Ova metoda vraća class loader threada. Po defaultu, class loader novo kreiranog threada je class loader roditeljskog (parent) threada, koji ga je kreirao. Ova vrednost se može promeniti sa setContextClassLoader(). Na ovaj način je moguće dobiti referencu na isti class loader bez obzira na trenutni nivo u hijerarhiji class loadera.

Kombinacijom ova dve funkcionalnosti, na mnogim mestima je sledeći način preporučen za dinamičko učitavanje klasa:

Class.forName("Foo", true, Thread.currentThread().getContextClassLoader());

gde se sa drugim parametrom može upravljati inicijalizacijom učitane klase. Često se ovaj preporučeni način proširuje tako što se prvo koristi class loader trenutne klase, pa tek onda onaj definisan u threadu.

Primer #3 - forName vs. getContextClassLoader

Treći primer je nastao proširenjem prethodnog. Sada se u klasu Ext1, koja se i sama dinamički učitava, uvodi učitavanje klase Misc. Dinamičko učitavanje klase Misc se radi dva puta: prvi put koristeći Class.forName(), a drugi put getContextClassLoader().

Glavni program, dakle, prvo dinamički učitava klasu Ext1 koristeći FooClassLoader. Kada se napravi instanca klase Ext1, poziva se njen metod u okviru kojeg se dinamički učitava Misc klasa na dva različita načina. Na ovaj način se simulira neko dinamičko okruženje koji može imati razgranatu strukturu class loadera. Učitana klasa se ne instancira, da Ext1 ne bio implicitno zavisan od Misc. Rezultat rada programa je:

+ ext2
Ext2.foo
Misc.foo
Misc.foo

+ ext1
... (sve isto kao i ranije) ...
Ext1.foo
#1
> load class: java.lang.Class
> system class
> load class: Misc
> system class
#2
> load class: java.lang.Thread
> system class
> load class: java.lang.ClassLoader
> system class

Ovaj primer ilustruje razliku između dva gore pomenuta načina učitavanja. Za prvi način je korišćen stari oblik Class.forName(). Kako on koristi class loader klase koja radi učitavanje, ovde se, dakle, opet koristi FooClassLoader, pošto je njime učitana klasa Ext1. Zato na konzoli postoji ispis "load class: Misc". Misc klasa se u ovom primeru nalazi na CLASSPATH-u, pa FooClassLoader može da je učita, jer delegira zahtev Javinim class loaderima. Međutim, da je FooClassLoader bio implementiran drugačije i da nije delegirao zahtev, Ext1 ne bi mogao da učita klasu Misc iako je na CLASSPATH-u!

Sa drugim načinom (#2) učitavanja ovde nema takvih problema: getContextClassLoader() vraća referencu na class loader glavne aplikacija (glavnog threada), tako da FooClassLoader uopšte ne učestvuje u drugom učitavanju klase Misc. Zato i nema nikakvog ispisa na konzoli koji bi označavao aktivnost instance FooClassLoader.

Proširenja primera #3

Metod Ext1.foo() sadrži komentarisan poziv Ext.look(), metode koja se nalazi u osnovnoj klasi. Šta se dešava kada se ova linija odkomentariše?

Ext1.foo
Exception in thread "main" java.lang.IllegalAccessError: tried to access method Ext.look()V from class Ext1
        at Ext1.foo(Ext1.java:7)
        at RunMe.main(RunMe.java:16)

Stvar je jasna: zbog različitih namespace-ova, default access metod Ext.look() se ne vidi u Ext1.foo().

Proširenja primera #2

Ovo proširenje koristi izolovan custom class loader. Za svrhu ovog primera, FooClassLoader je izmenjen tako da delegira samo zahteve za učitavanjem osnovnih Java klasa. Zbog toga je bilo potrebno imati i fajl 'Ext.klasa' u posebnom folderu. Rezultat rada programa je:

+ ext2
Ext2.foo

+ ext1
> load class: Ext1
> find class: Ext1
> load class: Ext
> find class: Ext
> load class: java.lang.Object
> system class
> class loaded ok
> class loaded ok
Exception in thread "main" java.lang.ClassCastException
        at RunMe.main(RunMe.java:15)

Program puca u liniji:

Ext ext1 = (Ext) clazz.newInstance();

Iako na prvi pogled deluje neobično, jer je tip kastovanja dobar, nema mesta zabuni. Kastovanje se radi u glavnom programu, pa se kastovanje vrši u klasu Ext koja je na CLASSPATH-u, dok clazz.newInstance() vraća instancu klase Ext koju je učitao FooClassLoader iz posebnog foldera. Ovde se, dakle, razlikuju dve Ext klase: jedna koje je učitana Javinim class loaderima i koja se nalazi na CLASSPATH-u, i druga koju je učitao FooClassLoader iz posebnog foldera koji nije na CLASSPATH-u. Kako su zato u pitanju različite klase, kastovanje ne može da uspe.

Sigurnost

Class loaderi se nalaze u samom centru Javinog sigurnosnog modela. Njihova arhitektura i organizacija rešava potencijalne sigurnosne probleme koristeći sledeće strategije:

Resursi

Sledi lista primera za download.

primer #1
primer #2
primer #3
proširenje primera #2