DEV Community

João Victor Martins
João Victor Martins

Posted on

[PT-BR] Code Cache

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

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

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
lucasscharf profile image
Aleatório

Muito bom o artigo

Collapse
 
j_a_o_v_c_t_r profile image
João Victor Martins

Obrigado, Aleatório!!