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