Solução de problemas YGC, (do compartilhamento de grandes caras)

  Reimpresso de: https://mp.weixin.qq.com/s/O0l-d928hr994OpSNw3oow Luo Junwu Desenvolvimento do pessoal de  TI no local de trabalho

 

Sob alta simultaneidade, o problema de GC de programas Java é um tipo de problema muito típico e o impacto frequentemente será ampliado ainda mais. Independentemente de "a frequência do GC é muito rápida" ou "GC demora muito", devido ao problema Stop The World durante o GC, é fácil causar tempo limite de serviço e problemas de desempenho.

O sistema de publicidade que nossa equipe é responsável por realizar um tráfego relativamente grande do lado C. O volume de solicitações durante o período de pico atingiu basicamente milhares de QPS. No passado, também encontramos muitos problemas on-line relacionados ao GC.

Neste artigo em maio, apresentei um caso em que Full GC é muito frequente e resumi sistematicamente a estrutura de memória heap da JVM e os princípios de GC.

Neste artigo, vou compartilhar um caso online mais complicado de Young GC que leva muito tempo. Ao mesmo tempo, vou resolver os pontos de conhecimento relacionados ao YGC, espero que você possa ganhar algo. O conteúdo é dividido nas 2 partes a seguir:

  • Vamos começar com um caso em que YGC leva muito tempo

  • Resumo dos pontos de conhecimento relacionados ao YGC

 

01 Partindo de um caso em que YGC demora muito

Em abril deste ano, após o lançamento da nova versão do nosso serviço de publicidade, recebemos um grande número de alarmes de tempo limite de serviço. Você pode ver no seguinte gráfico de monitoramento: A quantidade de tempo limite aumentou repentinamente em uma grande área, e até milhares dos tempos limite da interface foram alcançados em 1 minuto. O processo de solução de problemas desse problema é descrito em detalhes abaixo.

1. Verifique e monitore

Depois de receber o alerta, verificamos o sistema de monitoramento pela primeira vez e descobrimos imediatamente a anormalidade que o YoungGC demorou muito. Nosso programa foi lançado por volta das 21h50. A figura abaixo mostra que o YGC foi basicamente concluído em dezenas de milissegundos antes de ficar online, mas o tempo que levou para o YGC ficar significativamente mais longo depois de ficar online, o mais longo chegou a mais de 3 segundos.

Como o programa irá parar o mundo durante o YGC e o tempo limite do serviço definido pelo nosso sistema upstream é de várias centenas de milissegundos, infere-se que é porque o YGC leva muito tempo para causar um tempo limite grande de serviço.

De acordo com o processo regular de solução de problemas de GC, removemos imediatamente um nó e, em seguida, despejamos o arquivo de memória heap para manter a cena por meio do seguinte comando.

jmap -dump: format = b, file = heap pid

Finalmente, o serviço online foi revertido. Após a reversão, o serviço voltou imediatamente ao normal. A próxima etapa é um processo de solução de problemas e reparo de um dia.

2. Confirme a configuração JVM

Com o seguinte comando, verificamos os parâmetros JVM novamente

ps aux | grep "applicationName = adsearch"

-Xms4g -Xmx4g -Xmn2g -Xss1024K 

-XX: ParallelGCThreads = 5 

-XX: + UseConcMarkSweepGC 

-XX: + UseParNewGC 

-XX: + UseCMSCompactAtFullCollection 

-XX: CMSInitiatingOccupancyFraction = 80

Pode-se ver que a memória heap é 4G, a nova geração e a velha geração são 2G e a nova geração usa o coletor ParNew.

Em seguida, use o comando jmap -heap pid para descobrir: A área Eden da nova geração é 1.6G, e as áreas S0 e S1 são ambas 0.2G.

Este lançamento não modificou nenhum parâmetro relacionado ao JVM, e o número de solicitações de nossos serviços foi basicamente o mesmo de costume. Então, adivinhe: esse problema provavelmente está relacionado ao código online.

3. Verifique o código

Voltando ao princípio do YGC para pensar sobre este problema, o processo de um YGC inclui principalmente as duas etapas a seguir:

1. Digitalize objetos do GC Root e marque os objetos sobreviventes

2. Copie o objeto sobrevivente para a área S1 ou promova-o para a área Antiga

De acordo com o gráfico de monitoramento abaixo, pode ser visto que em circunstâncias normais, a taxa de uso da área do sobrevivente foi mantida em um nível muito baixo (cerca de 30M), mas depois de entrar online, a taxa de uso da área do sobrevivente começou a flutuar e, no máximo, ocupou quase 0,2 G para cima. Além disso, o tempo consumido pelo YGC está basicamente correlacionado positivamente com a taxa de uso da zona do Sobrevivente. Portanto, especulamos que deve haver cada vez mais objetos de vida longa, levando a um aumento no processo demorado de rotular e copiar.

De volta ao desempenho geral do serviço: o tráfego upstream não mudou significativamente. Em circunstâncias normais, o tempo de resposta da interface principal é basicamente de 200 ms e a frequência de YGC é cerca de uma vez a cada 8 segundos.

Obviamente, para variáveis ​​locais, elas podem ser recuperadas imediatamente após cada YGC. Então, por que tantos objetos ainda estão vivos após o YGC?

Além disso, travamos o objeto suspeito em: as variáveis ​​globais do programa ou variáveis ​​estáticas de classe. No entanto, analisamos o código que foi lançado desta vez e não descobrimos que tais variáveis ​​foram introduzidas no código.

4. Analise o arquivo de memória heap de despejo

Depois que a investigação do código não progrediu, começamos a procurar pistas no arquivo de memória heap, usamos a ferramenta MAT para importar o arquivo heap despejado na etapa 1 e, em seguida, visualizamos todos os objetos grandes no heap atual por meio da Árvore Dominator visualizar.

Descobri imediatamente que a classe NewOldMappingService ocupa muito espaço. Ela está localizada por meio do código: Esta classe está localizada no pacote do cliente terceirizado e é fornecida pela equipe de produto de nossa empresa para realizar a conversão entre as categorias novas e antigas. ( Recentemente, a equipe de produtos está na categoria O sistema passa por uma transformação, para ser compatível com o antigo negócio, as novas e antigas categorias precisam ser mapeadas).

Olhando mais adiante no código, verifica-se que há um grande número de HashMaps estáticos nesta classe, que são usados ​​para armazenar em cache os vários dados que precisam ser usados ​​quando as categorias novas e antigas são convertidas, de modo a reduzir as chamadas RPC e melhorar o desempenho de conversão.

Originalmente, pensei que fosse muito próximo da verdade do problema, mas uma investigação aprofundada descobriu que todas as variáveis ​​estáticas desta classe são inicializadas quando a classe é carregada. Embora ocupe mais de 100 MB de memória, não ser adicionado mais tarde. Além disso, esta aula foi lançada no início de março e a versão do pacote do cliente não mudou.

Após a análise acima, esse tipo de HashMap estático sempre sobreviverá. Após várias rodadas de YGC, ele será promovido à velhice. Não deve ser a razão pela qual YGC continua demorando muito. Portanto, descartamos temporariamente esse ponto suspeito.

5. Analise o consumo de tempo de YGC para processar a Referência

A equipe tem muito pouca experiência na solução de problemas do YGC, e não sei como analisá-la mais profundamente. Basicamente, fiz a varredura de todos os casos disponíveis na Internet e descobri que os motivos se concentravam nestas duas categorias:

1. Leva muito tempo para rotular objetos ativos: por exemplo, o método Finalize da classe Object está sobrecarregado, o que faz com que a rotulagem da Referência Final demore muito; ou o método String.intern é usado incorretamente, o que faz com que o YGC para verificar a StringTable por muito tempo.

2. Acumulação excessiva de objetos de longo prazo: Por exemplo, o uso impróprio de caches locais acumula muitos objetos sobreviventes; ou a competição de bloqueios séria causa bloqueio de thread e o ciclo de vida das variáveis ​​locais se torna mais longo.

Para o primeiro tipo de problema, a demorada Referência de processamento de GC pode ser exibida por meio dos seguintes parâmetros -XX: + PrintReferenceGC. Depois de adicionar este parâmetro, você pode ver que o tempo de processamento para diferentes tipos de referências é muito curto, portanto, esse fator é excluído.

6. Voltar ao objeto de longo prazo para análise

Mais tarde, adicionamos vários parâmetros de GC e tentamos encontrar pistas sem sucesso. A partir de um monitoramento abrangente e várias análises: apenas objetos de longo prazo devem causar este problema para nós.

Depois de jogar por várias horas, no final, um pequeno parceiro encontrou o segundo ponto de suspeita na memória heap do MAT.

Na captura de tela acima, podemos ver que a classe ConfigService classificada em terceiro lugar entre os objetos grandes entrou em nosso campo de visão. Uma variável ArrayList desta classe contém objetos de 270W, e a maioria deles são os mesmos elementos.

A classe ConfigService está no pacote Apollo de terceiros, mas o código-fonte foi refeito pelo departamento de arquitetura da empresa. A partir do código, pode-se ver que o problema está na linha 11, e os elementos são adicionados à lista sempre que o método getConfig é chamado., E não faz o processamento de eliminação de duplicação.

Nosso serviço de publicidade armazena um grande número de configurações de estratégia de publicidade em apollo, e a maioria das solicitações chamará o método getConfig de ConfigService para obter a configuração, de modo que novos objetos são constantemente adicionados aos namespaces de variáveis ​​estáticas, o que causa esse problema.

Nesse ponto, todo o problema finalmente veio à tona. Este BUG foi acidentalmente introduzido pelo departamento de arquitetura durante o desenvolvimento personalizado do pacote do cliente Apollo. Obviamente, ele não foi cuidadosamente testado e foi lançado no armazém central um dia antes de entrarmos online. A versão do básico da empresa biblioteca de componentes aprovada O método super-pom é mantido de maneira uniforme e não há percepção do negócio.

7. Solução

Para verificar rapidamente se o YGC demorou muito para ser causado por esse problema, substituímos diretamente a versão antiga do pacote do cliente Apollo em um servidor e reiniciámos o serviço. Após observar por quase 20 minutos, o YGC voltou ao normal.

Por fim, notificamos o Departamento de Arquitetura para corrigir o BUG e relançamos o super-pom, que resolveu completamente o problema.

02 Resumo dos pontos de conhecimento relevantes do YGC

Por meio do caso acima, você pode ver que os problemas do YGC são, na verdade, mais difíceis de solucionar. Comparado com o FGC ou OOM, o log do YGC é muito simples, ele só conhece as mudanças e o consumo de tempo da nova geração de memória. Ao mesmo tempo, a memória heap despejada deve ser verificada com cuidado.

Além disso, se você não conhece o processo YGC, será mais difícil solucionar o problema. Aqui, irei separar os pontos de conhecimento relacionados ao YGC, para que todos possam entender o YGC de forma mais abrangente.

1. 5 perguntas para re-compreender a nova geração

A YGC é realizada no Cenozóico. Em primeiro lugar, a divisão da estrutura do monte do Cenozóico deve ser clara. A nova geração é dividida em uma área Eden e duas áreas Survivor, onde Eden: de: a = 8: 1: 1 (a proporção pode ser definida pelo parâmetro -XX: SurvivorRatio). Este é o entendimento mais básico.

Por que existe uma nova geração?

Se não houver geração, todos os objetos estão em uma área e cada GC precisa fazer a varredura de todo o heap, o que causa problemas de eficiência. Após as gerações, a frequência de recuperação pode ser controlada separadamente e diferentes algoritmos de recuperação podem ser usados ​​para garantir o desempenho global ideal do GC.

Por que a nova geração adota o algoritmo de replicação?

Os objetos da nova geração estão vivos e morrendo, cerca de 90% dos objetos recém-criados podem ser reciclados rapidamente, o custo do algoritmo de cópia é baixo e o espaço é garantido como livre de fragmentação. Embora o algoritmo de marcação e classificação também possa garantir que não haja fragmentação, devido ao grande número de objetos a serem limpos na nova geração, um grande número de operações de movimentação são necessárias antes que os objetos sobreviventes sejam classificados para os objetos a serem limpos , e a complexidade de tempo é maior do que a do algoritmo de cópia.

Por que a nova geração precisa de duas áreas de sobreviventes?

Para economizar espaço, se o algoritmo de cópia tradicional for usado e houver apenas uma área do sobrevivente, o tamanho da área do sobrevivente deve ser igual ao tamanho da área do Éden. Neste momento, o consumo de espaço é 8 * 2 , e dois sobreviventes podem manter novos objetos sempre criados na área do Éden. Os objetos sobreviventes podem ser transferidos entre o sobrevivente e o consumo de espaço é 8 + 1 + 1. Obviamente, a taxa de utilização do espaço deste último é maior.

Qual é o espaço real disponível para a nova geração?

Depois do YGC, sempre há uma área de sobrevivente que está livre, então o espaço de memória disponível da nova geração é de 90%. Ao visualizar o espaço da nova geração no log YGC ou por meio do comando jmap -heap pid, não se surpreenda se descobrir que a capacidade é de apenas 90%.

Como a área Eden acelera a alocação de memória?

A máquina virtual HotSpot usa duas técnicas para acelerar a alocação de memória. Eles são bump-the-pointer e TLAB (Thread Local Allocation Buffers).

Como a área Eden é contínua, o bump-the-pointer só precisa verificar se há memória suficiente atrás do último objeto quando o objeto é criado, acelerando assim a alocação de memória.

A tecnologia TLAB é para multi-threading. No Eden, uma região é alocada para cada thread para reduzir conflitos de bloqueio durante a alocação de memória, acelerar a alocação de memória e melhorar o rendimento.

2. Quatro tipos de colecionadores na nova geração

SerialGC (Serial Collector), o mais antigo, execução single-threaded, adequado para cenários de CPU única.

ParNew (Coletor Paralelo), o coletor serial é multiencadeado, adequado para cenários multi-CPU e precisa ser usado com o coletor CMS antigo.

ParallelGC (Coletor Paralelo) é diferente de ParNew porque foca no rendimento e pode definir o tempo de pausa esperado.Ele ajusta automaticamente o tamanho do heap e outros parâmetros quando funciona.

G1 (Garage-First Collector), o coletor padrão do JDK 9 e versões posteriores, leva em consideração a nova e a antiga geração. Ele divide o heap em uma série de regiões e não requer blocos de memória contínuos. A nova geração é ainda coletados em paralelo.

Todos os coletores mencionados acima usam o algoritmo de replicação, são exclusivos e irão parar o mundo durante a execução.

3. Tempo de disparo do YGC

Quando a área do Éden for insuficiente, o YGC será acionado. Vejamos o processo detalhado em conjunto com a alocação de memória dos objetos da nova geração:

1. O novo objeto tentará primeiro alocá-lo na pilha. Se não funcionar, tente alocá-lo no TLAB. Caso contrário, ele verificará se a condição de objeto grande foi atendida. Deve ser alocado no velhice e, finalmente, considere a possibilidade de se candidatar a um espaço na área de Eden.

2. Se não houver espaço adequado na área do Éden, o YGC é acionado.

3. Durante YGC, os objetos sobreviventes na área do Éden e na área do sobrevivente são processados. Se as condições para a determinação da idade dinâmica forem atendidas ou o espaço na área do sobrevivente for insuficiente, a idade será inserida diretamente. espaço para a velhice não é suficiente, uma promoção falhada ocorrerá., Desencadeando a reciclagem da velhice. Caso contrário, o objeto sobrevivente é copiado para a área To Survivor.

4. Neste momento, os objetos restantes na área do Éden e na área do Sobrevivente são objetos de lixo, que podem ser apagados e reciclados diretamente.

Além disso, se o coletor CMS for utilizado na velhice, para reduzir o consumo de tempo da fase CMS Observação, ele também pode acionar um YGC, que não será ampliado aqui.

4. O processo de execução do YGC

O algoritmo de replicação adotado pelo YGC é dividido principalmente nas duas etapas a seguir:

1. Encontre GC Roots e copie os objetos referenciados para a área S1

2. Percorra recursivamente o objeto na etapa 1, copie o objeto referenciado para a área S1 ou promova-o para a área Antiga

Todo o processo mencionado acima requer a suspensão de threads de negócios (STW), mas coletores de nova geração, como ParNew, podem ser executados em paralelo em várias threads para melhorar a eficiência do processamento.

YGC usa o algoritmo de análise de alcançabilidade para pesquisar para baixo a partir da raiz GC (o ponto de partida do objeto alcançável) para marcar os objetos sobreviventes no momento e, em seguida, os objetos não marcados restantes são os objetos que precisam ser reciclados.

Os objetos de GC Root que podem ser usados ​​como YGC incluem o seguinte:

1. Objetos referenciados na pilha de máquina virtual

2. Objetos referenciados por propriedades estáticas e constantes na área do método

3. Objetos referenciados na pilha de método local

4. Objetos mantidos por bloqueios sincronizados

5. Grave o SystemDictionary da classe atualmente carregada

6. Registre a StringTable referenciada pela constante de string

7. Objetos com referências de geração cruzada

8. Objetos na mesma CardTable que GC Root

Entre eles, 1-3 é fácil de imaginar e 4-8 é fácil de passar despercebido, mas é muito provável que seja uma dica ao analisar problemas de YGC.

Além disso, deve-se notar que para a referência cruzada de gerações na figura a seguir, o objeto A da velhice também deve fazer parte da raiz GC, mas se a velhice for varrida a cada YGC, deve haver uma eficiência problema. No HotSpot JVM, a Tabela de Cartões é introduzida para acelerar a marcação de referências de geração cruzada.

Mesa de Cartão, um entendimento simples é uma forma de espaço para o tempo, porque provavelmente há menos de 1% de objetos referenciados entre gerações, então o espaço de heap pode ser dividido em páginas de cartão com um tamanho de 512 bytes. tem uma referência de geração cruzada, 1 byte pode ser usado para identificar que a página do cartão está em um estado sujo e o estado da página do cartão é mantido por meio da tecnologia de barreira de gravação.

Depois de atravessar as GC Roots, você pode encontrar o primeiro lote de objetos sobreviventes e copiá-los para a área S1. Em seguida, há um processo de localização e cópia recursiva de objetos sobreviventes.

Para facilitar a manutenção da área de memória na área S1, duas variáveis ​​de ponteiro são introduzidas: _saved_mark_word e _top, onde _saved_mark_word representa a localização do objeto percorrido atual, e _top representa a localização da memória alocável atual. Obviamente, o objeto entre _saved_mark_word e _top São todos objetos que foram copiados, mas não verificados.

Conforme mostrado na figura acima, sempre que um objeto é verificado, _saved_mark_word avançará. Durante este período, se houver novos objetos, ele também será copiado para a área S1 e _top também avançará até que _saved_mark_word alcança _top , indicando todos os objetos na área S1. O percurso foi concluído.

Há um detalhe que precisa ser observado: o espaço de destino do objeto de cópia não é necessariamente a área S1, mas também pode ser a idade avançada. Se a idade de um objeto (o número de YGCs experimentados) atende à condição de determinação de idade dinâmica, ele é promovido diretamente para a idade avançada. A idade do objeto é armazenada na estrutura de dados da palavra de marca do cabeçalho do objeto Java (se você estiver familiarizado com bloqueios simultâneos Java, deve conhecer esta estrutura de dados. Se não estiver familiarizado, é recomendável verificar as informações para compreensão, e não vou expandi-lo aqui).

Acho que você gosta

Origin blog.csdn.net/qq_39809613/article/details/107354568
Recomendado
Clasificación