Um registro completo de resolução de problemas para vazamentos de memória fora do heap JVM

Prefácio

Registre um processo e ideias de solução de problemas de vazamento de memória fora de heap de JVM online, incluindo algumas "análises de princípio de alocação de memória de JVM" e "métodos de solução de problemas de JVM comuns e compartilhamento de ferramentas" , espero ajudar a todos.

Em todo o processo de investigação, também dei muitos desvios, mas no artigo ainda escreverei os pensamentos e ideias completos, como lição para as gerações futuras, o artigo também resume o problema de vazamento de memória rapidamente Vários princípios de investigação.

"O conteúdo principal deste artigo:"

  • Descrição da falha e processo de solução de problemas

  • Análise de causas e soluções de falha

  • JVM heap de memória e princípio de alocação de memória off-heap

  • Introdução e uso de instruções e ferramentas de solução de problemas de vazamento de memória de processo comumente usadas


Descrição de falha

Durante a pausa para o almoço de 12 de agosto, nosso serviço comercial recebeu um alarme de que a memória física (16G) ocupada pelo processo de serviço ultrapassou o limite de 80% e ainda estava aumentando.

O sistema de monitoramento chama o gráfico para visualizar:

Por exemplo, há um vazamento de memória no processo Java e nosso limite de memória heap é 4G, esse tipo de memória maior que 4G está quase cheia de memória deve ser um vazamento de memória fora do heap JVM.

Confirme a configuração de inicialização do processo de serviço naquele momento:

-Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80

Embora nenhum novo código tenha sido lançado naquele dia , "Estamos usando a fila de mensagens para enviar o script de reparo de dados históricos naquela manhã. Esta tarefa chamará um grande número de uma de nossas interfaces de serviço" , portanto, inicialmente suspeita-se que esteja relacionado a esta interface.

A figura a seguir mostra a mudança no tráfego da interface de chamada no dia:

Pode-se observar que o número de chamadas no momento do incidente aumentou muito (mais de 5.000 vezes por minuto) em comparação com a situação normal (mais de 200 vezes por minuto).

"Paramos temporariamente o script de enviar mensagens. O número de chamadas para esta interface caiu para mais de 200 vezes por minuto, e a memória do contêiner não aumentou mais em uma inclinação extremamente alta. Tudo parecia estar de volta ao normal."

A seguir, verifique se esta interface tem um vazamento de memória.


Processo de investigação

Em primeiro lugar, vamos revisar a alocação de memória do processo Java para facilitar nossa explicação das idéias de solução de problemas abaixo.

"Pegue a versão JDK1.8 que usamos online como exemplo . " Existem muitos resumos sobre a alocação de memória JVM na Internet, então não farei uma segunda criação.

A área de memória JVM é dividida em dois blocos: área de heap e área de não heap.

  • Área de pilha: É a nova geração e a velhice como a conhecemos.

  • Área não heap: conforme mostrado na figura, a área não heap possui uma área de metadados e memória direta.

"Uma observação adicional aqui é que a geração permanente (que é nativa do JDK8) armazena as classes usadas pela JVM durante o tempo de execução e os objetos da geração permanente são coletados como lixo durante o GC completo."

Depois de revisar a alocação de memória da JVM, vamos voltar à falha.

Análise de memória heap

Embora seja basicamente confirmado no início que não tem nada a ver com a memória heap, porque o uso de memória perdida excede o limite de memória heap de 4G, vamos olhar para a memória heap em busca de pistas.

Observamos a curva de ocupação da memória da nova geração e da geração anterior e as estatísticas do número de tempos de recuperação. Não houve nenhum grande problema, como de costume. Em seguida, despejamos um log da memória heap da JVM no contêiner no local do acidente.

Despejo de memória heap

Comando de despejo de instantâneo de memória de heap:

jmap -dump:live,format=b,file=xxxx.hprof pid

❝Voiceover: Você também pode usar jmap -histo: live pid para visualizar diretamente os objetos ativos na memória heap.

Após exportar, baixe o arquivo Dump de volta para o local, e então use o MAT (Memory Analyzer) do Eclipse ou o JVisualVM que acompanha o JDK para abrir o arquivo de log.

Use o MAT para abrir o arquivo conforme mostrado:

"Você pode ver que há alguns objetos grandes relacionados a nio na memória heap, como o nioChannel que está recebendo mensagens da fila de mensagens e nio.HeapByteBuffer, mas o número não é grande e não pode ser usado como base para julgamento. Vamos observá-lo primeiro."

Em seguida, comecei a navegar pelo código da interface. A lógica principal dentro da interface é chamar o cliente WCS do grupo e escrever os dados na tabela do banco de dados para o WCS depois de consultar a tabela. Não há outra lógica adicional.

Depois de descobrir que não há uma lógica especial, comecei a duvidar se há um vazamento de memória no pacote do cliente WCS. O motivo para essa suspeita é que a camada inferior do cliente WCS é encapsulada pelo cliente SCF. Como uma estrutura RPC, o protocolo de transmissão de comunicação subjacente pode ser aplicável Memória direta.

"Meu código gerou um bug no cliente WCS, fazendo com que ele se aplicasse continuamente a chamadas diretas de memória e, eventualmente, consumisse a memória."

Entrei em contato com o oficial de serviço da WCS e descrevi o problema que encontramos com eles. Eles nos responderam e farão um teste de pressão da operação de gravação localmente para ver se nosso problema pode ser reproduzido.

Uma vez que leva tempo para esperar por seus comentários, estamos prontos para descobrir os motivos por nós mesmos.

"Concentrei minhas suspeitas na memória direta. Suspeitei que a quantidade de chamadas de interface era muito grande, e o uso indevido de nio pelo cliente levou ao uso de ByteBuffer para solicitar muita memória direta."

"Voiceover: O resultado final provou que essa ideia pré-concebida levou a um desvio no processo de investigação. No processo de investigação do problema, é possível estreitar o escopo da investigação com suposições razoáveis, mas é melhor primeiro considerar todas as possibilidades Deixe claro que, quando você se vir profundamente em uma certa possibilidade, sem sucesso, deve olhar para trás e examinar outras possibilidades no tempo. ”

Reprodução de ambiente sandbox

Para restaurar o cenário de falha naquele momento, solicitei uma máquina de teste de estresse no ambiente sandbox para garantir a consistência com o ambiente online.

"Primeiro, vamos simular a situação de estouro de memória (um grande número de chamadas para a interface):"

Deixamos o script continuar a enviar dados, chamar nossa interface e continuar a observar o uso de memória.

Quando a chamada é iniciada, a memória continua a crescer e não parece estar restrita (o GC Completo não é acionado devido a restrições).

"A seguir, vamos simular o volume normal da chamada (interface de chamada normal):"

Cortamos o volume normal de chamadas da interface (menor e uma chamada em lote a cada 10 minutos) para a máquina de teste de estresse e obtivemos as tendências de memória de geração anterior e memória física como a seguinte figura:

"A questão é: por que a memória continua subindo e devorando a memória?"

Na época, foi especulado que, como o processo JVM não limitava o tamanho da memória direta (-XX: MaxDirectMemorySize), a memória fora do heap continuava a aumentar e não acionaria a operação FullGC.

"A imagem acima pode tirar duas conclusões:"

  • Quando a quantidade de chamadas de interface para vazamentos de memória for grande, se outras condições, como a geração antiga no heap, não atenderem à condição FullGC, não será FullGC e a memória direta aumentará totalmente.

  • No caso de um volume de chamadas normalmente baixo, os vazamentos de memória são mais lentos, FullGC sempre virá e a parte vazada será recuperada.Este também é o motivo pelo qual não há problemas em horários normais e está funcionando normalmente por muito tempo.

"Como mencionado acima, não há limite de memória direto nos parâmetros de inicialização do nosso processo, então adicionamos a configuração -XX: MaxDirectMemorySize e testamos novamente no ambiente sandbox."

Acontece que a memória física ocupada pelo processo continuará aumentando, excedendo o limite que definimos, e a configuração de "looks" parece não funcionar.

Isso me surpreendeu. Existe um problema com o limite de memória da JVM?

"Quando chego aqui, posso ver que meu pensamento está obcecado por vazamentos diretos de memória durante o processo de investigação, e isso se foi para sempre."

"Voiceover: Devemos confiar no domínio da memória do JVM. Se descobrirmos que os parâmetros são inválidos, devemos descobrir o motivo por nós mesmos e ver se usamos os parâmetros incorretos."

Análise de memória direta

Para investigar melhor o que está na memória direta, comecei a trabalhar na memória direta. Visto que a memória direta não é como a memória heap, é fácil ver todos os objetos ocupados. Precisamos de alguns comandos para solucionar problemas de memória direta. Usei vários métodos para ver quais são os problemas da memória direta.

Exibir informações de memória de processo pmap

mapa de memória de relatório pmap de um processo (ver informações de mapa de memória de processo)

O comando pmap é usado para relatar a relação de mapeamento de memória do processo e é uma boa ferramenta para depuração, operação e manutenção do Linux.

pmap -x pid 如果需要排序  | sort -n -k3**

Após a execução, obtive a seguinte saída, e a saída é a seguinte:

..
00007fa2d4000000    8660    8660    8660 rw---   [ anon ]
00007fa65f12a000    8664    8664    8664 rw---   [ anon ]
00007fa610000000    9840    9832    9832 rw---   [ anon ]
00007fa5f75ff000   10244   10244   10244 rw---   [ anon ]
00007fa6005fe000   59400   10276   10276 rw---   [ anon ]
00007fa3f8000000   10468   10468   10468 rw---   [ anon ]
00007fa60c000000   10480   10480   10480 rw---   [ anon ]
00007fa614000000   10724   10696   10696 rw---   [ anon ]
00007fa6e1c59000   13048   11228       0 r-x-- libjvm.so
00007fa604000000   12140   12016   12016 rw---   [ anon ]
00007fa654000000   13316   13096   13096 rw---   [ anon ]
00007fa618000000   16888   16748   16748 rw---   [ anon ]
00007fa624000000   37504   18756   18756 rw---   [ anon ]
00007fa62c000000   53220   22368   22368 rw---   [ anon ]
00007fa630000000   25128   23648   23648 rw---   [ anon ]
00007fa63c000000   28044   24300   24300 rw---   [ anon ]
00007fa61c000000   42376   27348   27348 rw---   [ anon ]
00007fa628000000   29692   27388   27388 rw---   [ anon ]
00007fa640000000   28016   28016   28016 rw---   [ anon ]
00007fa620000000   28228   28216   28216 rw---   [ anon ]
00007fa634000000   36096   30024   30024 rw---   [ anon ]
00007fa638000000   65516   40128   40128 rw---   [ anon ]
00007fa478000000   46280   46240   46240 rw---   [ anon ]
0000000000f7e000   47980   47856   47856 rw---   [ anon ]
00007fa67ccf0000   52288   51264   51264 rw---   [ anon ]
00007fa6dc000000   65512   63264   63264 rw---   [ anon ]
00007fa6cd000000   71296   68916   68916 rwx--   [ anon ]
00000006c0000000 4359360 2735484 2735484 rw---   [ anon ]

Pode-se perceber que o resultado final é o mapeamento da memória heap, que ocupa 4G, e a outra possui uma pegada de memória muito pequena, mas ainda não conseguimos ver o problema por meio dessas informações.

NativeMemoryTracking

❝Native Memory Tracking (NMT) é uma função usada pelo Hotspot VM para analisar o uso da memória interna da VM. Podemos usar o jcmd (integrado no jdk) para acessar os dados NMT.

O NMT deve primeiro ser ativado por meio dos parâmetros de inicialização da VM, mas deve-se observar que ativar o NMT causará perda de desempenho de cerca de 5% a 10%.

-XX:NativeMemoryTracking=[off | summary | detail]
# off: 默认关闭
# summary: 只统计各个分类的内存使用情况.
# detail: Collect memory usage by individual call sites.

Em seguida, execute o processo, você pode usar o seguinte comando para visualizar a memória direta:

jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]
 
# summary: 分类内存使用情况.
# detail: 详细内存使用情况,除了summary信息之外还包含了虚拟内存使用情况。
# baseline: 创建内存使用快照,方便和后面做对比
# summary.diff: 和上一次baseline的summary对比
# detail.diff: 和上一次baseline的detail对比
# shutdown: 关闭NMT

Nós usamos:

jcmd pid VM.native_memory detail scale=MB > temp.txt

Obtenha o resultado conforme mostrado:

Nenhuma das informações que nos foi dada na imagem acima pode ver claramente o problema, pelo menos eu ainda não consegui ver o problema através dessas várias informações.

A investigação parece ter chegado a um impasse.

Não há como sair da dúvida

Quando a investigação parou, obtivemos uma resposta do WCS e do SCF, "Ambas as partes confirmaram que não há vazamento de memória em seu pacote". O WCS não usou memória direta e o SCF foi usado como o protocolo RPC subjacente, mas Não vai deixar um bug de memória tão óbvio, caso contrário, deve haver muito feedback online.

Ver informações de memória JVM jmap

Nesse ponto, se não consegui encontrar o problema, abri um novo contêiner de sandbox novamente, executei o processo de serviço e, em seguida, executei o comando jmap para dar uma olhada na "configuração real" da memória JVM :

jmap -heap pid

obteve a resposta:

Attaching to process ID 1474, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.66-b17

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 4294967296 (4096.0MB)
   NewSize                  = 2147483648 (2048.0MB)
   MaxNewSize               = 2147483648 (2048.0MB)
   OldSize                  = 2147483648 (2048.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 1932787712 (1843.25MB)
   used     = 1698208480 (1619.5378112792969MB)
   free     = 234579232 (223.71218872070312MB)
   87.86316621615607% used
Eden Space:
   capacity = 1718091776 (1638.5MB)
   used     = 1690833680 (1612.504653930664MB)
   free     = 27258096 (25.995346069335938MB)
   98.41346682518548% used
From Space:
   capacity = 214695936 (204.75MB)
   used     = 7374800 (7.0331573486328125MB)
   free     = 207321136 (197.7168426513672MB)
   3.4349974840697497% used
To Space:
   capacity = 214695936 (204.75MB)
   used     = 0 (0.0MB)
   free     = 214695936 (204.75MB)
   0.0% used
concurrent mark-sweep generation:
   capacity = 2147483648 (2048.0MB)
   used     = 322602776 (307.6579818725586MB)
   free     = 1824880872 (1740.3420181274414MB)
   15.022362396121025% used

29425 interned Strings occupying 3202824 bytes

Nas informações de saída, pode-se observar que as gerações antigas e novas são normais, o metaespaço ocupa apenas 20M, e a memória direta parece ser 2g ...

OK? Porque MaxMetaspaceSize = 17592186044415 MB? "Aparência e sem restrições como」 .

Dê uma olhada em nossos parâmetros de inicialização:

-Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80

O que se configura é -XX:PermSize=256m -XX:MaxPermSize=512mo espaço de memória da geração permanente. "E depois de 1.8, a máquina virtual Hotspot foi removida permanentemente gerações, usando o yuan em vez do espaço." Uma vez que estamos usando o JDK1.8 online, "então temos a capacidade máxima das restrições de espaço de yuan simplesmente não fazem" , o -XX:PermSize=256m -XX:MaxPermSize=512mque Os dois parâmetros são parâmetros expirados para 1.8.

A figura a seguir descreve a mudança de 1,7 para 1,8, a geração permanente:

"Será que a memória do metaspace vazou?"

Escolhi testar localmente, o que é conveniente para alterar os parâmetros, e também é conveniente usar a ferramenta JVisualVM para ver visualmente as alterações de memória.

Use JVisualVM para observar o processo em execução

Primeiro limite o meta-espaço, use parâmetros -XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128me depois chame a interface problemática localmente.

Obtenha como mostrado:

"Pode-se ver que quando o meta-espaço se esgota, o sistema inicia Full GC, a memória do meta-espaço é recuperada e muitas classes são descarregadas."

Em seguida, removemos a restrição de meta espaço, ou seja, usamos os parâmetros que apresentavam problemas antes:

-Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80 -XX:MaxDirectMemorySize=2g -XX:+UnlockDiagnosticVMOptions

Obtenha como mostrado:

“Percebe-se que o metaespaço está subindo, e as classes carregadas também estão subindo com o aumento do número de ligações, mostrando uma tendência de correlação positiva”.

Aldeia Liu An Hua Ming You Yi

Resolva os problemas imediatamente, "Com cada interface de chamada, muito provavelmente uma classe está constantemente sendo criada, ocupando espaço de memória yuan yuan .

Observe a situação de carregamento da classe JVM - detalhada

❝Ao depurar um programa, às vezes é necessário verificar as classes carregadas pelo programa, a situação de recuperação da memória, a interface local chamada, etc. Neste momento, o comando -verbose é necessário. Em myeclipse, você pode clicar com o botão direito para definir (como segue), ou pode inserir java-na linha de comando verboso para ver.

-verbose:class 查看类加载情况
-verbose:gc 查看虚拟机中内存回收情况
-verbose:jni 查看本地方法调用的情况

No ambiente local, adicionamos parâmetros de inicialização para -verbose:classchamar a interface ciclicamente.

Você pode ver que incontáveis ​​são gerados com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto:

[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]

Ao chamá-lo muitas vezes e acumular certas classes, executamos manualmente Full GC para reciclar o carregador de classes. Descobrimos que um grande número de classes relacionadas a fastjson foram recicladas.

"Se você usar o jmap para verificar o carregamento da classe antes de reciclar, também poderá encontrar um grande número de classes relacionadas ao fastjson:"

jmap -clstats 7984

Agora eu tenho uma direção, "verifique o código com atenção desta vez" , verifique onde fastjson é usado na lógica do código e encontrei o seguinte código:

/**
 * 返回Json字符串.驼峰转_
 * @param bean 实体类.
 */
public static String buildData(Object bean) {
    try {
        SerializeConfig CONFIG = new SerializeConfig();
        CONFIG.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
        return jsonString = JSON.toJSONString(bean, CONFIG);
    } catch (Exception e) {
        return null;
    }
}

Causa raiz do problema

Serializamos a classe de entidade do campo camel case em um campo sublinhado antes de chamar wcs. Isso requer o uso de SerializeConfig de Fastjson e instanciamos em um método estático. Quando SerializeConfig é criado, uma classe de proxy ASM é criada por padrão para realizar a serialização do objeto de destino. Ou seja, as classes com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDtoque são frequentemente criadas acima . Se reutilizarmos SerializeConfig, fastjson irá procurar as classes proxy criadas e reutilizá-las. Mas se new SerializeConfig () não puder encontrar a classe de proxy gerada originalmente, ele continuará a gerar uma nova classe de proxy WlkCustomerDto.

O código-fonte do local do problema nas duas imagens a seguir:

Usamos SerializeConfig como uma variável estática da classe, e o problema foi resolvido.

private static final SerializeConfig CONFIG = new SerializeConfig();

static {
    CONFIG.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
}

O que fastjson SerializeConfig faz

Introdução ao SerializeConfig:

❝A principal função do SerializeConfig é configurar e registrar a classe de serialização (classe de implementação da interface ObjectSerializer) correspondente a cada tipo Java. Por exemplo, Boolean.class usa BooleanCodec (veja o nome para saber que esta classe implementará serialização e desserialização juntas) Como a classe de implementação de serialização, float []. Class usa FloatArraySerializer como a classe de implementação de serialização. Algumas dessas classes de implementação de serialização são implementadas por padrão no FastJSON (como as classes básicas Java) e algumas são geradas por meio da estrutura ASM (como o próprio usuário Classe de definição) e até mesmo algumas classes de serialização definidas pelo usuário (por exemplo, a implementação padrão da estrutura do tipo Date é convertida em milissegundos e o aplicativo precisa ser convertido em segundos). Claro, isso envolve o uso de ASM para gerar classes serializadas ou JavaBeans O problema de serialização da classe de serialização, o julgamento aqui é baseado em se o ambiente Android (a variável de ambiente "java.vm.name" é "dalvik" ou "lemur" é o ambiente Android), mas o julgamento não está apenas aqui, mas também o acompanhamento Julgamentos mais específicos.

Teoricamente falando, se cada instância de SerializeConfig serializar a mesma classe, ela encontrará a classe proxy dessa classe gerada antes para serialização. Nosso serviço instancia um objeto ParseConfig para definir as configurações de desserialização Fastjson toda vez que a interface é chamada. Se o proxy ASM não estiver desabilitado, já que cada chamada para ParseConfig é uma nova instância, está sempre verificado A classe de proxy criada não está disponível, então Fastjson constantemente cria novas classes de proxy e as carrega no metaspace, o que eventualmente leva à expansão contínua do metaspace e esgota a memória da máquina.

Os problemas só ocorrerão após a atualização do JDK1.8

Ainda vale a pena prestar atenção à causa do problema. Por que esse problema não ocorre antes da atualização? Isso é para analisar a diferença entre a máquina virtual de ponto de acesso que vem com jdk1.8 e 1.7.

❝ A partir do jdk1.8, a máquina virtual do ponto de host que vem com ela cancela a área permanente no passado e adiciona uma área de metaspace. Do ponto de vista funcional, o metaspace pode ser considerado semelhante à área permanente e sua função principal é armazenar metadados , Mas o mecanismo real é bem diferente.

Em primeiro lugar, o valor máximo padrão do metasspaço é o tamanho da memória física de toda a máquina, então a expansão contínua do metasspaço fará com que os programas java invadam a memória disponível do sistema e, eventualmente, o sistema não terá memória disponível; enquanto a área permanente tem um tamanho padrão fixo e não se expandirá para todo A memória disponível da máquina. Quando a memória alocada se esgota, ambos disparam gc total, mas a diferença é que quando a área permanente está em gc total, os metadados de classe (objeto de classe) na área permanente são recuperados com um mecanismo semelhante quando a memória heap é recuperada. Objetos que não podem ser alcançados pela referência raiz podem ser reciclados. Metaspace determina se os metadados da classe podem ser reciclados. Ele é julgado com base na possibilidade de reciclagem do Classloader que carrega os metadados da classe. Desde que o Classloader não possa ser reciclado, os metadados da classe são carregados por meio dele Não será reciclado. Isso também explica por que nossos dois serviços têm problemas após a atualização para 1.8, porque na versão jdk anterior, embora muitas classes de proxy tenham sido criadas toda vez que fastjson foi chamado, muitas classes de proxy foram carregadas na área permanente. Instâncias de classe, mas essas instâncias de classe são criadas quando o método é chamado e ficam inacessíveis após a chamada ser concluída. Portanto, quando a memória de área permanente estiver cheia e o gc completo for acionado, eles serão reciclados.

Ao usar 1.8, porque essas classes de proxy são carregadas por meio do Classloader do thread principal, este Classloader nunca será reciclado durante a execução do programa, portanto, as classes de proxy carregadas por meio dele nunca serão recicladas. Isso levou à expansão contínua do metaespaço, que acabou esgotando a memória da máquina.

Esse problema não se limita ao fastjson, desde que seja necessário carregar e criar classes através do programa, esse problema pode ocorrer. "Especialmente no framework, um grande número de ferramentas como ASM, javassist, etc. são frequentemente usadas para aprimoramento de bytecode. De acordo com a análise acima, antes de jdk1.8, porque na maioria dos casos a classe carregada dinamicamente pode ser obtida em gc completo Reciclagem, portanto, não está sujeito a problemas " e, portanto, muitos frameworks e kits de ferramentas não lidam com esse problema. Depois de atualizado para 1.8, esses problemas podem ser expostos.

 

Resumindo

O problema foi resolvido. Em seguida, analisei todo o processo de solução de problemas. Todo o processo revelou muitos problemas para mim. O mais importante é " Não estou familiarizado com a alocação de memória de diferentes versões do JVM" , o que levou a erros de julgamento da geração anterior e do metaespaço. , Fiz muitos desvios e passei muito tempo investigando na memória direta.

Em segundo lugar, a investigação precisa de “um é cuidadoso e o outro é abrangente.” É melhor separar todas as possibilidades primeiro, caso contrário, é fácil cair no escopo da investigação definido por você mesmo e entrar em um beco sem saída.

Finalmente, some os ganhos com este problema:

  • A partir de JDK 1.8, a máquina virtual do ponto de host que vem com ele cancela a área permanente anterior e adiciona uma área de metaspace. Do ponto de vista funcional, o metaspace pode ser considerado semelhante à área permanente. Sua função principal é armazenar metadados, mas O mecanismo real é bastante diferente.

  • A memória na JVM precisa ser restrita na inicialização, incluindo a familiar memória heap, mas também a memória direta e a área de meta-geração.Este é o último pacote para garantir a operação normal dos serviços online.

  • Ao usar bibliotecas de classes, preste mais atenção à maneira como o código é escrito e tente não mostrar vazamentos de memória óbvios.

  • Para bibliotecas que usam ferramentas de aprimoramento de bytecode, como ASM, tome cuidado ao usá-las (especialmente após JDK 1.8).

referência

Observe o processo de carregamento da classe quando o programa está em execução

blog.csdn.net/tenderhearted/article/details/39642275

Introdução geral do metaespaço (a razão pela qual a geração permanente é substituída, as características do metaespaço, o método de análise da memória do metaespaço)

https://www.cnblogs.com/duanxz/p/3520829.html

Procedimentos de solução de problemas comuns para exceções de uso de memória java (incluindo exceções de memória off-heap)

https://my.oschina.net/haitaohu/blog/3024843

Interpretação completa da memória off-heap da análise de código-fonte JVM

http://lovestblog.cn/blog/2015/05/12/direct-buffer/

Desinstalação de classes JVM

https://www.cnblogs.com/caoxb/p/12735525.html

fastjson abre asm em jdk1.8

https://github.com/alibaba/fastjson/issues/385

fastjson : PropertyNamingStrategy_cn

https://github.com/alibaba/fastjson/wiki/PropertyNamingStrategy_cn

Desconfie de vazamentos de memória Metaspace causados ​​por agentes dinâmicos

https://blog.csdn.net/xyghehehehe/article/details/78820135


Não tem jeito, mas a técnica pode ser alcançada; se não tem jeito acaba com a técnica

Bem-vindos a todos que sigam a conta pública do Java Way

Bom artigo, estou lendo ❤️

Acho que você gosta

Origin blog.csdn.net/hollis_chuang/article/details/108525633
Recomendado
Clasificación