Argumentar: depois de ler até o fim, você entenderá como e por que exatamente o GC funciona

Eu direi imediatamente: nunca espero uma resposta detalhada a esta pergunta sobre segurança social. Isso é estúpido e, no meu caso, egoísta. No entanto, na minha opinião, além do interesse geral na plataforma, é muito útil saber como ela funciona, porque isso remove uma série de problemas. Por exemplo, exclui a opção quando o desenvolvedor acredita que Dispose é chamado automaticamente e você não precisa chamá-lo. Ou, se o desenvolvedor é mais experiente, ajuda-o automaticamente, no nível da memória muscular, a escrever um código que leve ao menor número de problemas.


Outra questão que eu realmente não gosto subjetivamente é como o trabalho dele é explicado. Portanto, proponho uma abordagem alternativa descrita em meu livro, .NET Platform Architecture .


Se quisermos entender completamente por que esses dois algoritmos de gerenciamento de memória foram escolhidos: Sweep e Compact, teremos que considerar dezenas de algoritmos de gerenciamento de memória existentes no mundo: começando com dicionários comuns e terminando com estruturas livres de bloqueios muito complexas. Em vez disso, deixando nossos pensamentos sobre o que é útil, simplesmente justificamos a escolha e, assim, entendemos por que a escolha foi feita dessa maneira. Não olhamos mais para o livreto de lançamento do booster: temos em nossas mãos um conjunto completo de documentação.


A disputa é mutuamente benéfica: se não estiver claro, corrigirei os pontos pouco claros do livro , uma pequena parte do qual é o texto fornecido.



Escolhi o formato do raciocínio para que você sinta que os arquitetos da plataforma e eu chegamos às mesmas conclusões que os arquitetos reais chegaram na sede da Microsoft em Redmond.

Com base na classificação de objetos alocados com base em seu tamanho, é possível dividir o espaço para alocação de memória em duas seções grandes: um local com objetos dimensionados abaixo de um determinado limite e um local com um tamanho acima desse limite e ver que diferença pode ser feita no gerenciamento desses grupos (com base em tamanho) e o que resulta disso.


Se considerarmos o gerenciamento de objetos " pequenos " convencionalmente, podemos ver que, se aderirmos à idéia de armazenar informações sobre cada objeto, será muito caro mantermos estruturas de dados de gerenciamento de memória que armazenarão links para cada um desses objetos. No final, pode acontecer que, para armazenar informações sobre um objeto, você precise de tanta memória quanto o próprio objeto. Em vez disso, você deve considerar: se durante a coleta de lixo dançamos desde as raízes, aprofundando o gráfico nos campos de saída do objeto, e precisamos de uma passagem linear ao longo da pilha apenas para identificar objetos de lixo, precisamos armazenar informações sobre cada objeto nos algoritmos de gerenciamento de memória? A resposta é óbvia: não há necessidade disso. Portanto, podemos tentar prosseguir com o fato de que não devemos armazenar essas informações: podemos passar por um monte linearmente, conhecendo o tamanho de cada objeto e movendo o ponteiro a cada vez pelo tamanho do próximo objeto.


Não há estruturas de dados adicionais no heap que contêm ponteiros para cada objeto que o heap controla.

No entanto, quando não precisamos mais de memória, devemos liberá-la. E ao liberar memória, torna-se difícil confiar na passagem linear da pilha: ela é longa e não é eficaz. Como resultado, chegamos à conclusão de que precisamos armazenar informações de alguma forma sobre áreas de memória livre.


A pilha possui listas de memória livre.

Se, como decidimos, armazenar informações sobre áreas livres e, ao liberar memória, essas áreas eram muito pequenas, primeiro chegamos ao mesmo problema de armazenar informações sobre áreas livres que encontramos ao considerar áreas ocupadas (se nas laterais do objeto ocupado foi liberado, para armazenar informações sobre ele, é necessário, na pior das hipóteses, 2/3 do seu tamanho. Ponteiro + tamanho versus SyncBlockIndex + VMT + algum campo - no caso do objeto). Isso novamente parece um desperdício, você deve admitir: nem sempre é boa sorte liberar um grupo de objetos que se seguem. Geralmente, eles são liberados de maneira caótica. Mas, diferentemente dos sites ocupados, que não precisamos pesquisar linearmente, precisamos procurar sites gratuitos porque, quando alocamos memória, podemos precisar deles novamente. Portanto, surge um desejo completamente natural de reduzir a fragmentação e espremer a pilha, movendo todas as áreas ocupadas para locais livres, formando assim uma grande área da área livre onde a memória pode ser alocada.


É daí que surge a idéia do algoritmo de compactação.

Mas espere, você diz. Afinal, esta operação pode ser muito difícil. Imagine que você liberou um objeto no início do heap. E o que, você diz, você precisa mover tudo? Bem, é claro, você pode sonhar com o assunto de instruções vetoriais da CPU, que você pode usar para copiar uma enorme área ocupada de memória. Mas este é apenas o começo do trabalho. Também devemos corrigir todos os ponteiros dos campos de objetos para objetos que foram submetidos a movimentos. Esta operação pode demorar muito tempo. Não, devemos proceder de outra coisa. Por exemplo, dividindo todo o segmento da memória heap em setores e trabalhando com eles separadamente. Se trabalharmos separadamente em cada setor (para previsibilidade e escalabilidade dessa previsibilidade - de preferência tamanhos fixos), a ideia de compactação não parece tão pesada: basta compactar um único setor e você pode começar a falar sobre o tempo necessário para compactar um desses setores .


Agora resta entender em que base se dividir em setores. Aqui devemos nos voltar para a segunda classificação, que é introduzida na plataforma: compartilhamento de memória, com base no tempo de vida de seus elementos individuais.


A divisão é simples: se considerarmos que alocaremos memória à medida que os endereços aumentarem, os primeiros objetos selecionados se tornarão os mais antigos e aqueles que estiverem nos endereços seniores se tornarão os mais jovens. Além disso, sendo inteligente, você pode concluir que, nos aplicativos, os objetos são divididos em dois grupos: aqueles criados para vida longa e aqueles criados para viver muito pouco. Por exemplo, para armazenar temporariamente ponteiros para outros objetos na forma de uma coleção. Ou os mesmos objetos DTO. Consequentemente, de tempos em tempos, espremendo um monte, obtemos vários objetos de vida longa - nos endereços mais baixos e vários de vida curta - nos idosos.


Assim, recebemos gerações .

Dividindo a memória em gerações, temos a oportunidade de observar com menos frequência os objetos da geração mais velha, que estão se tornando cada vez mais.


Mas surge outra questão: se tivermos apenas duas gerações, teremos problemas. Ou tentaremos fazer o GC funcionar sem máscara rapidamente: depois do tamanho da geração mais jovem, tentaremos fazer o tamanho mínimo. Como resultado, os objetos falharão acidentalmente na geração mais antiga (se o GC funcionasse "no momento, durante uma furiosa alocação de memória para muitos objetos"). Ou, para minimizar falhas acidentais, aumentaremos o tamanho da geração mais jovem. Em seguida, o GC da geração mais jovem funcionará por tempo suficiente, desacelerando e desacelerando o aplicativo.


A saída é a introdução da geração "intermediária". Adolescente. Em outras palavras, se você vive até a adolescência, é mais provável que viva até a velhice. A essência de sua introdução é alcançar um equilíbrio entre obter a menor geração mais jovem e a geração mais estável , onde é melhor não tocar em nada. Esta é uma zona em que o destino dos objetos ainda não foi decidido. A primeira geração (não esqueça o que pensamos do zero) também é criada pequena e o GC parece menos frequente lá. O GC permite, assim, que os objetos que estão na primeira geração temporária não entrem na geração mais antiga, que é extremamente difícil de coletar.


Então, tivemos a ideia de três gerações.

A próxima camada de otimização é uma tentativa de recusar a compactação. Afinal, se você não fizer isso, nos livraremos de uma enorme camada de trabalho. Vamos voltar à questão dos sites gratuitos.


Se depois de termos usado toda a memória disponível no heap e o GC tiver sido chamado, existe um desejo natural de recusar a compactação em favor da alocação de memória adicional nas seções liberadas, se o tamanho delas for suficiente para acomodar um certo número de objetos. Aqui chegamos à idéia de um segundo algoritmo para liberar memória no GC, chamado Sweep : não compactamos memória, usamos vazios de objetos liberados para colocar novos objetos


Então, descrevemos e justificamos todos os conceitos básicos dos algoritmos de GC.

Então, depois de dois dias, podemos tirar algumas conclusões. Pelo que entendi, a maioria das pessoas entende a maior parte do texto, ou mesmo o todo. Algumas pessoas responderam que não entendiam, outras, respectivamente, entendiam parcialmente. A disputa foi vencida por uma equipe de leitores, embora com uma ligeira margem, como eles dizem. Mas, como eu disse, todos serão beneficiados com isso: o texto será alterado e complementado. Além disso, atualizado nos dois lugares: no livro e aqui, no artigo.


Source: https://habr.com/ru/post/pt464169/


All Articles