DEV Community

João Victor Martins
João Victor Martins

Posted on

11 2

[PT-BR] Falando sobre JIT Compiler

A JVM (Java Virtual Machine) é uma peça fundamental para a plataforma Java. Graças a este recurso, é possível escrever o código e executá-lo em diferentes sistemas operacionais. A diferença dessa abordagem em relação ao processo realizado por outras linguagens, por exemplo o C++, é que não é necessário compilar o código para diferentes S.O's. Apesar de trazer a vantagem citada anteriormente, a abordagem traz alguns trade-off's, entre eles, o impacto na performance. Enquanto no C++ o código é compilado e executado nativamente (binário), no Java é necessário ser compilado para bytecode (linguagem que a JVM entende), interpretado (binário) e executado nativamente. Parece ser pouca a diferença entre as duas linguagens, sendo necessário, no Java, realizar apenas um passo a mais, mas este passo é crucial na performance. Uma das ações tomadas pelos engenheiros e desenvolvedores da plataforma, foi criar um mecanismo que auxilia e provê mais performance na interpretação. Esse mecanismo é conhecido como JIT Compiler ou Just in Time Compiler.

JIT Compiler

Para entender o JIT, é necessário entender, mesmo que em linhas gerais, o que a JVM faz. Seus objetivos são interpretar os bytecodes e gerenciar a memória da aplicação. Por enquanto o foco será no primeiro objetivo.

Alt Text

A imagem acima mostra o processo de compilação do código Java para bytecode, o carregamento para a JVM e a interpretação para binário. O problema destes passos é que, em uma aplicação, o mesmo método ou bloco de código será chamado muitas vezes durante o ciclo de vida da aplicação, sendo necessária a interpretação em cada uma das vezes. Como pode-se imaginar, o impacto na performance cresce exponencialmente e pensando nisso, os engenheiros da plataforma tiveram a ideia do JIT Compiler. O JIT é um processo que ocorre de forma paralela à interpretação (em outra thread) e seu objetivo é transformar (compilar) os trechos de códigos mais executados em linguagem nativa (binário). A diferença da interpretação padrão, citada anteriormente, é que conforme o processo vai sendo executado, vai havendo melhora na compilação até chegar no grau máximo de otimização. Chegando neste grau, a JVM não precisa interpretar mais aquele trecho de código.

Alt Text

Praticando

O processo de compilação em runtime, citado anteriormente, pode ser visto usando a flag -XX:+PrintCompilation no momento da execução do projeto. Para exemplificar, será utilizado o código abaixo:

import java.util.*;
public class Principal {
    public static void main(String[] args) {
        Integer numeroMaximo = Integer.parseInt(args[0]);
        Principal principal = new Principal();
        principal.guardarNumerosPares(numeroMaximo);
    }
    private void guardarNumerosPares(Integer numeroMaximo){
        int numero = 0;
        List<Integer> numerosPares = new ArrayList<>();
        while(numero <= numeroMaximo) {
            if(validarSeEPar(numero)) numerosPares.add(numero);
            numero++;
        }
    }
    private Boolean validarSeEPar(Integer numero) {
        if (numero % 2 == 0) return true;
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

A ideia do código é bastante simples. O usuário fornece um número como argumento e o programa valida quais são os números pares no intervalo de 0 até o número recebido. Para executá-lo com a flag, é necessário o comando java -XX:+PrintCompilation Principal 10. O resultado será:

24    1       3       java.lang.String::isLatin1 (19 bytes)
25    3       3       java.lang.StringLatin1::hashCode (42 bytes)
26    6       3       java.util.ImmutableCollections$SetN::probe (56 bytes)
28    9       3       java.lang.StringLatin1::equals (36 bytes)
28   10       3       java.util.ImmutableCollections$SetN::hashCode (46 bytes)
29    4       3       java.lang.Object::<init> (1 bytes)
29    2       3       java.lang.String::hashCode (60 bytes)
30    5       3       java.lang.Math::floorMod (20 bytes)
30    8       3       java.util.Set::of (4 bytes)
31   11       3       java.util.Objects::requireNonNull (14 bytes)
31   18       4       java.lang.Object::<init> (1 bytes)
31   13       3       java.lang.String::coder (15 bytes)
32    4       3       java.lang.Object::<init> (1 bytes)   made not entrant
33   22       3       java.util.ImmutableCollections$SetN$SetNIterator::next (47 bytes)
33   26     n 0       jdk.internal.misc.Unsafe::getReferenceVolatile (native)
33   20       3       java.util.ImmutableCollections$MapN::probe (60 bytes)
34   29   !   3       java.util.concurrent.ConcurrentHashMap::putVal (432 bytes)
36   16       3       java.util.ImmutableCollections$SetN$SetNIterator::nextIndex (56 bytes)
36   32       3       java.util.HashMap::hash (20 bytes)
37   35       3       java.util.HashMap::putVal (300 bytes)
38   23       3       java.util.ImmutableCollections$SetN$SetNIterator::hasNext (13 bytes)
38   44       4       java.lang.String::hashCode (60 bytes)
38    7       3       java.lang.String::equals (50 bytes)
39   36       3       java.util.HashMap::newNode (13 bytes)
42    2       3       java.lang.String::hashCode (60 bytes)   made not entrant
43   46       4       java.util.ImmutableCollections$SetN$SetNIterator::nextIndex (56 bytes)
44   14       1       java.lang.module.ModuleReference::descriptor (5 bytes)
44   16       3       java.util.ImmutableCollections$SetN$SetNIterator::nextIndex (56 bytes)   made not entrant
45   12       1       java.lang.module.ModuleDescriptor::name (5 bytes)
46   21       1       java.lang.module.ResolvedModule::reference (5 bytes)
46   44       4       java.lang.String::hashCode (60 bytes)   made not entrant
47   33       1       java.lang.module.ModuleDescriptor$Exports::source (5 bytes)
47   31       1       java.util.ImmutableCollections$SetN::size (5 bytes)
Enter fullscreen mode Exit fullscreen mode

Percebe-se que as informações obtidas são divididas por colunas e o objetivo é entender cada uma delas.

  1. O tempo de compilação em milissegundos desde o startup da máquina virtual
  2. Ordem de compilação
  3. Provê informações sobre a compilação. Ex: "!" significa que houve uma exceção, "n" significa que é um método nativo.
  4. Grau da compilação. Os números vão de 0 a 4. 0 significa que não houve compilação, apenas interpretação e 4 que houve compilação e a compilação obteve a máxima otimização.
  5. Full qualified name da classe e seu método que passou pela compilação.

Os métodos da aplicação não constam na lista dos métodos compilados, isso acontece pois o tempo de execução do projeto não foi o suficiente para necessitar da compilação otimizada. Aumentando o número para 50000 este comportamento será alterado.

java -XX:+PrintCompilation Principal 50000

//Resto das informações omitidas para facilitar a visualização
68   54       2       Principal::validarSeEPar (19 bytes)
68   52       2       java.lang.Integer::<init> (10 bytes)   made not entrant
69   60       4       java.lang.Integer::valueOf (32 bytes)
69   50       1       java.lang.Boolean::booleanValue (5 bytes)
70   55       2       java.lang.Boolean::valueOf (14 bytes)
70   53       2       java.lang.Integer::valueOf (32 bytes)   made not entrant
71   61       4       Principal::validarSeEPar (19 bytes)
73   56       2       java.util.ArrayList::add (25 bytes)
75   54       2       Principal::validarSeEPar (19 bytes)   made not entrant
76   57       2       java.util.ArrayList::add (23 bytes)
76   41       3       java.util.HashMap$Node::<init> (26 bytes)
77   62       4       java.util.ArrayList::add (25 bytes)
77   14       1       java.lang.module.ModuleReference::descriptor (5 bytes)
Enter fullscreen mode Exit fullscreen mode

Aumentando o número fornecido para o sistema, o método validarSeEPar passa a ser requisitado mais vezes, sendo necessário uma otimização por parte da JVM. É possível verificar já na primeira linha que o método foi compilado com grau 2 e com o passar do tempo houve uma melhora na compilação, indo para grau 4 na linha 7.

Por conta da execução paralela do JIT, a aplicação pode sofrer com performance momentaneamente, mas a longo prazo, quanto mais a aplicação vai sendo executada, mais a execução vai ficando fluída.

Concluindo ...

A ideia era mostrar sobre como os engenheiros do Java pensaram em melhorar a performance das aplicações e diminuir o trade-off das linguagens compiladas/interpretadas. Essa é só uma pequena parte do que a JVM faz. Ainda há detalhes de como o JIT guarda os métodos, compilados no grau 4, na memória, entre outras questões que ficarão para posts futuros. Dúvidas e sugestões são sempre bem-vindas. Até o próximo!!

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (2)

Collapse
 
eduardoklosowski profile image
Eduardo Klosowski

Esse assunto do JIT é bem interessante, pois ao se compilar muitas vezes é necessário optar por uma heurística para otimizar o código, ao fazer isso em tempo de execução pode-se testar diferentes heurísticas com os dados de execuções reais para escolher a melhor heurística, diferente de C, por exemplo, que tem que presumir qual seria a melhor otimização para aquele código, e se essa não for uma boa heurística para aquele código ter uma performance pior do que poderia ser obtida.

Collapse
 
j_a_o_v_c_t_r profile image
João Victor Martins

Eduardo, também acho interessante demais!! Na verdade, todo processo da JVM é muito interessante.

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up