Professional Documents
Culture Documents
di Guido Anselmi
Tutto quello che accade nella JVM durante linizializzazione di una classe o interfaccia Java
Guido Anselmi Laureato in Ingegneria Informatica, si interessa da molti anni di programmazione Object Oriented. Attualmente si occupa di formazione ` Java e di qualita del software presso una ` importante societa nanziaria di Milano.
Limpaginazione automatica di questa rivista e realizzata al ` 100% con strumenti Open Source usando OpenOffice, Emacs, BHL, LaTeX, Gimp, Inkscape e i linguaggi Lisp, Python e BASH
For copyright information about the contents of Computer Programming, please see the section Copyright at the end of each article if exists, otherwise ask authors. Infomedia contents is 2004 Infomedia and released as Creative Commons 2.5 BY-NC-ND. Turing Club content is 2004 Turing Club released as Creative Commons 2.5 BY-ND. Le informazioni di copyright sul contenuto di Computer Programming sono riportate nella sezione Copyright alla ne di ciascun articolo o vanno richieste direttamente agli autori. Il contenuto Infomedia e 2004 Infome` dia e rilasciato con Licenza Creative Commons 2.5 BYNC-ND. Il contenuto Turing Club e 2004 Turing Club ` e rilasciato con Licenza Creative Commons 2.5 BY-ND. Si applicano tutte le norme di tutela dei marchi e dei segni distintivi. ` E in ogni caso ammessa la riproduzione parziale o totale dei testi e delle immagini per scopo didattico purch e vengano integralmente citati gli autori e la completa identicazione della testata. Manoscritti e foto originali, anche se non pubblicati, non si restituiscono. Contenuto pubblicitario inferiore al 45%. La biograa dellautore riportata nellarticolo e sul sito www.infomedia.it e di norma quella disponibi` le nella stampa dellarticolo o aggiornata a cura dellautore stesso. Per aggiornarla scrivere a info@infomedia.it o farlo in autonomia allindirizzo http://mags.programmers.net/moduli/biograa
linguaggi
Prima Parte
copo del presente articolo la descrizione dettagliata del processo di inizializzazione dei tipi non primitivi in unapplicazione Java. Tale processo, spesso ignorato o sottovalutato dai programmatori, un meccanismo molto complesso e per certi versi raffinato, la cui conoscenza approfondita pu essere daiuto nella realizzazione di applicazioni correttamente funzionanti.
FIGURA 1
Guido Anselmi
ganselmi@infomedia.it
Laureato in Ingegneria Informatica, si interessa da molti anni di programmazione Object Oriented. Attualmente si occupa di formazione Java e di qualit del software presso una importante societ finanziaria di Milano.
JVM non dicono in alcun modo dove i dati rappresentanti un tipo vadano conservati, quindi nessuno vieterebbe di realizzare una JVM che prenda tali dati ad esempio da un database proprietario. Larea di memoria designata a contenere tutte le informazioni sui tipi Java denominata Method Area. In essa viene conservata la descrizione completa di ogni tipo caricato in memoria per una data applicazione. Tali informazioni conterranno il nome del tipo, il nome della superclasse diretta (a meno che il tipo sia Object o una interfaccia), una lista ordinata di tutte le superinterfacce direttamente implementate, oltre che il modificatore daccesso del tipo. Fanno parte della Method Area anche la descrizione dei field e dei metodi dichiarati nel tipo; per i field tali informazioni consistono essenzialmente in nome, modificatore daccesso e tipo, mentre per i metodi ci saranno anche il nome del tipo eventualmente ritornato (oppure void), la lista con nomi e tipo dei parametri, il bytecode corrispondente al codice del metodo ed infine il modificatore daccesso. Tra i class data di ciascun tipo nella Method Area trovano logicamente posto anche tutte le variabili di classe del tipo (ossia quelle dichiarate con il modifi-
44
PROGRAMMING
TABELLA 1 I valori di default
Tipo int long short char byte boolean reference float double
la verifica che non ci siano dichiarazioni di metodi incompatibili, ad esempio due metodi con ugual nome e lista dei parametri, ma differente tipo di ritorno; la verifica dellintegrit del bytecode, ad esempio sincerandosi che nessuna istruzione di salto setti il program counter oltre la fine di un metodo, causando il crash della JVM. Effettuati con esito positivo questi (e molti altri) controlli, la fase di verification lascia spazio alla preparation. In tale contesto a tutte le variabili di classe vengono assegnati i valori di default, in accordo a quanto riportato in Tabella 1. molto importante ricordare che qualunque variabile di classe passa sempre per il suo valore di default, anche quando il programmatore intende assegnarle un valore proprio, e questo pu richiedere alcune accortezze per evitare situazioni di errore.
catore static), non legate quindi a nessuna istanza di una classe. Infine, per ciascun tipo, la JVM inserisce nei class data della Method Area un constant pool. Esso una sorta di array ordinato contenente tutte le costanti usate dal tipo, inclusi i literals ed i references simbolici ad altri tipi. La descrizione esatta della struttura del constant pool esula dagli scopi di tale articolo, tuttavia esso, o meglio la sua versione a runtime (il runtime constant pool), gioca un ruolo cruciale nel successivo processo di risoluzione. Bench la Method Area non sia direttamente accessibile alle applicazioni Java, qualunque programma in esecuzione vi fa continuamente riferimento [1]. Una volta costruite opportunamente le strutture dati rappresentanti il tipo nella Method Area, la fase di loading prevede infine la creazione di una nuova istanza di java.lang.Class che rappresenta una sorta di ponte di collegamento tra la Method Area ed i programmi Java, consentendo ai programmatori di recuperare ogni sorta di informazione sui tipi caricati al suo interno. Le istanze di tale classe sono infatti alla base del meccanismo di RunTimeTypeIdentification di Java.
public class Esempio1 { static { echo(Uno); } static String s = echo(Due); static { echo(Tre); } private static String echo(String s) { System.out.print(s + ,); return s; } public static void main(String[] args) { } }
45
linguaggi
LISTATO 2
Inizializzazione di gerarchie di classi
suo primo uso attivo. Solo le seguenti sei attivit sono classificate come uso attivo: creazione di una nuova istanza di una classe; invocazione di un metodo statico di una classe; uso di un field statico non costante dichiarato in una classe o interfaccia; uso di alcuni metodi della reflection API; inizializzazione di una sottoclasse; designazione di una classe come main-class. Si noti come nessunaltra attivit al di fuori di queste sei comporti inizializzazione di un tipo da parte della JVM. Lo scopo ultimo dellinizializzazione lassegnazione del valore proprio alle variabili di classe; a tal fine, Java mette a disposizione del programmatore due strumenti distinti: static variable initializers static initialization block Un semplice esempio duso di tali meccanismi riportato nel Listato 1. In esso sono presenti due static initialization block ed uno static variable initializers, che vengono sempre eseguiti nellordine testuale di dichiarazione. Bench una strategia come quella descritta nel Listato 1, con due static initialization block distinti, possa sembrare strana, essa perfettamente legale, bench non raccomandabile in quanto a chiarezza del codice.
class Super { static { System.out.println(Super inizializzato); } } class Sub extends Super { static { System.out.println(Sub inizializzato); } public static int x = 5; }
class EsempioPadre { static { System.out.println(EsempioPadre inizializzato); } } public class Esempio2 extends EsempioPadre { static { System.out.println(Esempio2 inizializzato); } public static void main(String a[]){ int i = Sub.x; } }
decidere di posporla ad una fase successiva a quella di linking, e pi precisamente al primo utilizzo del reference simbolico stesso. In questo ultimo caso si parla di late resolution, mentre si parla di early resolution quando la JVM decide di risolvere i tipi direttamente nella fase di linking. Lunico obbligo imposto dalle specifiche agli implementatori quello di dare sempre e comunque lidea di star utilizzando late resolution. Questo vuol dire che anche quando la JVM utilizza una strategia di early resolution, essa deve segnalare gli eventuali errori incontrati solo allultimo momento utile, e cio in corrispondenza del primo utilizzo effettivo di ogni reference. Ad esempio la risoluzione di un qualunque reference simbolico ad un field o ad un metodo potrebbe sollevare un NoClassDefFoundError se la classe cui fa riferimento non fosse rintracciabile in base alle impostazioni correnti del classpath. Se tuttavia quello stesso reference non fosse mai utilizzato nel corso dellesecuzione, lerrore non andrebbe mai segnalato e tutto dovrebbe procedere come se la sua risoluzione non avesse mai avuto corso.
La fase di verification
corrisponde allanalisi dei dati appena caricati in memoria ed alla verifica della loro correttezza in termini dellobbedienza alla semantica del linguaggio
Si noti come il metodo main della classe in oggetto sia completamente vuoto, ma laver designato tale classe come main-class corrisponde ad un suo uso attivo e d quindi luogo allinizializzazione. Sia gli static variable initializers che gli static initialization block sono raccolti dalla JVM in un unico ClassInitializationMethod, che viene invocato come ultimo passo della procedura di inizializzazione, come vedremo in seguito in maggior dettaglio. La JVM non costruisce un ClassInitializationMethod per tutte le classi che carica ed inizializza, ma solo per
Inizializzazione di classi
In questa fase, infine, alle variabili di classe viene assegnato il valore proprio, sostituendo il valore di default, in base a quanto specificato dal programmatore. Mentre le specifiche della JVM lasciano molta libert alle implementazioni per quanto riguarda le fasi di verification e linking, esse definiscono invece rigorosamente il momento in cui un tipo deve essere inizializzato. Tale momento coincide inderogabilmente con il
46
PROGRAMMING
quelle che richiedono effettivamente del codice Java per linizializzazione. Si consideri ad esempio il seguente codice:
public class NoClInit{ public static final int x = 10; public static final int y = 20; }
Ad un primo esame tale codice sembra del tutto simile al Listato 1, ma la JVM non crea per la classe NoClInit il ClassInitializationMethod. Il motivo risiede nel fatto che le due variabili in essa dichiarate sono inizializzate a valori costanti noti a compile time. La JVM tratta le costanti in maniera peculiare; infatti esse non vengono memorizzate nella Method Area tra i dati di classe della classe che le dichiara, ma il loro valore viene copiato direttamente nel constant pool delle classi che le utilizzano. Ad esempio una classe come la sottostante, che accede ad una costante dichiarata al suo esterno:
public class UsaCostanti{ static final int a = NoClInit.x; }
loro valore proprio e non a quello di default. Questa strategia implica che la prima classe inizializzata sia sempre e comunque Object. Un esempio di quanto avviene in presenza di gerarchie complesse di classi riportato nel Listato 2. In esso si fa un uso attivo della classe Esempio2, in quanto main class dellapplicazione, che deve pertanto essere inizializzata. Appena terminato il suo linking, la JVM verifica se la superclasse diretta stata a sua volta inizializzata. Poich questo non ancora avvenuto, prima di invocare il ClassInitializationMethod di Esempio2, la JVM effettua linizializzazione di EsempioPadre. Il processo si ripete a tal punto identico, con EsempioPadre nel ruolo di discendente ed Object in quello di antenato (si noti come in nessuna delle implementazioni esistenti della JVM, Object necessiti di un ClassInitializationMethod). Le classi inizializzate immediatamente prima dellesecuzione del metodo main sono dunque le seguenti, ordinate cronologicamente: Object EsempioPadre Esempio2.
Larea di memoria
designata a contenere tutte le informazioni sui tipi Java denominata Method Area
Solo dopo linizializzazione di EsempioPadre, la JVM inizia lesecuzione del bytecode corrispondente al corpo del metodo main. In tale metodo si accede ad un field statico della classe Sub e ci, come ormai sappiamo, un uso attivo di tale classe. Poich per la superclasse diretta di Sub non a quel punto ancora stata inizializzata, la JVM non chiama subito il ClassInitializationMethod di Sub, ma passa dapprima ad inizializzare la classe Super, come si pu facilmente constatare analizzando loutput prodotto dallesecuzione di questa semplice applicazione. molto importante notare come lutilizzo di un field statico sia un uso attivo solo della classe che dichiara quel field. Consideriamo ad esempio il seguente semplice codice:
class Super{ static int a = 10; static {System.out.println(Super initialized);} }
avr il valore 10 memorizzato da qualche parte del suo constant pool ed il ClassInitializationMethod acceder ad esso per inizializzare la variabile a.
interface I { int i = Esempio3.getX(5); } interface J extends I { int j = Esempio3.getX(10); } public class Esempio3 { public static int getX(int x){ System.out.println(getX + x); return x; } public static void main (String a[]) { int l = J.j; } }
class Sub extends Super{ static { System.out.println(Sub initialized);} } public class Test { public static void main (String[] args){ int a = Sub.a;
47
linguaggi
LISTATO 4
Possibile implementazione della procedura di inizializzazione
Inizializzazione di interfacce
Quanto detto finora per le classi vale in larga misura anche per le interfacce. In esse non possibile dichiarare degli static initialization block, ma alloccorrenza viene comunque costruito un InterfaceInitializationMethod che la JVM invoca al termine della procedura di inizializzazione. La differenza fondamentale con il processo di inizializzazione delle classi che per le interfacce non avviene linizializzazione delle superinterfacce dirette. Consideriamo ad esempio il Listato 3. In esso siamo ormai immediatamente in grado di individuare il primo uso attivo che viene fatto della classe Esempio3, in quanto main class dellapplicazione. Allo stesso modo laccesso al field j dellinterfaccia J un uso attivo dellinterfaccia stessa. Il punto chiave, e lunica differenza con quanto detto finora per le classi, che linterfaccia I, padre di J, non viene inizializzata in tale frangente. Loutput del programma consiste infatti nella sola linea di testo getX=10, a testimonianza che solo lInterfaceInitializationMethod di J stato eseguito. Si noti inoltre come, sostituendo lunica istruzione del metodo main con la seguente
int i = J.i;
/** Questo codice presentato esclusivamente a fini didattici, allo scopo di illustrare il lavoro svolto dalla JVM allatto di inizializzare una qualsiasi classe o interfaccia. Non esiste nulla di simile nelle Java API. */ public int initialize(InitializingType aType) throws ExceptionInInitializerError { synchronized (aType) { while (aType.getStatus() == InitializingType. INITIALIZATION_IN_PROGRESS){ if (aType.getInitializingThread() == Thread.currentThread()){ return 0; } try{ aType.wait(); }catch(InterruptedException ie) {/*ignore*/} if (aType.getStatus() == InitializingType.FULLY_INITIALIZED ){ return 0; } else if (aType.getStatus() == InitializingType.INITIALIZATION_ERROR ){ throw new NoClassDefFoundError(); } else { aType.setStatus(InitializingType. INITIALIZATION_IN_PROGRESS); aType.setInitialingThread (Thread.currentThread()); } }//while }//synchronized if (aType.isAClass() && aType.getSuperClass(). getStatus() == InitializingType.NOT_INITIALIZED){ initialize(aType.getSuperClass()); } try { if (aType.isAClass() ){ aType.executeClassInitializationMethod(); } else{ aType.executeInterfaceInitializationMethod(); } }catch(Exception ie){ synchronized (aType){ aType.setStatus(InitializingType. INITIALIZATION_ERROR); aType.notifyAll(); } throw new ExceptionInInitializerError (ie); }//catch synchronized (aType){ aType.setStatus(InitializingType. FULLY_INITIALIZED); aType.notifyAll(); } return 0; }//initialize
si ottiene in output: getX=5. Analogamente a quanto detto per la classi, laccesso ad un field statico di uninterfaccia corrisponde ad un uso attivo esclusivamente dellinterfaccia che dichiara il field e non delle eventuali sottointerfacce che ereditano il field stesso.
} }
Lesecuzione di tale codice comporta linizializzazione solo della classe Super e non di Sub poich, bench si acceda al field a attraverso il nome della sottoclasse, tale field dichiarato nel padre. Si noti come naturalmente anche le classi Test ed Object, alla luce di quanto finora detto, vengano a loro volta inizializzate nel corso dellesecuzione dellapplicazione in esame.
static int rnd(){ return (int)Math.random();} } public class B{ static int getX(){ return A.rnd();} }
Il processo di inizializzazione per la classe A, come abbiamo visto, comporta lesecuzione del suo unico
48
PROGRAMMING
class variable initializer che, a sua volta, richiede linizializzazione della classe B. Lassegnazione del valore proprio alla variabile x comporta infatti linvocazione di un metodo statico definito nella classe B e questo corrisponde anche al primo uso attivo di B. Eseguendo tale metodo, la JVM incorre tuttavia in un nuovo uso attivo della classe A che, pur non essendo ancora stata completamente inizializzata, non tuttavia da considerare da inizializzare ex-novo, ma piuttosto in fase di inizializzazione ad opera dello stesso thread. levando una NoClassDefFoundError, devono essere intraprese due azioni distinte). In primo luogo lo stato del tipo viene impostato al valore INITIALIZING; successivamente il thread corrente si registra al tipo come il thread che sta effettuando linizializazzione stessa, rilasciando infine il lock acquisito. Solo a questo punto inizia il processo di inizializzazione vero e proprio. Se il tipo una classe e la superclasse diretta non stata inizializzata, si invoca ricorsivamente il metodo initialize() sulla sua superclasse diretta, in ossequio alle regole di inizializzazione illustrate in precedenza. Se tale processo va a buon fine, si procede infine allinvocazione del ClassInitializationMethod del tipo, che a sua volta conterr linvocazione dei class variable initializers e degli static block initializers, o dellInterfaceInitializationMethod se il tipo una interfaccia. Se lesecuzione degli initializers completa normalmente, si riacquisisce il lock sul tipo, si imposta il suo stato a FULLY_INITIALIZED e si notificano tutti i thread eventualmente in attesa. In caso di eccezione, invece, dopo aver riottenuto il lock, si imposta lo stato di errore e si notificano i thread in attesa, dopodich si propaga leccezione a segnalare la mancata inizializzazione.
Conclusioni
Abbiamo visto con dovizia di particolari tutto quanto legato al primo uso attivo di una classe o interfaccia, attivit per molti versi banale dal punto di vista della programmazione in Java, ma che si traduce in un processo molto complesso a livello della macchina virtuale. La conoscenza approfondita di questo processo comunque imprescindibile per realizzare programmi Java correttamente funzionanti. Nel prossimo articolo affronteremo nei dettagli un uso attivo particolare tra i sei individuati: listanziazione di un nuovo oggetto sullo heap.
CODICE ALLEGATO
ftp.infomedia.it
Init1
49