2010-04-01 14 views
11

considerar esta clase Java simple:¿Por qué Java necesita invokevirtual para resolver la clase de tiempo de compilación del método llamado?

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

quiero hablar de lo que sucede en la línea de c.foo().

original, engañosa Pregunta

Nota: No todo esto realmente sucede con cada código de operación invokevirtual individuo. Sugerencia: si desea comprender la invocación del método Java, ¡no lea solo la documentación de invokevirtual!

A nivel de código de bytes, la carne de c.foo() será el código de operación invokevirtual, y, de acuerdo con the documentation for invokevirtual, más o menos sucederá lo siguiente:

  1. Busque el método foo definido en tiempo de compilación clase MyClass. (Esto implica la primera resolución de MyClass).
  2. Realice algunas comprobaciones, que incluyen: Verifique que c no sea un método de inicialización, y verifique que la invocación de MyClass.foo no viole ningún modificador protegido.
  3. Averigua qué método llamar realmente. En particular, busque c runtime tipo de tiempo de ejecución. Si ese tipo tiene foo(), llama a ese método y regresa. Si no, busca la superclase del tipo de tiempo de ejecución de c; si ese tipo tiene foo, llama a ese método y regresa. Si no, busca la superclase de la superclase del tipo de tiempo de ejecución de c; si ese tipo tiene foo, llama a ese método y regresa. Etc .. Si no se puede encontrar un método adecuado, entonces hay un error.

El paso 3 solo parece adecuado para averiguar qué método utilizar para llamar y verificar que dicho método tenga los tipos de argumento/retorno correctos. Entonces mi pregunta es por qué el paso # 1 se realiza en primer lugar. Las posibles respuestas parecen ser:

  • No tiene suficiente información para realizar el paso 3 hasta que se complete el paso # 1. (Esto parece inverosímil a primera vista, así que explíquelo.)
  • Las comprobaciones del modificador de acceso o enlace realizadas en los números 1 y 2 son esenciales para evitar que ocurran ciertas cosas malas, y esas comprobaciones deben realizarse en función del tiempo de compilación, en lugar de la jerarquía de tipo de tiempo de ejecución. (Por favor explique.)

revisado Pregunta

El núcleo de la salida del compilador javac para la c.foo línea() será una instrucción como esta:

invokevirtual i 

donde i es un índice del grupo de constante de tiempo de ejecución de MyClass. Esa entrada de grupo constante será del tipo CONSTANT_Methodref_info e indicará (tal vez indirectamente) A) el nombre del método llamado (es decir, foo), B) la firma del método, y C) el nombre de la clase de tiempo de compilación que se llama el método encendido (es decir, MyClass).

La pregunta es, ¿por qué es necesaria la referencia al tipo de tiempo de compilación (MyClass)? Dado que invokevirtual va a hacer el despacho dinámico en el tipo de tiempo de ejecución de c, ¿no es redundante almacenar la referencia a la clase en tiempo de compilación?

+0

Esto es debido a la verificación. Ver mi respuesta actualizada, a continuación. –

Respuesta

4

Se trata de rendimiento. Cuando al calcular el tipo de tiempo de compilación (también conocido como: tipo estático), la JVM puede calcular el índice del método invocado en la tabla de funciones virtuales del tipo de tiempo de ejecución (también conocido como tipo dinámico). Usar este paso de índice 3 simplemente se convierte en un acceso a una matriz que se puede lograr en tiempo constante. No se requiere bucle.

Ejemplo:

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

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

Por defecto, A extiende Object que define estos métodos (métodos finales omitidos, ya que se invocan a través de invokespecial):

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

Ahora, considere esta invocación:

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

Al averiguar th en el tipo estático de x es A la JVM también puede averiguar la lista de métodos que están disponibles en este sitio llamado: hashCode, equals, toString, finalize, clone, foo, bar. En esta lista, foo es la 6ta entrada (hashCode es 1 °, equals es 2 °, etc.). Este cálculo del índice se realiza una vez, cuando la JVM carga el archivo de clase.

Después de eso, cada vez que la JVM procesa x.foo() es sólo tiene que acceder a la sexta entrada en la lista de métodos que X ofrece, lo que equivale a x.getClass().getMethods[5], (que apunta a A.foo() si el tipo dinámico de x es A) e invocar ese método. No es necesario buscar exhaustivamente esta matriz de métodos.

Tenga en cuenta que el índice del método sigue siendo el mismo independientemente del tipo dinámico de x. Es decir: incluso si x apunta a una instancia de B, el sexto método sigue siendo foo (aunque esta vez apuntará a B.foo()).

actualización

[A la luz de su actualización]: Tienes razón. Para realizar un despacho de métodos virtuales, todas las necesidades de JVM son el nombre + firma del método (o el desplazamiento dentro de la tabla). Sin embargo, la JVM no ejecuta las cosas a ciegas. Primero verifica que los archivos cassfiles cargados en él sean correctos en un proceso llamado verification (vea también here).

La verificación expresa uno de los principios de diseño de la JVM: No se basa en el compilador para producir el código correcto. Comprueba el código antes de que se pueda ejecutar. En particular, el verificador verifica que cada método virtual invocado esté realmente definido por el tipo estático del objeto receptor. Obviamente, el tipo estático del receptor es necesario para realizar dicho control.

+0

Una pequeña corrección es que los métodos 'finales' se invocan con' invokevirtual'. –

1

No es la forma en que lo entiendo después de leer la documentación. Creo que tienes los pasos 2 y 3 transpuestos, lo que haría que toda la serie de eventos sea más lógica.

+0

Supongamos que tengo los pasos 2 y 3 transpuestos. (Es plausible. En los documentos a los que me refería, la oración "El método nombrado se resolvió" parece ambiguo.) ¿Aún acepta que la JVM está realizando algún tipo de comprobación con respecto al tipo de tiempo de compilación, o sospecha? ¿También estoy equivocado? (En particular, ¿todos los controles están en contra del tipo de tiempo de ejecución?). Todavía estoy bastante seguro de que la JVM sabe que MyClass es el tipo de tiempo de compilación asociado con la llamada a foo, incluso si estoy confundido con lo que hace con esa información. – Chris

+0

Después de leer :) 1) El índice calculado a partir de los operandos virtuales se utiliza para buscar en el conjunto de constantes de tiempo de ejecución de MyClass, que apuntará a una referencia simbólica al método. Algo como: MyClass/foo() V. 2) A partir de esa referencia simbólica, se busca la clase "MyClass" y se busca el método "void foo()" en esa clase y se comprueba la protección de acceso. 3) Se comprueba el tipo de tiempo de ejecución de la variable "c" para un método "void foo()", si no se vuelve a generar la jerarquía de clases hasta que encuentre una. Tal vez sea el primer paso para que pueda fallar rápidamente. Michael E podría estar en lo cierto;) –

+0

Por cierto, gracias por hacer esta pregunta, muy educativo. –

1

Presumiblemente, # 1 y # 2 ya han pasado por el compilador. Sospecho que al menos parte del propósito es asegurarse de que aún se mantienen con la versión de la clase en el entorno de tiempo de ejecución, que puede ser diferente de la versión contra la que se compiló el código.

No he digerido la documentación invokevirtual para verificar su resumen, por lo que Rob Heiser podría estar en lo cierto.

1

Supongo que responderé "B".

la vinculación o acceder a los controles modificadores hecho en el # 1 y # 2 son esenciales para prevenir ciertos que sucedan cosas malas, y esos controles deben realizarse basándose en el tipo en tiempo de compilación, en lugar del tipo de tiempo de ejecución jerarquía. (Por favor, explique)

# 1 está descrito por 5.4.3.3 Method Resolution, lo que hace algunas comprobaciones importantes. Por ejemplo, # 1 comprueba la accesibilidad del método en el tipo en tiempo de compilación y puede devolver un IllegalAccessError si no lo es:

... De lo contrario, si el método de referencia no es accesible (§5.4.4) a D, la resolución de método arroja un IllegalAccessError. ...

Si sólo marcó el tipo de tiempo de ejecución (a través # 3), entonces el tipo de tiempo de ejecución podría ampliar ilegalmente la accesibilidad del método reemplazado (también conocido como "algo malo"). Es cierto que el compilador debería evitar este caso, pero la JVM se está protegiendo a sí misma del código falso (por ejemplo, código malévolo construido manualmente).

0

Para entender completamente esto, debe comprender cómo funciona la resolución de métodos en Java. Si está buscando una explicación en profundidad, le sugiero consultar el libro "Dentro de la máquina virtual Java". Las siguientes secciones del Capítulo 8, "El modelo de enlace", están disponibles en línea y parecen particularmente relevantes:

(entradas CONSTANT_Methodref_info son entradas en la clase encabezado de archivo que describe los métodos llamados por esa clase.)

Gracias a Itay por inspirar m e para hacer el Google necesario para encontrar esto.

Cuestiones relacionadas