[PT-BR] Falando sobre JIT Compiler
Tags: java, jvm, ptbr
Published: 2020-11-17 12:27:44

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](https://dev-to-uploads.s3.amazonaws.com/i/3s4hsajb24fe5tav31nr.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/7wmhgmlls7s0r3e0k5eq.png)

# 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:

```java
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;
    }
}
```     
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)
```

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)
```
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!!