Neste artigo, você encontrará duas fontes de informações ao mesmo tempo:
- Conclua o curso Garbage Collector em russo: CLRium # 6 ( workshop atual aqui )
- Tradução de um artigo do BOTR "Garbage Collector Device" de Maoni Stevens.

1. CLRium # 5: Curso Completo de Coletor de Lixo

2. Dispositivo coletor de lixo por Maoni Stephens ( @ maoni0 )
Nota: para saber mais sobre a coleta de lixo em geral, consulte O Manual de Coleta de Lixo ; informações especializadas sobre o coletor de lixo no CLR são fornecidas no livro Pro .NET Memory Management . Links para os dois recursos são fornecidos no final do documento.
Arquitetura de componentes
A coleta de lixo está associada a dois componentes: um distribuidor e um coletor. O alocador é responsável por alocar memória e chamar o coletor, se necessário. O coletor coleta lixo ou memória de objetos que não são mais usados pelo programa.
Existem outras maneiras de chamar o coletor, por exemplo manualmente, usando GC.Collect. Além disso, o encadeamento do finalizador pode receber uma notificação assíncrona de que a memória está acabando (o que causará o coletor).
Dispositivo distribuidor
O distribuidor é chamado pelos componentes auxiliares do tempo de execução com as seguintes informações:
- o tamanho necessário do gráfico alocado;
- contexto de alocação de memória para o encadeamento de execução;
- sinalizadores que indicam, por exemplo, se o objeto é finalizável.
O coletor de lixo não fornece métodos de processamento especiais para diferentes tipos de objetos. Ele recebe informações sobre o tamanho do objeto do tempo de execução.
Dependendo do tamanho, o coletor divide os objetos em duas categorias: pequeno (<85.000 bytes) e grande (> = 85.000 bytes). Em geral, a montagem de objetos pequenos e grandes pode ocorrer da mesma maneira. No entanto, o coletor os separa por tamanho, pois a compactação de objetos grandes requer muitos recursos.
O coletor de lixo aloca memória para o alocador com base em contextos de alocação. O tamanho do contexto de alocação é determinado pelos blocos de memória alocada.
Os contextos de seleção são pequenas áreas de um segmento de heap específico, cada qual destinado a um fluxo de execução específico. Em uma máquina com um processador (significando 1 processador lógico), um único contexto de alocação de memória é usado para objetos da geração 0.
Bloco de memória alocada - a quantidade de memória alocada pelo alocador sempre que precisar de mais memória para posicionar um objeto dentro da área. O tamanho do bloco geralmente é de 8 KB e o tamanho médio dos objetos gerenciados é de 35 bytes. Portanto, em um bloco você pode colocar muitos objetos.
Objetos grandes não usam contextos e blocos. Um objeto grande pode ser maior que esses pequenos pedaços de memória. Além disso, os benefícios do uso dessas áreas (descritas abaixo) são evidentes apenas ao trabalhar com objetos pequenos. O espaço para objetos grandes é alocado diretamente no segmento de heap.
O distribuidor é projetado para que:
chame o coletor de lixo quando necessário: o alocador chama o coletor quando a quantidade de memória alocada para objetos exceder o valor limite (definido pelo coletor) ou se o alocador não puder mais alocar memória nesse segmento. Limiares e segmentos controlados serão descritos em detalhes posteriormente.
salvar localização dos objetos: os objetos localizados juntos em um segmento da pilha são armazenados em endereços virtuais próximos uns dos outros.
use o cache com eficiência: o alocador aloca memória em blocos , e não para cada objeto. Zera a quantidade de memória necessária para preparar o cache do processador, pois alguns objetos serão colocados diretamente nele. O bloco de memória alocada é geralmente 8 KB.
efetivamente limitar a área alocada ao encadeamento de execução: a proximidade dos contextos e blocos de memória alocados para o encadeamento garante que apenas um encadeamento grave dados no espaço alocado. Como resultado, não há necessidade de limitar a alocação de memória até que o espaço no contexto de alocação atual termine.
garantir a integridade da memória: o coletor de lixo sempre zera a memória dos objetos recém-alocados para que seus links não aponteem para seções arbitrárias da memória.
garantir a continuidade da pilha: o alocador cria um objeto livre da memória restante em cada bloco alocado. Por exemplo, se 30 bytes forem deixados no bloco e 40 bytes forem necessários para acomodar o próximo objeto, o alocador transformará esses 30 bytes em um objeto livre e solicitará um novo bloco de memória.
API
Object* GCHeap::Alloc(size_t size, DWORD); Object* GCHeap::Alloc(alloc_context* acontext, size_t size, DWORD);
Usando essas funções, você pode alocar memória para objetos pequenos e grandes. Há uma função para alocar espaço diretamente na pilha de objetos grandes (LOH):
Object* GCHeap::AllocLHeap(size_t size, DWORD);
Dispositivo coletor
Tarefas do coletor de lixo
O GC foi projetado para gerenciamento eficiente de memória. Os desenvolvedores que escrevem código gerenciado podem usá-lo sem muito esforço. Boa governança significa:
- a coleta de lixo deve ocorrer com frequência suficiente para não sobrecarregar a pilha gerenciada com um grande número (por proporção ou em quantidade absoluta) de objetos não utilizados (lixo) para os quais a memória é alocada;
- a coleta de lixo deve ocorrer o mais raramente possível, para não desperdiçar o tempo útil do processador, embora uma coleta mais frequente permita menos uso de memória;
- a coleta de lixo deve ser produtiva, porque, como resultado da montagem, apenas uma pequena parte da memória foi liberada, a montagem e o tempo gasto do processador foram em vão;
- a coleta de lixo deve ser rápida, pois muitas cargas de trabalho exigem um pequeno tempo de atraso;
- os desenvolvedores que escrevem código gerenciado não precisam saber muito sobre a coleta de lixo para obter um uso eficiente da memória (em comparação com a carga de trabalho);
- O coletor de lixo deve se adaptar à natureza diferente do uso da memória.
Descrição lógica do heap gerenciado
O coletor de lixo CLR coleta objetos que são logicamente separados por geração. Após a montagem dos objetos na geração N , os objetos restantes são marcados como pertencentes à geração N + 1 . Esse processo é chamado de promoção de objetos entre gerações. Há exceções nesse processo quando é necessário transferir um objeto para uma geração mais baixa ou não avançar de maneira alguma.
No caso de objetos pequenos, o heap é dividido em três gerações: gen0, gen1 e gen2. Para objetos grandes, há apenas uma geração - gen3. Gen0 e gen1 são chamados de gerações efêmeras (objetos vivem neles por um curto período de tempo).
Para vários objetos pequenos, o número da geração significa a idade deles. Por exemplo, gen0 é a geração mais jovem. Isso não significa que todos os objetos em gen0 são mais novos que objetos em gen1 ou gen2. Há exceções descritas abaixo. Montar uma geração significa montar objetos nesta geração, bem como em todas as gerações mais jovens.
Teoricamente, a montagem de objetos grandes e pequenos pode ocorrer da mesma maneira. No entanto, como a compactação de objetos grandes requer muitos recursos, sua montagem ocorre de maneira diferente. Objetos grandes estão contidos apenas no gen2 e são coletados apenas durante a coleta de lixo nesta geração por motivos de desempenho. Tanto o gen2 quanto o gen3 podem ser grandes, e a construção de um objeto em gerações efêmeras (gen0 e gen1) não deve consumir muito recursos.
Os objetos são colocados na geração mais jovem. Para objetos pequenos, esse é gen0, e para objetos grandes, gen3.
Descrição física do heap gerenciado
Um heap gerenciado consiste em um conjunto de segmentos. Um segmento é um bloco contínuo de memória que o sistema operacional passa para o coletor de lixo. Os segmentos de heap são divididos em seções pequenas e grandes para acomodar objetos pequenos e grandes. Os segmentos de cada heap são conectados juntos. Pelo menos um segmento para um objeto pequeno e um para um grande são reservados ao carregar o CLR.
Em cada pilha de objetos pequenos, há apenas um segmento efêmero, onde as gerações gen0 e gen1 estão localizadas. Esse segmento pode ou não conter objetos de geração gen2. Além dos segmentos efêmeros, pode existir um ou mais segmentos adicionais, que serão segmentos gen2, pois contêm objetos da geração 2.
Uma pilha de objetos grandes consiste em um ou mais segmentos.
O segmento de heap é preenchido de endereços inferiores para superiores. Isso significa que os objetos localizados nos endereços inferiores do segmento são mais antigos do que aqueles localizados nos idosos. Também há exceções descritas abaixo.
Os segmentos de heap são alocados conforme necessário. Se eles não contiverem objetos usados, os segmentos serão excluídos. No entanto, o segmento inicial na pilha sempre existe. Um segmento é alocado por vez para cada heap. No caso de objetos pequenos, isso acontece durante a coleta de lixo e, para objetos grandes, durante a alocação de memória para eles. Esse esquema aumenta a produtividade, pois objetos grandes são montados apenas na geração 2 (o que requer muitos recursos).
Os segmentos de heap são unidos em seleções. O último segmento da cadeia é sempre efêmero. Os segmentos em que todos os objetos são coletados podem ser reutilizados, por exemplo, como efêmeros. A reutilização de segmento se aplica apenas a montes de objetos pequenos. Para acomodar objetos grandes toda vez que todo o conjunto de objetos grandes é considerado. Objetos pequenos são colocados apenas em segmentos efêmeros.
Valor limite da memória alocada
Este é um conceito lógico relacionado ao tamanho de cada geração. Se for excedido, a geração inicia a coleta de lixo.
O valor limite para uma geração específica é definido dependendo do número de objetos sobreviventes nessa geração. Se esse valor for alto, o valor do limite se tornará mais alto. Espera-se que a proporção de objetos usados e não utilizados seja melhor durante a próxima geração de sessão de coleta de lixo.
Seleção de geração para coleta de lixo
Quando ativado, o coletor deve determinar em qual geração construir. Além do valor limite, outros fatores influenciam essa escolha:
- fragmentação de uma geração - se uma geração é altamente fragmentada, é provável que a coleta de lixo seja produtiva;
- se a memória da máquina estiver muito ocupada, o coletor poderá realizar uma limpeza mais profunda, se essa limpeza tiver mais chances de liberar espaço e evitar a troca desnecessária de páginas (memória em toda a máquina);
- se um segmento efêmero ficar sem espaço, o coletor poderá realizar uma limpeza mais profunda nesse segmento (coletar mais objetos da geração 1) para evitar a alocação de um novo segmento de heap.
Processo de coleta de lixo
Fase de marcação
Durante esta fase, o CLR deve encontrar todos os objetos vivos.
A vantagem de um colecionador com o apoio de gerações é sua capacidade de limpar o lixo apenas em parte da pilha, em vez de observar constantemente todos os objetos. Coletando lixo em gerações efêmeras, o coletor deve receber informações do ambiente de tempo de execução sobre quais objetos nessas gerações ainda são usados pelo programa. Além disso, objetos nas gerações mais antigas podem usar objetos nas gerações mais jovens, referindo-se a eles.
Para marcar objetos antigos que referenciam novos, o coletor de lixo usa bits especiais. Os bits são definidos pelo mecanismo do compilador JIT durante as operações de atribuição. Se o objeto pertencer à geração efêmera, o compilador JIT definirá o byte que contém o bit indicando a posição inicial. Coletando lixo em gerações efêmeras, o coletor pode usar esses bits para toda a pilha restante e exibir apenas os objetos aos quais esses bits correspondem.
Estágio de planejamento
Nesse ponto, a compactação é modelada para determinar sua eficácia. Se o resultado for produtivo, o coletor inicia a compactação real. Caso contrário, ele apenas faz a limpeza.
Estágio em movimento
Se o coletor executar compactação, isso fará com que os objetos se movam. Nesse caso, você deve atualizar os links para esses objetos. Durante a fase de movimentação, o coletor deve encontrar todos os links que apontam para objetos nas gerações em que a coleta de lixo ocorre. Por outro lado, durante o estágio de marcação, o coletor marca apenas objetos ativos, portanto, não é necessário considerar links fracos.
Estágio de compressão
Esse estágio é bastante simples, pois o coletor já determinou novos endereços para mover objetos durante o estágio de planejamento. Quando compactados, os objetos serão copiados para esses endereços.
Estágio de limpeza
Durante esta fase, o coletor procura espaço não utilizado entre objetos vivos. Em vez deste espaço, ele cria objetos livres. Objetos não utilizados nas proximidades se tornam um objeto livre. Todos os objetos livres são colocados na lista de objetos livres .
Fluxo de código
Termos:
- WKS GC: Coleta de Lixo no Modo Estação de Trabalho
- SVR GC: Coleta de Lixo no Modo Servidor
Comportamento funcional
GC WKS sem coleta de lixo paralela
- O encadeamento do usuário usou toda a memória alocada para ele e chama o coletor de lixo.
- O coletor chama o
SuspendEE
para suspender todos os encadeamentos gerenciados. - O coletor escolhe uma geração para limpeza.
- A marcação dos objetos começa.
- O coletor vai para a fase de planejamento e determina a necessidade de compactação.
- Se necessário, o coletor move objetos e realiza a compactação. Em outro caso, apenas faz a limpeza.
- O coletor chama
RestartEE
para reiniciar os encadeamentos gerenciados. - Os threads do usuário continuam funcionando.
GC WKS com coleta de lixo paralela
Esse algoritmo descreve a coleta de lixo em segundo plano.
- O encadeamento do usuário usou toda a memória alocada para ele e chama o coletor de lixo.
- O coletor chama o
SuspendEE
para suspender todos os encadeamentos gerenciados. - O coletor determina se deve executar a coleta de lixo em segundo plano.
- Nesse caso, o encadeamento de coleta de lixo em segundo plano é ativado. Esse encadeamento chama
RestartEE
para retomar os encadeamentos gerenciados. - A alocação de memória para processos gerenciados continua ao mesmo tempo que a coleta de lixo em segundo plano.
- Um encadeamento do usuário pode usar toda a memória alocada para ele e iniciar a coleta de lixo efêmera (também conhecida como coleta de lixo de alta prioridade). É executado da mesma maneira que no modo de estação de trabalho sem coleta de lixo paralela.
- O
SuspendEE
coleta de lixo em segundo plano chama o SuspendEE
novamente para concluir a marcação e, em seguida, chama RestartEE
para iniciar uma limpeza paralela com os threads do usuário em execução. - A coleta de lixo em segundo plano está concluída.
GC SVR sem coleta de lixo paralela
- O encadeamento do usuário usou toda a memória alocada para ele e chama o coletor de lixo.
- Os encadeamentos de coleta de lixo no modo de servidor são ativados e fazem com que o
SuspendEE
pause a execução dos encadeamentos gerenciados. - Os fluxos de coleta de lixo no modo servidor executam as mesmas operações que no modo estação de trabalho sem coleta de lixo paralela.
- Os threads de coleta de lixo no modo de servidor chamam
RestartEE
para iniciar os threads gerenciados. - Os threads do usuário continuam funcionando.
GC SVR com coleta de lixo paralela
O algoritmo é o mesmo que no caso da coleta de lixo paralela no modo de estação de trabalho, somente a coleta não-fônon é realizada nos encadeamentos do servidor.
Arquitetura física
Esta seção o ajudará a entender o fluxo de código.
Quando o encadeamento do usuário fica sem memória, ele pode obter espaço livre usando a função try_allocate_more_space
.
A função try_allocate_more_space
chama GarbageCollectGeneration
quando você precisa iniciar o coletor de lixo.
Se a coleta de lixo no modo de estação de trabalho não for paralela, o GarbageCollectGeneration
será executado no encadeamento do usuário que o coletor de lixo chamou. O fluxo de código é o seguinte:
GarbageCollectGeneration() { SuspendEE(); garbage_collect(); RestartEE(); } garbage_collect() { generation_to_condemn(); gc1(); } gc1() { mark_phase(); plan_phase(); } plan_phase() { // , // if (compact) { relocate_phase(); compact_phase(); } else make_free_lists(); }
Se a coleta de lixo paralela for realizada no modo de estação de trabalho (por padrão), o fluxo de código da coleta de lixo em segundo plano será semelhante a:
GarbageCollectGeneration() { SuspendEE(); garbage_collect(); RestartEE(); } garbage_collect() { generation_to_condemn(); // // do_background_gc(); } do_background_gc() { init_background_gc(); start_c_gc (); // . wait_to_proceed(); } bgc_thread_function() { while (1) { // // gc1(); } } gc1() { background_mark_phase(); background_sweep(); }
Links de Recursos