Coleta de Lixo Detalhada em Java

Coleta de Lixo Detalhada em Java

*1. Visão geral *

Neste tutorial,* veremos como ativar a coleta de lixo detalhada em um aplicativo Java *. Começaremos apresentando o que é a coleta de lixo detalhada e por que ela pode ser útil.

A seguir, veremos vários exemplos diferentes e aprenderemos sobre as diferentes opções de configuração disponíveis. Além disso, também focaremos em como interpretar a saída de nossos logs detalhados.

Para saber mais sobre o Garbage Collection (GC) e as diferentes implementações disponíveis, consulte nosso artigo em https://www..com/jvm-garbage-collectors [Java Garbage Collectors].

*2. Breve introdução à coleta de lixo verboso *

*É necessário ativar o log detalhado da coleta de lixo ao ajustar e depurar muitos problemas* , principalmente problemas de memória. De fato, alguns argumentam que, para monitorar estritamente a integridade de nossos aplicativos, devemos sempre monitorar o desempenho da Coleta de Lixo da JVM.

Como veremos, o log do GC é uma ferramenta muito importante para revelar possíveis melhorias na pilha e na configuração do GC de nosso aplicativo. Para cada GC acontecendo, o log do GC fornece dados exatos sobre seus resultados e duração.

Com o tempo, a análise dessas informações pode nos ajudar a entender melhor o comportamento ou o nosso aplicativo e a ajustar o desempenho do aplicativo. Além disso, ele pode ajudar a otimizar a frequência e os tempos de coleta do GC, especificando os melhores tamanhos de heap, outras opções de JVM e algoritmos alternativos do GC.

2.1. Um programa Java simples

Usaremos um programa Java direto para demonstrar como ativar e interpretar nossos logs do GC:

public class Application {

    private static Map<String, String> stringContainer = new HashMap<>();

    public static void main(String[] args) {
        System.out.println("Start of program!");
        String stringWithPrefix = "stringWithPrefix";

       //Load Java Heap with 3 M java.lang.String instances
        for (int i = 0; i < 3000000; i++) {
            String newString = stringWithPrefix + i;
            stringContainer.put(newString, newString);
        }
        System.out.println("MAP size: " + stringContainer.size());

       //Explicit GC!
        System.gc();

       //Remove 2 M out of 3 M
        for (int i = 0; i < 2000000; i++) {
            String newString = stringWithPrefix + i;
            stringContainer.remove(newString);
        }

        System.out.println("MAP size: " + stringContainer.size());
        System.out.println("End of program!");
    }
}

Como podemos ver no exemplo acima, este programa simples carrega 3 milhões de instâncias String em um objeto Map. Em seguida, fazemos uma chamada explícita para o coletor de lixo usando _System.gc () _.

Por fim, removemos 2 milhões das instâncias String do Map. Também usamos explicitamente System.out.println para facilitar a interpretação da saída.

Na próxima seção, veremos como ativar o log do GC.

*3. Ativando o log "simples" do GC *

Vamos começar executando nosso programa e ativando o GC detalhado por meio de nossos argumentos de inicialização da JVM:

-XX:+UseSerialGC -Xms1024m -Xmx1024m -verbose:gc
*O argumento importante aqui é o _-verbose: gc_, que ativa o registro de informações da coleta de lixo em sua forma mais simples* . Por padrão, o log do GC é gravado em _stdout_ e deve gerar uma linha para cada GC da geração jovem e para todo o GC.

Para os fins do nosso exemplo, especificamos o coletor de lixo serial, a implementação mais simples do GC, através do argumento -XX: + UseSerialGC.

Também definimos um tamanho mínimo e máximo de heap de 1024mb, mas é claro que existem mais https://www..com/jvm-parameters [parâmetros da JVM] que podemos ajustar.

3.1. Compreensão básica da saída detalhada

Agora vamos dar uma olhada na saída do nosso programa simples:

Start of program!
[GC (Allocation Failure)  279616K->146232K(1013632K), 0.3318607 secs]
[GC (Allocation Failure)  425848K->295442K(1013632K), 0.4266943 secs]
MAP size: 3000000
[Full GC (System.gc())  434341K->368279K(1013632K), 0.5420611 secs]
[GC (Allocation Failure)  647895K->368280K(1013632K), 0.0075449 secs]
MAP size: 1000000
End of program!

Na saída acima, já podemos ver muitas informações úteis sobre o que está acontecendo dentro da JVM.

A princípio, essa saída pode parecer bastante assustadora, mas agora vamos passo a passo.

Em primeiro lugar, podemos ver que ocorreram quatro coleções, uma GC completa e três Gerações jovens de limpeza.

3.2. A saída detalhada em mais detalhes

Vamos decompor as linhas de saída com mais detalhes para entender exatamente o que está acontecendo:

  1. GC ou Full GC - O tipo de coleta de lixo, GC ou Full GC, para distinguir uma coleta de lixo secundária ou completa

  2. _ (Falha na alocação) _ ou _ (System.gc ()) _ - A causa da coleção - Falha na alocação indica que não há mais espaço no Eden para alocar nossos objetos

  3. 279616K→ 146232K - a memória de heap ocupada antes e depois do GC, respectivamente (separadas por uma seta)

  4. _ (1013632K) _ - A capacidade atual do heap

  5. 0.3318607 secs - a duração do evento do GC em segundos

Portanto, se pegarmos a primeira linha, 279616K→ 146232K (1013632K) _ significa que o GC reduziu a memória heap ocupada de _279616K para 146232K. A capacidade de heap no momento do GC era 1013632K e o GC levou 0.3318607 segundos.

No entanto, embora o formato simples de registro em GC possa ser útil, ele fornece detalhes limitados. Por exemplo, não podemos dizer se o GC mudou algum objeto da geração jovem para a geração antiga ou qual era o tamanho total da geração jovem antes e depois de cada coleção .

Por esse motivo, o registro detalhado do GC é mais útil que o simples.

*4. Ativando o log "detalhado" do GC *

*Para ativar o registro detalhado do GC, usamos o argumento _-XX: + PrintGCDetails _.* Isso fornecerá mais detalhes sobre cada GC, como:
  • Tamanho da geração jovem e antiga antes e depois de cada GC

  • O tempo que leva para que um GC aconteça nas gerações jovens e velhas

  • O tamanho dos objetos promovidos em cada GC *Um resumo do tamanho da pilha total

No próximo exemplo, veremos como capturar informações ainda mais detalhadas em nossos logs, combinando -verbose: gc com esse argumento extra.

5. Interpretando a saída detalhada "detalhada"

Vamos executar nosso programa de amostra novamente:

-XX:+UseSerialGC -Xms1024m -Xmx1024m -verbose:gc -XX:+PrintGCDetails

Desta vez, a saída é bem mais detalhada:

Start of program!
[GC (Allocation Failure) [DefNew: 279616K->34944K(314560K), 0.3626923 secs] 279616K->146232K(1013632K), 0.3627492 secs] [Times: user=0.33 sys=0.03, real=0.36 secs]
[GC (Allocation Failure) [DefNew: 314560K->34943K(314560K), 0.4589079 secs] 425848K->295442K(1013632K), 0.4589526 secs] [Times: user=0.41 sys=0.05, real=0.46 secs]
MAP size: 3000000
[Full GC (System.gc()) [Tenured: 260498K->368281K(699072K), 0.5580183 secs] 434341K->368281K(1013632K), [Metaspace: 2624K->2624K(1056768K)], 0.5580738 secs] [Times: user=0.50 sys=0.06, real=0.56 secs]
[GC (Allocation Failure) [DefNew: 279616K->0K(314560K), 0.0076722 secs] 647897K->368281K(1013632K), 0.0077169 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
MAP size: 1000000
End of program!
Heap
 def new generation   total 314560K, used 100261K [0x00000000c0000000, 0x00000000d5550000, 0x00000000d5550000)
  eden space 279616K,  35% used [0x00000000c0000000, 0x00000000c61e9370, 0x00000000d1110000)
  from space 34944K,   0% used [0x00000000d3330000, 0x00000000d3330188, 0x00000000d5550000)
  to   space 34944K,   0% used [0x00000000d1110000, 0x00000000d1110000, 0x00000000d3330000)
 tenured generation   total 699072K, used 368281K [0x00000000d5550000, 0x0000000100000000, 0x0000000100000000)
   the space 699072K,  52% used [0x00000000d5550000, 0x00000000ebcf65e0, 0x00000000ebcf6600, 0x0000000100000000)
 Metaspace       used 2637K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 283K, capacity 386K, committed 512K, reserved 1048576K

Deveríamos ser capazes de reconhecer todos os elementos do log simples do GC. Mas* existem vários itens novos. *

Vamos agora considerar os novos itens na saída, destacados em azul na próxima seção:

5.1. Interpretando um GC menor na geração jovem

Começaremos analisando as novas peças em um GC menor:

*[GC (falha de alocação) [DefNew: 279616K-> 34944K (314560K), 0,3626923 s] 279616K-> 146232K (1013632K), 0,3627492 s] [Tempos: usuário = 0,33 sys = 0,03, real = 0,36 s]

Como antes, dividiremos as linhas em partes:

  1. DefNew - Nome do coletor de lixo usado.* Esse nome não tão óbvio representa o coletor de lixo de parada única do mundo, cópia de marca e é o que é usado para limpar a geração Young *

  2. 279616K→ 34944K - Uso da geração Young antes e depois da coleta

  3. _ (314560K) _ - O tamanho total da geração Young

  4. 0,3626923 segundos - A duração em segundos

  5. _ [Tempos: usuário = 0,33 sys = 0,03, real = 0,36 seg.] - Duração do evento do GC, medido em diferentes categorias

Agora vamos explicar as diferentes categorias:

  • user - O tempo total da CPU consumido pelo Garbage Collector

  • sys - O tempo gasto em chamadas do SO ou aguardando eventos do sistema

  • real - Todo esse tempo decorrido, incluindo intervalos de tempo usados ​​por outros processos

    *Como estamos executando nosso exemplo usando o Serial Garbage Collector, que sempre usa apenas um único encadeamento, o tempo real é igual à soma das horas do usuário e do sistema.*

5.2. Interpretando um GC completo

Neste penúltimo exemplo, vemos que, para uma coleção principal (GC completo), que foi acionada por nossa chamada de sistema, o coletor usado foi Tenured.

A parte final das informações adicionais que vemos é uma divisão seguindo o mesmo padrão para o Metaspace:

[Metaspace: 2624K->2624K(1056768K)], 0.5580738 secs]
*_Metaspace_ é um novo https://www..com/java-permgen-metaspace [espaço de memória] introduzido no Java 8 e é uma área da memória nativa.*

5.3. Análise de quebra de heap Java

*A parte final da saída inclui um detalhamento do heap, incluindo um resumo da pegada de memória para cada parte da memória* .

Podemos ver que o espaço Eden tinha uma pegada de 35% e a Tenured, uma pegada de 52%. Um resumo para o espaço de metadados e o espaço de classe também está incluído.

A partir dos exemplos acima, agora podemos entender exatamente o que estava acontecendo com o consumo de memória dentro da JVM durante os eventos do GC.

*6. Adicionando informações de data e hora *

Nenhum bom registro é concluído sem informações de data e hora.

*Essas informações extras podem ser altamente úteis quando precisamos correlacionar dados de log do GC com dados de outras fontes, ou podem simplesmente ajudar a facilitar a pesquisa.*

Podemos adicionar os dois argumentos a seguir quando executamos nosso aplicativo para obter informações de data e hora em nossos logs:

-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps

Agora, cada linha começa com a data e hora absolutas em que foi gravada, seguida por um carimbo de data e hora que reflete o tempo real passado em segundos desde o início da JVM:

2018-12-11T02:55:23.518+0100: 2.601: [GC (Allocation ...

*7. Registrando em um arquivo *

Como já vimos, por padrão, o log do GC é gravado em stdout. Uma solução mais prática é especificar um arquivo de saída.

*Podemos fazer isso usando o argumento _-Xloggc: <file> _ onde _file_ é o caminho absoluto para o nosso arquivo de saída:*
-Xloggc:/path/to/file/gc.log

*8. Uma palavra sobre Java 9 e além *

Provavelmente, encontraremos outro argumento no mundo do log de GC, -XX: + PrintGC. No Java 8, -verbose: gc é um alias exato para XX: + PrintGC. No entanto, -verbose: gc é uma opção padrão, enquanto -XX: + PrintGC não é.

Também devemos salientar que -XX: + PrintGC está obsoleto no Java 9 em favor da opção de registro unificado -Xlog: gc. No entanto, _ – verbose: gc_ ainda funciona no JDK 9 e 10.

Para saber mais sobre o registro unificado da JVM, consulte o JEP 158 standard.

===* *9. A * *Ferramenta para analisar logs do GC

Pode ser demorado e bastante tedioso analisar os logs do GC usando um editor de texto. Dependendo da versão da JVM e do algoritmo do GC usado, o formato do log do GC pode ser diferente.

Existe uma ferramenta de análise gráfica gratuita muito boa que analisa os logs da Coleta de Lixo, fornece muitas métricas sobre possíveis problemas da Coleta de Lixo e até fornece soluções em potencial para esses problemas.

Definitivamente, confira o Universal GC Log Analyzer!

===* 10. Conclusão*

Para resumir, neste tutorial, exploramos em detalhes a coleta de lixo detalhada em Java.

Primeiro, começamos introduzindo o que é uma coleta de lixo detalhada e por que podemos usá-la. Em seguida, analisamos vários exemplos usando um aplicativo Java simples. Começamos com a ativação do log do GC em sua forma mais simples antes de explorar vários exemplos mais detalhados e como interpretar a saída.

Por fim, exploramos várias opções extras para registrar informações de hora e data e como gravar informações em um arquivo de log.

Os exemplos de código podem ser encontrados em no GitHub.