DEV Community

João Victor Martins
João Victor Martins

Posted on

5 1

[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.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

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

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More