Máquina Virtual Java - O Compilador JIT

Neste capítulo, aprenderemos sobre o compilador JIT e a diferença entre linguagens compiladas e interpretadas.

Linguagens compiladas vs. interpretadas

Linguagens como C, C ++ e FORTRAN são linguagens compiladas. Seu código é entregue como código binário direcionado à máquina subjacente. Isso significa que o código de alto nível é compilado em código binário de uma vez por um compilador estático escrito especificamente para a arquitetura subjacente. O binário produzido não será executado em nenhuma outra arquitetura.

Por outro lado, linguagens interpretadas como Python e Perl podem rodar em qualquer máquina, desde que tenham um interpretador válido. Ele examina linha por linha o código de alto nível, convertendo-o em código binário.

O código interpretado é normalmente mais lento do que o código compilado. Por exemplo, considere um loop. Um interpretado converterá o código correspondente para cada iteração do loop. Por outro lado, um código compilado tornará a tradução apenas uma. Além disso, como os intérpretes veem apenas uma linha de cada vez, eles são incapazes de executar qualquer código significativo, como alterar a ordem de execução de instruções como compiladores.

Veremos um exemplo dessa otimização abaixo -

Adding two numbers stored in memory. Visto que acessar a memória pode consumir vários ciclos de CPU, um bom compilador emitirá instruções para buscar os dados da memória e executar a adição apenas quando os dados estiverem disponíveis. Não vai esperar e, entretanto, executa outras instruções. Por outro lado, nenhuma otimização seria possível durante a interpretação, uma vez que o intérprete não tem conhecimento de todo o código em um determinado momento.

Mas então, as linguagens interpretadas podem ser executadas em qualquer máquina que tenha um interpretador válido dessa linguagem.

O Java é compilado ou interpretado?

Java tentou encontrar um meio-termo. Uma vez que a JVM fica entre o compilador javac e o hardware subjacente, o compilador javac (ou qualquer outro compilador) compila o código Java no Bytecode, que é compreendido por uma JVM específica da plataforma. A JVM então compila o Bytecode em binário usando a compilação JIT (Just-in-time), conforme o código é executado.

HotSpots

Em um programa típico, há apenas uma pequena seção de código que é executada com frequência e, freqüentemente, é esse código que afeta significativamente o desempenho de todo o aplicativo. Essas seções de código são chamadasHotSpots.

Se alguma seção do código for executada apenas uma vez, compilá-la seria uma perda de esforço e seria mais rápido interpretar o Bytecode. Mas se a seção for uma seção ativa e for executada várias vezes, a JVM a compilará. Por exemplo, se um método é chamado várias vezes, os ciclos extras que seriam necessários para compilar o código seriam compensados ​​pelo binário mais rápido gerado.

Além disso, quanto mais a JVM executa um determinado método ou um loop, mais informações ela reúne para fazer diversas otimizações para que um binário mais rápido seja gerado.

Vamos considerar o seguinte código -

for(int i = 0 ; I <= 100; i++) {
   System.out.println(obj1.equals(obj2)); //two objects
}

Se este código for interpretado, o interpretador irá deduzir para cada iteração que classes de obj1. Isso ocorre porque cada classe em Java possui um método .equals (), que é estendido da classe Object e pode ser substituído. Portanto, mesmo que obj1 seja uma string para cada iteração, a dedução ainda será feita.

Por outro lado, o que realmente aconteceria é que a JVM notaria que, para cada iteração, obj1 é da classe String e, portanto, geraria o código correspondente ao método .equals () da classe String diretamente. Portanto, nenhuma pesquisa será necessária e o código compilado será executado mais rapidamente.

Esse tipo de comportamento só é possível quando a JVM sabe como o código se comporta. Portanto, ele espera antes de compilar certas seções do código.

Abaixo está outro exemplo -

int sum = 7;
for(int i = 0 ; i <= 100; i++) {
   sum += i;
}

Um interpretador, para cada loop, busca o valor de 'soma' da memória, adiciona 'I' a ele e o armazena de volta na memória. O acesso à memória é uma operação cara e normalmente leva vários ciclos de CPU. Como esse código é executado várias vezes, ele é um HotSpot. O JIT irá compilar este código e fazer a seguinte otimização.

Uma cópia local de 'sum' seria armazenada em um registro, específico para um determinado segmento. Todas as operações seriam feitas para o valor no registro e quando o loop fosse concluído, o valor seria escrito de volta na memória.

E se outros threads acessarem a variável também? Como as atualizações estão sendo feitas em uma cópia local da variável por algum outro encadeamento, eles veriam um valor obsoleto. A sincronização de threads é necessária em tais casos. Uma primitiva de sincronização muito básica seria declarar 'soma' como volátil. Agora, antes de acessar uma variável, um thread iria liberar seus registradores locais e buscar o valor da memória. Após acessá-lo, o valor é imediatamente gravado na memória.

Abaixo estão algumas otimizações gerais feitas pelos compiladores JIT -

  • Método inlining
  • Eliminação de código morto
  • Heurísticas para otimizar sites de chamadas
  • Dobra constante