Como o polimorfismo é implementado dentro da JVM

Uma tradução deste artigo foi preparada especificamente para estudantes do curso Java Developer.





No meu artigo anterior, Tudo sobre sobrecarga de método versus substituição de método , examinamos as regras e diferenças de sobrecarga e substituição de método. Neste artigo, veremos como a sobrecarga e a substituição de métodos são tratadas dentro da JVM.

Por exemplo, faça as aulas do artigo anterior: o pai Mammal (mamífero) e o filho Human (humano).

 public class OverridingInternalExample { private static class Mammal { public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); } } private static class Human extends Mammal { @Override public void speak() { System.out.println("Hello"); } //   speak() public void speak(String language) { if (language.equals("Hindi")) System.out.println("Namaste"); else System.out.println("Hello"); } @Override public String toString() { return "Human Class"; } } //           public static void main(String[] args) { Mammal anyMammal = new Mammal(); anyMammal.speak(); // Output - ohlllalalalalalaoaoaoa // 10: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V Mammal humanMammal = new Human(); humanMammal.speak(); // Output - Hello // 23: invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V Human human = new Human(); human.speak(); // Output - Hello // 36: invokevirtual #7 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:()V human.speak("Hindi"); // Output - Namaste // 42: invokevirtual #9 // Method org/programming/mitra/exercises/OverridingInternalExample$Human.speak:(Ljava/lang/String;)V } } 

Podemos examinar a questão do polimorfismo de dois lados: do "lógico" e do "físico". Vamos primeiro olhar para o lado lógico da questão.

Ponto de vista lógico


Do ponto de vista lógico, no estágio de compilação, o método chamado é considerado relacionado ao tipo de referência. Mas em tempo de execução, o método do objeto referenciado será chamado.

Por exemplo, na linha humanMammal.speak(); o compilador pensa que Mammal.speak() será chamado, pois humanMammal declarado como Mammal . Porém, em tempo de execução, a JVM saberá que humanMammal contém um objeto Human e, na verdade, Human.speak() o método Human.speak() .

É tudo muito simples, desde que permaneçamos em um nível conceitual. Mas como a JVM lida com tudo isso internamente? Como a JVM calcula qual método deve ser chamado?

Também sabemos que métodos sobrecarregados não são chamados polimórficos e são resolvidos em tempo de compilação. Embora às vezes a sobrecarga de método seja chamada de polimorfismo em tempo de compilação ou ligação estática / precoce .

Os métodos substituídos (substituição) são resolvidos no tempo de execução porque o compilador não sabe se existem métodos substituídos no objeto atribuído ao link.

Ponto de vista físico


Nesta seção, tentaremos encontrar evidências "físicas" para todas as declarações acima. Para fazer isso, observe o javap -verbose OverridingInternalExample que podemos obter executando o javap -verbose OverridingInternalExample . O parâmetro -verbose nos permitirá obter um bytecode mais intuitivo correspondente ao nosso programa java.

O comando acima mostrará duas seções do bytecode.

1. O conjunto de constantes . Ele contém quase tudo o que é necessário para executar o programa. Por exemplo, referências de método ( #Methodref ), classes ( #Class ), literais de string ( #String ).



2. O bytecode do programa. Instruções de bytecode executáveis.



Por que a sobrecarga de método é chamada de ligação estática


No exemplo acima, o compilador pensa que o método humanMammal.speak() será chamado da classe Mammal , embora em tempo de execução seja chamado a partir do objeto referenciado em humanMammal - será um objeto da classe Human .

Observando nosso código e o resultado do javap , vemos que diferentes javap código são usados ​​para chamar os métodos humanMammal.speak() , human.speak() e human.speak("Hindi") , pois o compilador pode distingui-los com base na referência de classe .

Portanto, no caso de uma sobrecarga de método, o compilador é capaz de identificar instruções de bytecode e endereços de método em tempo de compilação. É por isso que isso é chamado de ligação estática ou polimorfismo em tempo de compilação.

Por que a substituição de método é chamada de ligação dinâmica


Para chamar os anyMammal.speak() e humanMammal.speak() , o bytecode é o mesmo, pois, do ponto de vista do compilador, os dois métodos são chamados para a classe Mammal :

 invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V 

Portanto, agora a questão é: se as duas chamadas tiverem o mesmo bytecode, como a JVM saberá qual método chamar?

A resposta está oculta no próprio bytecode e na instrução invokevirtual . De acordo com a especificação da JVM (nota do tradutor: referência à especificação da JVM 2.11.8 ) :
A instrução invokevirtual chama o método de instância através do envio no tipo (virtual) do objeto. Esse é o envio normal de métodos na linguagem de programação Java.
A JVM usa a invokevirtual para chamar nos métodos Java equivalentes aos métodos virtuais C ++. No C ++, para substituir um método em outra classe, o método deve ser declarado como virtual. Porém, em Java, por padrão, todos os métodos são virtuais (exceto os métodos final e estático); portanto, na classe filho, podemos substituir qualquer método.

A instrução invokevirtual leva um ponteiro para o método a ser chamado (# 4 é o índice no pool constante).

 invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V 

Mas a referência # 4 refere-se ainda a outro método e classe.

 #4 = Methodref #2.#27 // org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V #2 = Class #25 // org/programming/mitra/exercises/OverridingInternalExample$Mammal #25 = Utf8 org/programming/mitra/exercises/OverridingInternalExample$Mammal #27 = NameAndType #35:#17 // speak:()V #35 = Utf8 speak #17 = Utf8 ()V 

Todos esses links são usados ​​juntos para obter uma referência ao método e à classe na qual o método desejado está localizado. Isso também é mencionado na especificação da JVM ( nota do tradutor: referência à especificação da JVM 2.7 ):
A Java Virtual Machine não requer nenhuma estrutura interna específica de objetos.
Em algumas implementações Java da Oracle Virtual Machine, uma referência a uma instância de classe é uma referência a um manipulador, que consiste em um par de links: um aponta para uma tabela de métodos de objeto e um ponteiro para um objeto Class que representa o tipo do objeto e o outro para a área dados no heap contendo dados do objeto.

Isso significa que cada variável de referência contém dois ponteiros ocultos:

  1. Um ponteiro para uma tabela que contém os métodos do objeto e um ponteiro para o objeto Class , por exemplo, [speak(), speak(String) Class object]
  2. Um ponteiro para a memória no heap alocado para dados do objeto, como valores de campo do objeto.

Mas, novamente, surge a pergunta: como o invokevirtual trabalha com isso? Infelizmente, ninguém pode responder a essa pergunta, porque tudo depende da implementação da JVM e varia de JVM para JVM.

Pelo raciocínio acima, podemos concluir que uma referência a um objeto indiretamente contém um link / ponteiro para uma tabela que contém todas as referências aos métodos desse objeto. Java emprestou esse conceito do C ++. Essa tabela é conhecida por vários nomes, como tabela de método virtual (VMT), tabela de função virtual (vftable), tabela virtual (vtable), tabela de despacho .

Não podemos ter certeza de como a vtable é implementada em Java, porque depende da JVM específica. Mas podemos esperar que a estratégia seja a mesma que em C ++, em que vtable é uma estrutura semelhante a uma matriz que contém nomes de métodos e suas referências. Sempre que a JVM tenta executar um método virtual, solicita seu endereço na vtable.

Para cada classe, existe apenas uma vtable, o que significa que a tabela é única e igual para todos os objetos da classe, semelhante ao objeto Class. Os objetos de classe são discutidos em mais detalhes nos artigos Por que uma classe Java externa não pode ser estática e Por que Java é uma linguagem puramente orientada a objetos ou Por que não .

Portanto, existe apenas uma tabela de tabelas para a classe Object , que contém todos os 11 métodos (se registerNatives não registerNatives levados em consideração) e links correspondentes à sua implementação.



Quando a JVM carrega a classe Mammal na memória, cria um objeto Class para ela e cria uma tabela que contém todos os métodos da tabela de Object com as mesmas referências (já que o Mammal não substitui os métodos de Object ) e adiciona uma nova entrada para o método speak() .



Em seguida, a classe da classe Human entra, e a JVM copia todas as entradas da vtable da classe Mammal para a vtable da classe Human e adiciona uma nova entrada para a versão sobrecarregada de speak(String) .

A JVM sabe que a classe Human substituiu dois métodos: toString() de Object e speak() de Mammal . Agora, para esses métodos, em vez de criar novos registros com links atualizados, a JVM alterará os links para métodos existentes no mesmo índice em que estavam presentes anteriormente e manterá os mesmos nomes de métodos.



A instrução invokevirtual faz com que a JVM processe o valor na referência ao método # 4 não como um endereço, mas como o nome do método a ser procurado na vtable para o objeto atual.
Espero que agora fique mais claro como a JVM usa o pool constante e a tabela de métodos virtuais para determinar qual método chamar.
Você pode encontrar o código de amostra no repositório do Github .

Source: https://habr.com/ru/post/pt467197/


All Articles