[PT-BR] Code Cache
Published: 2020-12-02 14:00:02

No último post falamos sobre o funcionamento do *JIT Compiler*. Esse processo é necessário para uma melhor interpretação do *bytecode* para código nativo. O que não falamos é que quando o código é compilado para grau 4 (máxima otimização), o binário pode ser "guardado" na memória, em um espaço denominado *code cache*. 

A ideia deste post é mostrar algumas características do *code cache* e como realizar *tunnings* para melhorar a performance da aplicação.

# Recapitulando

Quando o código abaixo foi executado, foi possível extrair algumas informações da compilação usando a *flag* `-XX:+PrintCompilation`

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

O comando usado foi o `java -XX:+PrintCompilation Principal 50000` e o resultado foi o seguinte:

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

No trecho acima, percebemos alguns métodos com grau 4, ou seja, o grau máximo da otimização, porém o código nativo não esta "guardado" no *code cache* ainda. Como é que sabemos? Bom, o método que tem o binário salvo no *code cache* traz consigo uma informação adicional na terceira coluna, que é o carácter "%". Para garantir esta visualização, o código será executado com um número maior do que os exemplos anteriores. Usando o comando `java -XX:+PrintCompilation Principal 10000000` obtemos o resultado esperado:

``` 
//Resto das informações omitidas para facilitar a visualização
277  286   !   4       java.nio.DirectByteBuffer::get (28 bytes)
277  179 %     4       Principal::guardarNumerosPares @ 10 (50 bytes)
279  202   !   3       java.nio.DirectByteBuffer::get (28 bytes)   made not entrant
279  166       4       java.lang.Integer::valueOf (32 bytes)
305  322       3       java.lang.ClassLoader::loadClass (7 bytes)
305  323       3       jdk.internal.loader.BuiltinClassLoader::loadClass (22 bytes)
```

Podemos observar que o binário do método `guardarNUmeros` foi salvo no *code cache* e agora faz todo sentido a **JVM** não precisar compilá-lo novamente, pois sempre que o mesmo for chamado, será usado o binário salvo na memória.

# Mais sobre o code cache

Agora que vimos em qual momento o binário é salvo no *code cache*, precisamos pensar em alguns outros cenários. Ao longo da vida de uma aplicação, muitos métodos serão compilados e consequentemente serão guardados no *code cache*. E se esse espaço "acabar"? Qual será o comportamento? Esse cenário fará com que o método já salvo no *code cache* seja retirado para que o novo método possa ser salvo. Quando o método retirado é chamado novamente, irá ocorrer o mesmo cenário descrito anteriormente e ele será salvo no lugar de outro binário, prejudicando, e muito, a performance da aplicação. Pensando nesses cenários, que serão comuns à muitas aplicações, é possível *tunnar* o code cache, através do uso das *flags*. 

# Tunnando o code cache

Quando o cenário que descrevemos anteriormente ocorrer, a **JVM** enviará o seguinte alerta

> VM warning: CodeCache is full. Compiler has been disabled. 

Isso não significa que a aplicação ira parar de executar, apenas que não estará rodando de forma otimizada. Para verificar o tamanho do *code cache*, podemos utilizar a *flag* `-XX:+PrintCodeCache`. Executando o comando `java -XX:+PrintCodeCache Principal 5000` obtemos o resultado abaixo: 

```
CodeHeap 'non-profiled nmethods': size=120032Kb used=38Kb max_used=38Kb free=119993Kb
 bounds [0x00007f376befb000, 0x00007f376c16b000, 0x00007f3773433000]
```

No exemplo anterior, a aplicação não correria o risco do cenário citado antes, pois há muito espaço livre. Porém, pode-se pensar na situação em que o espaço livre já não correspondia a um número alto de disponibilidade, sendo necessário aumentar o tamanho do *code cache*

> O espaço disponível para aumentar o *code cache* dependerá da versão da JVM que está sendo usada. Se a versão for 7 ou anterior, o máximo disponível será 32 mb para a VM 32 bits e 48 mb para a VM 64 bits. Para a JVM 8 ou posterior, o máximo disponível é de 240 mb. 

Para informar o espaço que será reservado para o *code cache*, usamos a *flag* `-XX:ReservedCodeCacheSize=XXmb`. Para colocar 240 MB, basta que o comando a seguir seja executado: `java -XX:ReservedCodeCacheSize=240000k -XX:+PrintCodeCache Principal 5000` e o resultado será:

```
CodeCache: size=240000Kb used=1316Kb max_used=1316Kb free=238683Kb
 bounds [0x00007f6ba49d3000, 0x00007f6ba4c43000, 0x00007f6bb3433000]
 total_blobs=456 nmethods=173 adapters=195
 compilation: enabled
              stopped_count=0, restarted_count=0
 full_count=0
```

Podemos perceber que foram feitas as mudanças no espaço disponível para o *code cache*, e a aplicação que estaria sofrendo com performance seria executada de maneira mais otimizada após o *tunning*. 

#Concluindo 
O *code cache* é um forte aliado do *JIT Compiler*, complementando o trabalho de compilação em tempo de execução. A ideia do post foi mostrar algumas características deste espaço de memória reservado para guardar os binários e uma maneira de otimizá-lo. Para dúvidas, críticas e/ou sugestões, estou sempre à disposição. Até a próxima.