Fragmentação de heap de objects grandes

O aplicativo C # / .net que estou trabalhando está sofrendo de um memory leaks lenta. Eu tenho usado CDB com SOS para tentar determinar o que está acontecendo, mas os dados não parecem fazer qualquer sentido, então eu estava esperando que um de vocês tenha experimentado isso antes.

O aplicativo está sendo executado na estrutura de 64 bits. Ele está continuamente calculando e serializando os dados para um host remoto e está atingindo o Large Object Heap (LOH). No entanto, a maioria dos objects LOH que eu espero que sejam transitórios: uma vez que o cálculo esteja completo e tenha sido enviado para o host remoto, a memory deve ser liberada. O que estou vendo, no entanto, é um grande número de arrays de objects (vivos) intercalados com blocos livres de memory, por exemplo, pegando um segmento random 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 objects grandes e duradouros durante cada cálculo. (Ele faz isso e eu aceito que haverá um grau de fragmentação de LOH, mas esse não é o problema aqui.) O problema são os arrays de objects muito pequenos (1056 bytes) que você pode ver no dump acima que não consigo ver no código sendo criado e que estão permanecendo 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 despejo o object marcado (<-), o CDB / SOS informa tudo 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 da matriz de objects são todos strings e as strings são reconhecíveis a partir do código da nossa aplicação.

Além disso, eu sou incapaz de encontrar suas raízes GC como o comando! GCRoot trava e nunca volta (eu tentei mesmo deixá-lo durante a noite).

Então, eu agradeceria muito se alguém pudesse lançar alguma luz sobre por que esses pequenos (<85k) arrays de objetos estão acabando no LOH: que situações o .NET colocará um pequeno array de objetos lá? Além disso, alguém sabe de uma maneira alternativa de averiguar as raízes desses objetos?


Atualização 1

Outra teoria que surgiu ontem é que essas matrizes de objects começaram grandes, mas foram reduzidas deixando os blocos de memory livre que são evidentes nos despejos de memory. O que me faz suspeitar é que as matrizes de objects sempre parecem ter 1056 bytes de comprimento (128 elementos), 128 * 8 para as referências e 32 bytes de sobrecarga.

A ideia é que talvez algum código inseguro em uma biblioteca ou no CLR esteja corrompendo o campo de número de elementos no header da matriz. Pouco de um tiro longo eu sei …


Atualização 2

Graças a Brian Rasmussen (ver resposta aceita) o problema foi identificado como fragmentação do LOH causada pela tabela interna de 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 cancela referências de strings exclusivas em um loop. Isso é apenas para provar que a memory não vaza nesse 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 mesa interna. O que eu não percebi é como a tabela interna é representada. Parece que consiste em um conjunto de páginas – arrays de objects de 128 elementos de string – que são criados 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) 

Tirar 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 de objects é 528 (em vez de 1056) porque minha estação de trabalho é de 32 bits e o servidor de aplicativos é de 64 bits. Os arrays de objects ainda têm 128 elementos de comprimento.

Portanto, a moral dessa história é ter muito cuidado em internar. Se a string que você está internando não é conhecida por ser um membro de um conjunto finito, então seu aplicativo irá 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 identifica os identificadores de entidade durante o desmarque: agora suspeito fortemente que esse seja o culpado. No entanto, as intenções do desenvolvedor eram obviamente boas, pois queriam ter certeza de que, se a mesma entidade fosse desserializada várias vezes, apenas uma instância da cadeia de identificadores seria mantida na memory.

    O CLR usa o LOH para pré-alocar alguns objects (como o array usado para strings internadas ). Alguns deles têm menos de 85.000 bytes e, portanto, normalmente não seriam alocados no LOH.

    É um detalhe de implementação, mas suponho que a razão para isso é evitar a garbage collection desnecessária de instâncias que supostamente sobreviverão enquanto o processo for necessário.

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

    O .NET Framework 4.5.1, tem a capacidade de compactar explicitamente o heap de object grande (LOH) durante a garbage collection.

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

    Veja mais informações em GCSettings.LargeObjectHeapCompactionMode

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

    Se é isso que realmente acontece, então explicaria como pequenos objects acabam no mesmo lugar que o LOH – se eles tiverem vida longa o suficiente para acabar na segunda geração.

    Assim, o seu problema parece ser uma boa resposta à ideia que me ocorre – resultaria na fragmentação do LOH.

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

    Atualização: a saída de !dumpheap -stat praticamente sopra essa teoria da água! A geração 2 e o LOH têm suas próprias regiões.

    Se o formato for 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 os arrays serem intercalados com itens grandes e liberados me leva a supor que eles foram originalmente pareados ou pelo menos relacionados. Tente identificar os objects liberados para descobrir o que os estava gerando e as cadeias associadas.

    Depois de identificar o que está gerando essas cadeias, tente descobrir o que estaria impedindo que elas fossem geradas. Talvez eles estão sendo recheados em uma lista esquecida ou não utilizada para fins de registro ou algo similar.


    EDIT: Ignore a região de memory e o tamanho de matriz específico para o momento: apenas descobrir o que está sendo feito com essas seqüências de caracteres para causar um vazamento. Experimente o! GCRoot quando seu programa criou ou manipulou essas strings apenas uma ou duas vezes, quando há menos objects para rastrear.

    Ótima pergunta, aprendi lendo as perguntas.

    Acho que outro bit do caminho do código de desserialização também está usando o heap de object grande, daí a fragmentação. Se todas as cordas foram internadas ao mesmo tempo, eu 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 objects de cadeia normal provavelmente será bom o suficiente. Não faça nada mais complexo até que a necessidade seja comprovada.

    Eu iria, no máximo, olhar para manter uma tabela de hash das últimas cadeias de caracteres 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 maioria da fragmentação. Você precisa então de uma maneira de remover strings que você 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 forem de curta duração, você não ganhará muito se tiver alguma coisa.

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

    E para evitar a fragmentação LOH Pré-alocar grande variedade de objects e fixá-los. Reutilize esses objects quando necessário. Aqui está o post sobre Fragmentação LOH. Algo como isso poderia ajudar a evitar a fragmentação de LOH.

    De acordo com http://webcache.googleusercontent.com/search?q=cache:l3Ppy_eFoAAJ:www.wintellect.com/devcenter/sloscialo/hey-who-stole-all-my-memory+&cd=5&hl=en&ct=clnk&gl= mx e cliente = firefox-b

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