2010-04-01 19 views
11

Considerate questa semplice classe Java:Perché la richiesta invokevirtual di Java per risolvere la classe di compilazione del metodo chiamato?

class MyClass { 
    public void bar(MyClass c) { 
    c.foo(); 
    } 
} 

voglio discutere di ciò che accade sulla linea c.foo().

originale, ingannevole domanda

Nota: Non tutto questo in realtà accade con ogni singolo codice operativo invokevirtual. Suggerimento: se vuoi capire il richiamo del metodo Java, non leggere solo la documentazione per invokevirtual!

A livello bytecode, la carne di c.foo() sarà il codice operativo invokevirtual, e, secondo the documentation for invokevirtual, più o meno il seguente accadrà:

  1. Cercare il metodo foo definito in compile-time classe MyClass. (Ciò implica prima la risoluzione di MyClass.)
  2. Effettuare alcuni controlli, tra cui: Verificare che c non sia un metodo di inizializzazione e verificare che la chiamata a MyClass.foo non violi alcun modificatore protetto.
  3. Calcolare il metodo per chiamare effettivamente. In particolare, cercare il tipo di runtime c. Se quel tipo ha pippo(), chiama quel metodo e ritorna. Altrimenti, cercare la superclasse del tipo runtime di c; se quel tipo ha pippo, chiama quel metodo e ritorna. Altrimenti, cercare la superclasse della superclasse del tipo runtime di c; se quel tipo ha pippo, chiama quel metodo e ritorna. Ecc. Se non è possibile trovare un metodo adatto, allora errore.

Il passaggio n. 3 sembra adeguato per determinare quale metodo chiamare e verificare che il metodo abbia i tipi di argomento/ritorno corretti. Quindi la mia domanda è perché il primo passo viene eseguito in primo luogo. Le risposte possibili sembrano essere:

  • Non si dispone di informazioni sufficienti per eseguire il passaggio 3 fino al completamento del passaggio 1. (Questo sembra poco plausibile, quindi, per favore, spiegarlo.)
  • I controlli di modifica del collegamento o dell'accesso eseguiti in # 1 e # 2 sono essenziali per evitare che accadano determinate cose negative, e tali controlli devono essere eseguiti in base al tipo di compilazione, piuttosto che la gerarchia di tipi di runtime. (Si prega di spiegare.)

Revised Domanda

Il nucleo dell'uscita javac compilatore per la linea c.foo() sarà un'istruzione come questa:

invokevirtual i 

in cui i è un indice del pool costante di runtime di MyClass. La voce di pool costante sarà di tipo CONSTANT_Methodref_info e indicherà (forse indirettamente) A) il nome del metodo chiamato (cioè foo), B) la firma del metodo e C) il nome della classe di tempo di compilazione che il metodo è chiamato on (es. MyClass).

La domanda è: perché è necessario il riferimento al tipo in fase di compilazione (MyClass)? Dal momento che invokevirtual sta per eseguire l'invio dinamico sul tipo di runtime di c, non è ridondante memorizzare il riferimento alla classe di compilazione?

+0

Ciò è dovuto alla verifica. Vedi la mia risposta aggiornata, di seguito. –

risposta

4

Si tratta di prestazioni. Quando calcolando il tipo in fase di compilazione (ovvero: tipo statico), la JVM può calcolare l'indice del metodo richiamato nella tabella delle funzioni virtuali del tipo runtime (ovvero: tipo dinamico). L'uso di questo passo indice 3 diventa semplicemente un accesso a un array che può essere realizzato in un tempo costante. Non è necessario il loop.

Esempio:

class A { 
    void foo() { } 
    void bar() { } 
} 

class B extends A { 
    void foo() { } // Overrides A.foo() 
} 

Per default, A estende Object che definisce questi metodi (metodi final omesse come vengono richiamati tramite invokespecial):

class Object { 
    public int hashCode() { ... } 
    public boolean equals(Object o) { ... } 
    public String toString() { ... } 
    protected void finalize() { ... } 
    protected Object clone() { ... } 
} 

Ora, considerare questa invocazione:

A x = ...; 
x.foo(); 

Calcolando fuori al tipo statico di x è A la JVM può anche capire la lista dei metodi che sono disponibili a questo sito di chiamata: hashCode, equals, toString, finalize, clone, foo, bar. In questo elenco, foo è la sesta voce (è 1st, equals è 2nd, ecc.). Questo calcolo dell'indice viene eseguito una volta, quando JVM carica il file di classe.

Dopo di che, ogni volta che la JVM elabora x.foo() è solo bisogno di accedere al 6 ° voce nell'elenco di metodi che X offre, equivalente a x.getClass().getMethods[5], (che punta a A.foo() se il tipo dinamico di x è A) e richiamare il metodo. Non c'è bisogno di cercare esaustivamente questa gamma di metodi.

Si noti che l'indice del metodo rimane invariato indipendentemente dal tipo dinamico di x. Cioè: anche se x punta a un'istanza di B, il sesto metodo è ancora foo (sebbene questa volta punterà a B.foo()).

Aggiornamento

[Alla luce del vostro aggiornamento]: Hai ragione. Per eseguire un invio di metodo virtuale, tutte le esigenze della JVM sono il nome + firma del metodo (o l'offset all'interno del vtable). Tuttavia, la JVM non esegue le cose alla cieca. Prima controlla che i cassfile caricati in esso siano corretti in un processo chiamato verification (vedere anche here).

La verifica esprime uno dei principi di progettazione della JVM: Non si basa sul compilatore per produrre il codice corretto. Controlla il codice stesso prima di consentirne l'esecuzione. In particolare, il verificatore verifica che ogni metodo virtuale invocato sia effettivamente definito dal tipo statico dell'oggetto destinatario. Ovviamente, il tipo statico del ricevitore è necessario per eseguire tale controllo.

+0

Una piccola correzione è che i metodi 'final' sono invocati con' invokevirtual'. –

1

Questo non è il modo in cui l'ho capito dopo aver letto la documentazione. Penso che i passaggi 2 e 3 siano stati trasposti, il che renderebbe l'intera serie di eventi più logica.

+0

Supponiamo che i passaggi 2 e 3 siano stati trasposti. (È plausibile: nei documenti a cui mi riferivo, la frase "Il metodo con nome è risolto" sembra ambigua). Sei ancora d'accordo sul fatto che la JVM stia facendo qualche tipo di verifica contro il tipo in fase di compilazione, o sospetti? Ho sbagliato anche io? (In particolare, sono tutti i controlli contro il tipo di runtime?) Sono ancora abbastanza sicuro che la JVM sappia che MyClass è il tipo in fase di compilazione associato alla chiamata a foo, anche se sono confuso su quello che fa con quella informazione. – Chris

+0

Ulteriori letture :) 1) L'indice calcolato dagli operandi di invokevirtual viene utilizzato per esaminare il pool costante di runtime di MyClass, che indicherà un riferimento simbolico al metodo. Qualcosa come: MyClass/foo() V. 2) Da quel riferimento simbolico, viene cercata la classe "MyClass", e il metodo "void foo()" viene cercato in quella classe e viene controllata la protezione dell'accesso. 3) Il tipo di runtime della variabile "c" viene controllato per un metodo "void foo()", altrimenti esso ricorre alla gerarchia delle classi fino a quando non ne trova una. Forse fa il primo passo in modo che possa fallire velocemente. Michael E potrebbe avere ragione;) –

+0

BTW, grazie per aver fatto questa domanda - molto istruttivo. –

1

Presumibilmente, il # 1 e il # 2 sono già accaduti dal compilatore. Sospetto che almeno parte dello scopo sia quello di assicurarsi che mantengano ancora la versione della classe nell'ambiente di runtime, che potrebbe essere diversa dalla versione con cui è stato compilato il codice.

Non ho ancora digerito la documentazione invokevirtual per verificare il tuo sommario, quindi Rob Heiser potrebbe avere ragione.

1

Sto indovinando la risposta "B".

il collegamento o di accesso controlli modificatori fatto in # 1 e # 2 sono essenziali per evitare che certe cose cattive accada, e tali controlli devono essere eseguiti in base al tipo in fase di compilazione, piuttosto che il tipo run-time gerarchia. (Si prega di spiegare.)

# 1 è descritto da 5.4.3.3 Method Resolution, il che rende alcuni controlli importanti. Ad esempio, # 1 controlli l'accessibilità del metodo nel tipo in fase di compilazione e può restituire un IllegalAccessError se non è:

... In caso contrario, se il metodo si fa riferimento non è accessibile (§5.4.4) a D, la risoluzione del metodo genera un errore IllegalAccessError. ...

Se è stata selezionata solo il tipo run-time (via # 3), quindi il tipo di run-time potrebbe illegalmente ampliare l'accessibilità del metodo sovrascritto (anche noto come un "male"). È vero che il compilatore dovrebbe prevenire un caso del genere, ma la JVM si sta comunque proteggendo da codice non autorizzato (ad esempio codice malevolo costruito manualmente).

0

Per comprendere completamente questa roba, è necessario comprendere come la risoluzione dei metodi funziona in Java. Se stai cercando una spiegazione approfondita, ti suggerisco di guardare il libro "Inside the Java Virtual Machine". Le seguenti sezioni del capitolo 8, "Il modello Linking", sono disponibili online e sembrano particolarmente rilevanti:

(voci CONSTANT_Methodref_info sono voci nella classe intestazione del file che descrive i metodi chiamati da quella classe.)

Grazie a Itay per l'ispirazione m e per fare il googling richiesto per trovare questo.

Problemi correlati