Fragmentação de heap de objeto grande

97

O aplicativo C # / .net em que estou trabalhando está sofrendo de um lento vazamento de memória. Usei o CDB com SOS para tentar determinar o que está acontecendo, mas os dados não parecem fazer sentido, então esperava que um de vocês já tivesse experimentado isso antes.

O aplicativo está sendo executado na estrutura de 64 bits. Ele está continuamente calculando e serializando dados para um host remoto e está atingindo o Large Object Heap (LOH) um pouco. No entanto, espero que a maioria dos objetos LOH sejam transitórios: assim que o cálculo for concluído e enviado ao host remoto, a memória deve ser liberada. O que estou vendo, no entanto, é um grande número de matrizes de objetos (vivos) intercalados com blocos livres de memória, por exemplo, pegando um segmento aleatório do LOH:

0:000> !DumpHeap 000000005b5b1000  000000006351da10
         Address               MT     Size
...
000000005d4f92e0 0000064280c7c970 16147872
000000005e45f880 00000000001661d0  1901752 Free
000000005e62fd38 00000642788d8ba8     1056       <--
000000005e630158 00000000001661d0  5988848 Free
000000005ebe6348 00000642788d8ba8     1056
000000005ebe6768 00000000001661d0  6481336 Free
000000005f214d20 00000642788d8ba8     1056
000000005f215140 00000000001661d0  7346016 Free
000000005f9168a0 00000642788d8ba8     1056
000000005f916cc0 00000000001661d0  7611648 Free
00000000600591c0 00000642788d8ba8     1056
00000000600595e0 00000000001661d0   264808 Free
...

Obviamente, eu esperaria que esse fosse o caso se meu aplicativo estivesse criando objetos grandes e de longa duração durante cada cálculo. (Ele faz isso e eu aceito que haverá um grau de fragmentação do LOH, mas esse não é o problema aqui.) O problema são as matrizes de objetos muito pequenas (1056 bytes) que você pode ver no despejo acima, que não consigo ver no código sendo criados e que permanecem enraizados de alguma forma.

Observe também que o CDB não está relatando o tipo quando o segmento de heap é despejado: Não tenho certeza se isso está relacionado ou não. Se eu despejar o objeto marcado (<-), o CDB / SOS relatará bem:

0:015> !DumpObj 000000005e62fd38
Name: System.Object[]
MethodTable: 00000642788d8ba8
EEClass: 00000642789d7660
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Type: System.Object
Fields:
None

Os elementos do array de objetos são todos strings e os strings são reconhecíveis a partir do código do nosso aplicativo.

Além disso, não consigo encontrar suas raízes de GC, pois o comando! GCRoot trava e nunca mais volta (até tentei deixá-lo durante a noite).

Portanto, eu apreciaria muito se alguém pudesse esclarecer por que essas matrizes de objetos pequenos (<85k) estão terminando no LOH: em quais situações o .NET colocará uma matriz de objetos pequena lá? Além disso, por acaso alguém conhece uma maneira alternativa de determinar as raízes desses objetos?


Atualização 1

Outra teoria que desenvolvi ontem é que essas matrizes de objetos começaram grandes, mas foram reduzidas, deixando os blocos de memória livre que são evidentes nos despejos de memória. O que me deixa desconfiado é que as matrizes de objetos sempre parecem ter 1.056 bytes de comprimento (128 elementos), 128 * 8 para as referências e 32 bytes de overhead.

A ideia é que talvez algum código não seguro em uma biblioteca ou no CLR esteja corrompendo o campo de número de elementos no cabeçalho da matriz. Um pouco difícil, eu sei ...


Atualização 2

Graças a Brian Rasmussen (veja a resposta aceita), o problema foi identificado como fragmentação do LOH causada pela tabela interna da string! Eu escrevi um aplicativo de teste rápido para confirmar isso:

static void Main()
{
    const int ITERATIONS = 100000;

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = "NonInterned" + index;
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue.");
    Console.In.ReadLine();

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = string.Intern("Interned" + index);
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue?");
    Console.In.ReadLine();
}

O aplicativo primeiro cria e desreferencia strings exclusivas em um loop. Isso é apenas para provar que a memória não vaza neste cenário. Obviamente, não deveria e não é.

No segundo loop, strings exclusivas são criadas e internadas. Esta ação os enraíza na tabela interna. O que não percebi é como a mesa interna é representada. Parece que consiste em um conjunto de páginas - matrizes de objetos de 128 elementos de string - que são criadas no LOH. Isso é mais evidente no CDB / SOS:

0:000> .loadby sos mscorwks
0:000> !EEHeap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00f7a9b0
generation 1 starts at 0x00e79c3c
generation 2 starts at 0x00b21000
ephemeral segment allocation context: none
 segment    begin allocated     size
00b20000 00b21000  010029bc 0x004e19bc(5118396)
Large object heap starts at 0x01b21000
 segment    begin allocated     size
01b20000 01b21000  01b8ade0 0x00069de0(433632)
Total Size  0x54b79c(5552028)
------------------------------
GC Heap Size  0x54b79c(5552028)

Fazer um dump do segmento LOH revela o padrão que vi no aplicativo com vazamento:

0:000> !DumpHeap 01b21000 01b8ade0
...
01b8a120 793040bc      528
01b8a330 00175e88       16 Free
01b8a340 793040bc      528
01b8a550 00175e88       16 Free
01b8a560 793040bc      528
01b8a770 00175e88       16 Free
01b8a780 793040bc      528
01b8a990 00175e88       16 Free
01b8a9a0 793040bc      528
01b8abb0 00175e88       16 Free
01b8abc0 793040bc      528
01b8add0 00175e88       16 Free    total 1568 objects
Statistics:
      MT    Count    TotalSize Class Name
00175e88      784        12544      Free
793040bc      784       421088 System.Object[]
Total 1568 objects

Observe que o tamanho da matriz do objeto é 528 (em vez de 1056) porque minha estação de trabalho é de 32 bits e o servidor de aplicativos é de 64 bits. As matrizes de objetos ainda têm 128 elementos.

Portanto, a moral desta história é ser um estagiário muito cuidadoso. Se a string que você está internando não é conhecida como membro de um conjunto finito, seu aplicativo vazará devido à fragmentação do LOH, pelo menos na versão 2 do CLR.

No caso de nosso aplicativo, há um código geral no caminho do código de desserialização que internaliza os identificadores de entidade durante o desempacotamento: Agora suspeito fortemente que este é o culpado. No entanto, as intenções do desenvolvedor eram obviamente boas, pois eles queriam ter certeza de que, se a mesma entidade for desserializada várias vezes, apenas uma instância da string do identificador será mantida na memória.

Paul Ruane
fonte
2
Ótima pergunta - tenho notado a mesma coisa em meu aplicativo. Pequenos objetos deixados no LOH após os grandes blocos serem limpos, o que causa problemas de fragmentação.
Reed Copsey
2
Eu concordo, ótima pergunta. Eu estarei esperando por respostas.
Charlie Flowers,
2
Muito interessante. Parece que foi um problema para depurar!
Matt Jordan

Respostas:

46

O CLR usa o LOH para pré-alocar alguns objetos (como a matriz usada para strings internas ). Alguns deles têm menos de 85000 bytes e, portanto, normalmente não seriam alocados no LOH.

É um detalhe de implementação, mas suponho que a razão para isso seja evitar a coleta de lixo desnecessária de instâncias que deveriam sobreviver enquanto o próprio processo.

Também devido a uma otimização um tanto esotérica, qualquer um double[]dos 1000 ou mais elementos também é alocado no LOH.

Brian Rasmussen
fonte
Os objetos problemáticos são object [] s contendo referências a strings que eu sei que estão sendo criadas pelo código do aplicativo. Isso implica que o aplicativo está criando os objetos [] (não consigo ver evidências disso) ou que alguma parte do CLR (como a serialização) os está usando para trabalhar nos objetos do aplicativo.
Paul Ruane
1
Essa poderia ser a estrutura interna usada para strings internas. Por favor, verifique minha resposta a esta pergunta para mais detalhes: stackoverflow.com/questions/372547/…
Brian Rasmussen
Ah, essa é uma pista muito interessante, obrigado. Esqueci completamente a mesa interna. Eu sei que um de nossos desenvolvedores é um excelente interno, então isso é algo que definitivamente irei investigar.
Paul Ruane
1
85000 bytes ou 84 * 1024 = 87040 bytes?
Peter Mortensen
5
85000 bytes. Você pode verificar isso criando uma matriz de bytes de 85000-12 (tamanho do comprimento, MT, bloco de sincronização) e chamando GC.GetGenerationa instância. Isso retornará Gen2 - a API não distingue entre Gen2 e LOH. Faça o array um byte menor e a API retornará Gen0.
Brian Rasmussen
13

O .NET Framework 4.5.1 tem a capacidade de compactar explicitamente a grande pilha de objetos (LOH) durante a coleta de lixo.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

Veja mais informações em GCSettings.LargeObjectHeapCompactionMode

Andre Abrantes
fonte
2

Ao ler as descrições de como o GC funciona, e a parte sobre como os objetos de longa duração terminam na geração 2, e a coleção de objetos LOH acontece apenas na coleção completa - como faz a coleção da geração 2, a ideia que vem à mente é. .. por que não manter a geração 2 e os objetos grandes no mesmo heap, pois eles serão coletados?

Se isso realmente acontecer, isso explicaria como pequenos objetos acabam no mesmo lugar que o LOH - se eles têm vida longa o suficiente para chegar à geração 2.

E assim o seu problema parece ser uma refutação muito boa à ideia que me ocorre - isso resultaria na fragmentação do LOH.

Resumo: seu problema pode ser explicado pelo LOH e geração 2 compartilhando a mesma região de heap, embora isso não seja de forma alguma prova de que essa é a explicação.

Atualização: o resultado de !dumpheap -statpraticamente explode essa teoria! A geração 2 e LOH têm suas próprias regiões.

Daniel Earwicker
fonte
Use! Eeheap para mostrar os segmentos que compõem cada heap. Gen 0 e gen 1 vivem em um segmento (o mesmo segmento), gen 2 e LOH podem alocar vários segmentos, mas esses segmentos para cada heap permanecem separados.
Paul Ruane
Sim, vi isso, obrigado. Queria apenas mencionar o comando! Eeheaps, pois mostra esse comportamento de uma maneira muito mais clara.
Paul Ruane
A eficiência do GC principal decorre em grande parte do fato de que ele pode realocar objetos de forma que haverá apenas um pequeno número de regiões livres de memória no heap principal. Se um objeto no heap principal for fixado durante uma coleção, o espaço acima e abaixo do objeto fixado pode ter que ser rastreado separadamente, mas como o número de objetos fixados é normalmente muito pequeno, o mesmo será o número de áreas separadas que o GC deve faixa. A mistura de objetos realocáveis ​​e não realocáveis ​​(grandes) no mesmo heap prejudicaria o desempenho.
supercat
Uma pergunta mais interessante é por que o .NET coloca doublearrays maiores que 1000 elementos no LOH, em vez de ajustar o GC para garantir que eles estejam alinhados em limites de 8 bytes. Na verdade, mesmo em um sistema de 32 bits, eu esperaria que, devido ao comportamento do cache, impor o alinhamento de 8 bytes em todos os objetos cujo tamanho alocado é um múltiplo de 8 bytes provavelmente seria uma vitória de desempenho. Caso contrário, embora o desempenho de um muito usado double[]alinhado ao cache seja melhor do que aquele que não é, não sei por que o tamanho se correlacionaria com o uso.
supercat
@supercat Além disso, os dois heaps se comportam de maneira muito diferente na alocação. A pilha principal é (neste momento) basicamente uma pilha de padrões de alocação - ela sempre aloca no topo, ignorando qualquer espaço livre - quando a compactação chega, os espaços livres são comprimidos. Isso torna a alocação quase autônoma e ajuda na localização dos dados. Por outro lado, a alocação no LOH é semelhante a como o malloc funciona - ele encontrará o primeiro local livre que pode conter o que você está alocando e alocará lá. Como é para objetos grandes, a localidade dos dados é um dado e a penalidade para a alocação não é tão ruim.
Luaan
1

Se o formato é reconhecível como seu aplicativo, por que você não identificou o código que está gerando esse formato de string? Se houver várias possibilidades, tente adicionar dados exclusivos para descobrir qual caminho de código é o culpado.

O fato de que os arrays são intercalados com grandes itens liberados me leva a supor que eles foram originalmente emparelhados ou pelo menos relacionados. Tente identificar os objetos liberados para descobrir o que os estava gerando e as strings associadas.

Depois de identificar o que está gerando essas strings, tente descobrir o que as estaria impedindo de serem GCed. Talvez eles estejam sendo colocados em uma lista esquecida ou não utilizada para fins de registro ou algo semelhante.


EDIT: Ignore a região da memória e o tamanho específico do array por enquanto: apenas descubra o que está sendo feito com essas strings para causar um vazamento. Experimente o! GCRoot quando seu programa tiver criado ou manipulado essas strings apenas uma ou duas vezes, quando houver menos objetos para rastrear.

HUAGHAGUAH
fonte
As strings são uma mistura de Guids (que usamos) e chaves de string que são prontamente identificáveis. Eu posso ver onde eles são gerados, mas eles nunca são (diretamente) adicionados aos arrays de objetos e não criamos explicitamente os arrays de 128 elementos. No entanto, esses pequenos arrays não devem estar no LOH.
Paul Ruane
1

Ótima pergunta, aprendi lendo as perguntas.

Acho que outra parte do caminho do código de desserialização também está usando o heap de objeto grande, por isso a fragmentação. Se todas as strings fossem internadas ao mesmo tempo, acho que você estaria bem.

Dado o quão bom é o coletor de lixo .net, apenas permitir que o caminho do código de desserialização crie um objeto de string normal provavelmente será bom o suficiente. Não faça nada mais complexo até que a necessidade seja comprovada.

No máximo, eu tentaria manter uma tabela hash das últimas strings que você viu e reutilizá-las. Limitando o tamanho da tabela de hash e passando o tamanho ao criar a tabela, você pode interromper a maior parte da fragmentação. Em seguida, você precisa de uma maneira de remover strings que não viu recentemente da tabela de hash para limitar seu tamanho. Mas se as strings criadas pelo caminho do código de desserialização tiverem vida curta, você não ganhará muito, se é que alguma coisa.

Ian Ringrose
fonte
1

Aqui estão algumas maneiras de identificar a pilha de chamadas exata da alocação de LOH .

E para evitar a fragmentação do LOH Pré-aloque uma grande variedade de objetos e fixe-os. Reutilize esses objetos quando necessário. Aqui está uma postagem sobre Fragmentação de LOH. Algo como isso pode ajudar a evitar a fragmentação do LOH.

Naveen
fonte
Não consigo ver por que fixar aqui deve ajudar? BTW, objetos grandes em LOH não são movidos pelo GC de qualquer maneira. Porém, é um detalhe de implementação.
user492238