As cinco instruções de chamada de método da máquina virtual

De uns tempos para cá a Sun começou a apoiar ativamente o uso de linguagens de tipagem dinâmica na JVM, mostrando que pretende fazer do Java uma plataforma completa, em vez de apenas mais uma linguagem de programação. A próxima versão da máquina virtual traz também inovações que prometem facilitar a vida de quem escreve compiladores para outras linguagens. De quebra, os desenvolvedores que programam nessas linguagens ganham performance suficiente para competir de igual para igual com as implementações na linguagem Java. Hoje vamos ver de perto uma das inovações que tornarão isso possível.

É sabido que o programa javac cria um arquivo binário contendo instruções independentes do sistema operacional em que ele vai rodar. O conteúdo de um arquivo .class é costumeiramente chamado simplesmente de "bytecode", e é carregado pela máquina virtual na primeira vez em que a classe é utilizada. Existem programas como Jar Jar Links e a biblioteca Asm que manipulam bytecode diretamente, até mesmo em tempo de execução.

Nesse ambiente de registros, endereços e pilhas, uma chamada de método pode ser representada de quatro formas:

·         invokevirtual é a instrução que executa uma chamada virtual de método. Ela recebe uma assinatura (por exemplo java.lang.StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;) e procura o código a ser executado na classe do objeto em tempo de execução, subindo para as superclasses se necessário.

·         invokeinterface funciona da mesma forma, porém utiliza a assinatura de uma interface como ponto de partida na busca.

·         invokestatic invoca um método estático. Também recebe uma assinatura como parâmetro, por exemplo java.lang.Math.min (JJ)J. Para o invokestatic não é necessário fazer uma busca, já que em Java não é possível sobrescrever métodos estáticos. Imagine como seria complicado e ineficiente implementar essa busca.

·         invokespecial é utilizada quando o método a ser chamado depende de processamento especial. Construtores são chamados com esta instrução, assim como código de inicialização de classes e chamadas a métodos da superclasse (super.metodo()).

A JVM não faz busca ao executar invokespecial, ela utiliza a classe declarada junto com a instrução. Por exemplo, o construtor sem parâmetros da classe java.util.Vector chama o construtor que recebe um argumento inteiro:

public Vector() {
   this(10);
}

O bytecode deste construtor pode ser representado da seguinte forma:

tiagosilveiracod04.JPG

Nesse caso, não é necessário fazer uma busca para descobrir qual é o método certo a ser
chamado. Como também não é necessário fazer uma busca ao chamar um método privado,
os compiladores também utilizam invokespecial para esses métodos.

Nota: a chamada da linha 3 contém a assinatura do método como as máquina virtual usa. Daqui para frente, eu usarei uma notação derivada do UML, indicando o valor de retorno após dois-pontos:

tiagosilveiracod01.JPG

Identificando tipos

Para garantir a tipagem forte da linguagem cooperam dois mecanismos: o compilador deve verificar que cada chamada de método é enviada a um objeto que realmente implementa aquele método; além disso, em tempo de execução, um verificador processa o bytecode sendo executado para garantir que as instruções são coerentes. Por rodar durante o carregamento de código, o verificador deve ser extremamente eficiente, rápido e utilizar pouca memória. A Sun também está fazendo esforços nesse sentido, que não serão vistos antes da versão 1.7.

O comitê que procurava um auxílio para a compilação de linguagens de tipagem dinâmica para a JVM – como Groovy, Jython, JRuby, ECMAStript e outras – propôs na JSR-292 uma nova instrução de chamada de métodos batizada de invokedynamic. Esta é uma solução parcial para o problema de invocação dinâmica de métodos que não compromete o código existente e já traz muitos benefícios para as implementações existentes.

Para ter uma idéia do que as linguagens de tipagem dinâmica precisam enfrentar, considere o seguinte código:

public double area(java.awt.Rectangle rect) {
   return rect.getWidth() * rect.getHeight();
}

Em Java, o bytecode da chamada ao método getHeight pode ser representado como:

invokevirtual java.awt.Rectangle.getHeight();

Em linguagens dinâmicas, porém, é muito mais comum ver código assim:

public area(rect) {
   return rect.getWidth() * rect.getHeight();
}

E agora? O compilador não tem como saber nem o tipo da variável rect, e portanto não tem
nem como saber o valor de retorno do método. Tudo que ele sabe é o nome.

invokevirtual Tipo_desconhecido.getHeight():Tipo_de_retorno_desconhecido

Nem o compilador nem o verificador da JVM têm como saber de que tipos estamos falando, nem se é seguro usar o valor de retorno na multiplicação que vem a seguir. Afinal, se a VM apenas empilhar o resultado e chamar a operação de multiplicação, pode acontecer do valor de retorno ser o endereço de um objeto que será usado como inteiro. E todo esforço para prover tipagem forte e a proteção que o Java dá no acesso à memória viram pó, e voltamos aos dias negros da programação em C++.

Atualmente, as linguagens dinâmicas na JVM usam reflexão (classe no pacote java.lang.reflection) e casts dinâmicos que garantem a famosa ClassCastException ao tentar usar valores indevidamente. Porém, isso gera muitas instruções, faz muitas buscas e consome mais memória. A novidade é oferecer essa funcionalidade diretamente com apenas uma instrução:

tiagosilveiracod02.JPG

Isso permite várias otimizações na implementação da VM, além de utilizar menos memória e se beneficiar do algoritmo já existente para o invokevirtual. Mas ainda não é tudo.

Argumentos também são tipados

Há mais um problema que a JSR-292 quer tratar, que é a sobrecarga de métodos. Considere a seguinte classe Java:

class Player {
   void play(Chess board);
   void play(Poker table);
}

Quando um programador utilizar a classe Player numa linguagem de tipagem dinâmica, provavelmente ele vai fazer:

def player = ...;
def game = ...;
while (!game.isFinished()) {
   player.play(game);
}

Ao compilar o código, é necessário saber se estamos jogando xadrez ou pôquer. Mais uma vez, é possível encontrar o método certo usando reflexão, e as implementações atuais seguem essa linha. Mas a JVM tem uma implementação muito mais econômica e eficiente da busca de métodos – aquela usada com o invokevirtual e o invokeinterface. Portanto, a chamada acima poderá ser implementada com apenas uma instrução:

tiagosilveiracod03.JPG

A máquina virtual procurará, em tempo de execução, o método adequado para executar. Porém, pode ser que tal método não exista! E agora, teremos mais exceções malucas para lidar?

A solução para esse problema é antiga, já foi dada pelo Smalltalk. No Smalltalk-80, quando uma mensagem 2 M é enviada ao objeto O e este não implementa um método para aquela mensagem, a máquina virtual (do smalltalk) envia ao objeto uma mensagem doesNotUnderstand contendo uma descrição da mensagem original, algumas informações sobre o contexto em que ela foi gerada e os argumentos usados na chamada.

A idéia é criar um mecanismo similar ao doesNotUnderstand para o invokedynamic. A implementação padrão deve ficar na classe java.lang.Object e provavelmente lançará uma exceção, mas qualquer classe poderá sobrescrever esse método. Uma discussão sobre o equivalente do doesNotUnderstand em Groovy pode ser lida aqui e é altamente recomendável.

Para saber mais, recomendo ler as especificações da JVM e os slides da apresentação por Gilad Bracha na JAOO 2005.

Prometo a vocês que o próximo artigo será bem curtinho! Até mais.

Notas
tiagosilveirafig01.JPG

2- Na comunidade Java, não se costuma distinguir tanto um método de uma chamada de método, mas na comunidade Smalltalk a chamada de método chama-se mensagem e ela é enviada ao objeto. Se eu estivesse dirigindo-me a eles, este artigo provavelmente se chamaria "as cinco instruções de envio de mensagem da máquina virtual". O mundo mudou mesmo muito pouco desde os anos oitenta.