Introdução
Um vazamento de memória é geralmente chamado de situação quando a quantidade de memória ocupada no heap aumenta durante a operação de longo prazo do aplicativo e não diminui após a saída do Coletor de Lixo. Como você sabe, a memória jvm é dividida em heap e pilha. A pilha armazena os valores de variáveis de tipos simples e referências a objetos no contexto do fluxo, e o heap armazena os próprios objetos. Também no heap existe um espaço denominado Metaspace, que armazena dados sobre classes carregadas e dados vinculados às próprias classes, e não às suas instâncias, em particular, os valores das variáveis estáticas. O Garbage Collector (doravante GC), lançado periodicamente pela máquina java, encontra objetos no heap que não são mais referenciados e libera a memória ocupada por esses objetos. Os algoritmos de trabalho do GC são diferentes e complexos, em particular,na próxima vez que o GC for iniciado, ele não "examinará" todo o heap sempre para encontrar objetos não usados, então não vale a pena confiar no fato de que qualquer objeto não usado será removido da memória após um início do GC, mas se a quantidade de memória usada pelo aplicativo for estável cresce sem razão aparente por um longo tempo, então é hora de pensar no que poderia ter levado a tal situação.
O jvm inclui um utilitário multifuncional Visual VM (doravante denominado VM). VM permite que você observe visualmente a dinâmica dos indicadores-chave do jvm nos gráficos, em particular, a quantidade de memória livre e ocupada no heap, o número de classes carregadas, threads, etc. Além disso, usando a VM, você pode obter e examinar despejos de memória. Obviamente, a VM também permite o despejo de encadeamentos e a criação de perfis de aplicativos, mas uma visão geral desses recursos está além do escopo deste artigo. Tudo o que precisamos da VM neste exemplo é conectar-se à máquina virtual e primeiro olhar para o quadro geral do uso de memória. Gostaria de ressaltar que para conectar uma VM a um servidor remoto, os parâmetros jmxremote devem ser configurados nele, pois a conexão é via jmx.Para obter uma descrição desses parâmetros, você pode consultar a documentação oficial da Oracle ou vários artigos sobre Habré.
Portanto, vamos supor que nos conectamos com sucesso ao servidor de aplicativos usando a VM e dê uma olhada nos gráficos.
Na guia Heap, você pode ver a memória total e usada do jvm. Deve-se notar que esta guia também leva em consideração a memória do tipo Metaspace (bem, de que outra forma, porque também é um heap). A guia Metaspace exibe informações apenas sobre a memória ocupada pelos metadados (pelas próprias classes e objetos vinculados a elas).
Olhando para o gráfico, podemos ver que a memória heap total é de ~ 10 GB, o espaço ocupado atualmente é de ~ 5,8 GB. As cristas no gráfico correspondem às chamadas GC, uma linha quase reta (sem cristas) começando por volta das 10:18 pode (mas não necessariamente!) Indicar que o servidor de aplicativos estava quase fora de serviço a partir desse momento, uma vez que não havia alocação e liberação ativa memória. Em geral, este gráfico corresponde ao funcionamento normal do servidor de aplicativos (se, é claro, para julgar o trabalho apenas a partir da memória). O gráfico do problema seria aquele em que uma linha azul horizontal reta sem saliências ficaria mais ou menos na linha laranja, que representa a quantidade máxima de memória no heap.
Agora vamos dar uma olhada em outro gráfico.
Aqui chegamos diretamente à análise do exemplo, que é o tópico principal deste artigo. O gráfico Classes mostra o número de classes carregadas no Metaspace, e é de aproximadamente 73 mil objetos. Gostaria de chamar a atenção para o fato de que não estamos falando de instâncias de classes, mas das próprias classes, ou seja, objetos do tipo Class <?>. O gráfico não mostra quantas instâncias de cada tipo individual de ClassA ou ClassB são carregadas na memória. Talvez o número de classes idênticas do tipo ClassA por algum motivo se multiplique? Devo dizer que no exemplo que será descrito a seguir, 73.000 classes únicas era uma situação absolutamente normal.
O fato é que em um dos projetos em que o autor deste artigo participou, foi desenvolvido um mecanismo de descrição universal de entidades de domínio (como em 1C), denominado sistema de dicionário, e analistas que customizam o sistema para um cliente específico ou para uma área de negócio específica, teve a oportunidade, através de um editor especial, de modelar um modelo de negócio criando novas e alterando entidades existentes, operando não no nível de tabelas, mas com conceitos como "Documento", "Conta", "Empregado", etc. O kernel do sistema criava tabelas em um SGBD relacional para dados de entidade, e várias tabelas podiam ser criadas para cada entidade, já que o sistema universal permitia o armazenamento histórico de valores de atributos e muito mais exigindo a criação de tabelas de serviço adicionais no banco de dados.
Acredito que quem teve que trabalhar com frameworks ORM já adivinhou do que se tratava o autor, distraiu-se do tema principal do artigo falando sobre tabelas. O projeto usava Hibernate e para cada tabela deveria haver uma classe de bean Entity. Ao mesmo tempo, como novas tabelas foram criadas dinamicamente durante o trabalho do sistema pelos analistas, as classes do Hibernate Bean foram geradas, e não escritas manualmente pelos desenvolvedores. E com cada geração seguinte, cerca de 50-60 mil novas classes foram criadas. Havia significativamente menos tabelas no sistema (cerca de 5 a 6 mil), mas para cada tabela, não apenas a classe do bean Entity foi gerada, mas também muitas classes auxiliares, o que acabou levando a um número comum.
O mecanismo de trabalho foi o seguinte. No início do sistema, as classes Entity bean e auxiliares (doravante simplesmente bean classes) foram geradas com base nos metadados do banco de dados. Quando o sistema estava rodando, a fábrica de sessões do Hibernate criava sessões, sessões criavam instâncias de objetos de classe de bean. Ao alterar a estrutura (adicionar, alterar tabelas), as classes de bean foram regeneradas e uma nova fábrica de sessão foi criada. Após a regeneração, a nova fábrica criou novas sessões que usavam as novas classes de bean, a fábrica e as sessões antigas foram fechadas e as classes de bean antigas foram descarregadas pelo GC, uma vez que não eram mais referenciadas a partir dos objetos de infraestrutura do Hibernate.
Em algum ponto, surgiu um problema de que o número de classes de bin começou a aumentar após cada próxima regeneração. Obviamente, isso se devia ao fato de que o antigo conjunto de classes, que não deveria mais ser usado, por algum motivo não foi descarregado da memória. Para entender as razões desse comportamento do sistema, o Eclipse Memory Analizer (MAT) veio em nosso auxílio.
Encontrando um vazamento de memória
O MAT é capaz de trabalhar com despejos de memória, encontrando possíveis problemas neles, mas primeiro você precisa obter esse despejo de memória, mas em ambientes reais existem certas nuances com a obtenção de um despejo.
Removendo um despejo de memória
Conforme mencionado acima, o despejo de memória pode ser removido diretamente da VM pressionando o botão
Mas, devido ao grande tamanho do despejo, a VM pode simplesmente não lidar com esta tarefa, congelando algum tempo após pressionar o botão Heap Dump. Além disso, não é um fato que será possível se conectar via jmx ao servidor de aplicativos do produto necessário para a VM. Nesse caso, outro utilitário do jvm, chamado jMap, vem em socorro. Ele é executado na linha de comando, diretamente no servidor onde jvm está sendo executado e permite que você defina parâmetros de dump adicionais:
jmap -dump: live, format = b, file = / tmp / heapdump.bin 14616
O parâmetro –dump: live é extremamente importante, uma vez que permite que você reduza significativamente seu tamanho, excluindo objetos que não são mais referenciados.
Outra situação comum é quando o despejo manual não é possível devido ao fato de que o próprio jvm falha com um OutOfMemoryError. Nessa situação, a opção -XX: + HeapDumpOnOutOfMemoryError vem em nosso socorro e, além disso, -XX: HeapDumpPath , que permite especificar o caminho para o dump capturado.
A seguir, abra o dump capturado usando o Eclipse Memory Analizer. O arquivo pode ser grande (vários gigabytes), então você precisa fornecer memória suficiente no arquivo
MemoryAnalyzer.ini : -Xmx4096m
Localizando o problema usando MAT
Portanto, vamos considerar uma situação em que o número de classes carregadas aumenta de múltiplos em comparação ao nível inicial e não diminui mesmo após uma chamada forçada para a coleta de lixo (isso pode ser feito pressionando o botão correspondente na VM).
Acima, o processo de regeneração das classes de feijão e seu uso foi descrito conceitualmente. Em um nível mais técnico, parecia assim:
- Todas as sessões do Hibernate são fechadas (classe SessionImpl)
- A fábrica de sessão antiga (SessionFactoryImpl) é fechada e a referência a ela do LocalSessionFactoryBean é redefinida
- ClassLoader é recriado
- As referências a classes de bean antigas na classe geradora são anuladas
- Classes de feijão são regeneradas
Na ausência de referências a classes de bean antigas, o número de classes não deve aumentar após a coleta de lixo.
Inicie o MAT e abra o arquivo de despejo de memória obtido anteriormente. Depois de abrir o dump, o MAT exibe as maiores cadeias de objetos na memória.
Após clicar em Suspeitos de Vazamento, vemos os detalhes:
2 segmentos de círculo de 265 M cada são 2 instâncias de SessionFactoryImpl. Não está claro por que existem 2 instâncias deles e, muito provavelmente, cada uma das instâncias contém referências ao conjunto completo de classes de bean de entidade. O MAT nos informa sobre os problemas potenciais da seguinte maneira.
Observo imediatamente que o Problema Suspeito 3 não é realmente um problema. O projeto implementou um analisador de linguagem própria, que é um add-on multiplataforma sobre SQL e permite operar não com tabelas, mas com entidades do sistema, e 121M ocupa seu cache de consulta.
Vamos voltar a duas instâncias de SessionFactoryImpl. Clique em Duplicate Classes e veja que existem na verdade 2 instâncias de cada classe de bean de entidade. Ou seja, os links para as classes antigas dos beans Entity permanecem e, muito provavelmente, são links do SesssionFactoryImpl. Com base no código-fonte desta classe, as referências às classes de bean devem ser armazenadas no campo classMetaData.
Clique em Problem Suspect 1, então na classe SessionFactoryImpl e selecione List Objects-> With Outgouing References no menu de contexto. Dessa forma, podemos ver todos os objetos referenciados por SessionFactoryImpl.
Expanda o objeto classMetaData e certifique-se de que ele realmente armazena uma matriz de classes de bean de entidade.
Agora precisamos entender o que impede o coletor de lixo de descartar uma única instância de SessionFactoryImpl. Se voltarmos para Leak Suspects-> Leaks-> Problem Suspect 1, veremos uma pilha de links que leva a um link para SessionFactoryImpl.
Vemos que a variável entityManager do bean SessionInfoImpl contendo o contexto da sessão HTTP tem um array dbTransactionListeners que usa objetos SessionImpl do Hibernate como chaves, e as sessões referem-se a SessionFactoryImpl.
O fato é que os objetos de sessão foram armazenados em cache em dbTransactionListeners para certos propósitos e, antes que as classes de bean fossem regeneradas, as referências a eles poderiam permanecer neste array. As sessões, por sua vez, faziam referência à fábrica de sessões, que armazenava uma matriz de referências para todas as classes de bean. Além disso, as sessões mantinham referências a instâncias de classes de entidade e referiam as próprias classes de bean.
Assim, foi encontrado o ponto de entrada para o problema. Tratava-se de referências a sessões antigas de dbTransactionListeners. Depois que o erro foi corrigido e a matriz dbTransactionListeners começou a ser limpa, o problema foi corrigido.
Recursos do Analisador de Memória Eclipse
Portanto, o Eclipse Memory Analyzer permite que você:
- Descubra quais cadeias de objetos ocupam a quantidade máxima de memória e determine os pontos de entrada nessas cadeias (Suspeitos de Vazamento)
- Visualize uma árvore de todas as referências de objetos de entrada (caminhos mais curtos para o ponto de acumulação)
- Visualize a árvore de todas as referências de saída de um objeto (Objeto-> Listar Objetos-> Com referências de saída)
- Veja classes duplicadas carregadas por ClassLoaders diferentes (classes duplicadas)