Compartilhar via


Gerenciamento automático de memória

O gerenciamento automático de memória é um dos serviços que o Common Language Runtime fornece durante a Execução Gerenciada. O coletor de lixo do Common Language Runtime gerencia a alocação e a liberação de memória de um aplicativo. Para desenvolvedores, isso significa que você não precisa escrever código para executar tarefas de gerenciamento de memória ao desenvolver aplicativos gerenciados. O gerenciamento automático de memória pode eliminar problemas comuns, como esquecer de liberar um objeto e causar um vazamento de memória ou tentar acessar a memória de um objeto que já foi liberado. Esta seção descreve como o coletor de lixo aloca e libera memória.

Alocando memória

Quando você inicializa um novo processo, o runtime reserva uma região contígua de espaço de endereço para o processo. Esse espaço de endereço reservado é chamado de heap gerenciado. O heap gerenciado mantém um ponteiro para o endereço no qual o próximo objeto do heap será alocado. Inicialmente, esse ponteiro é definido como o endereço básico do heap gerenciado. Todos os tipos de referência são alocados no heap gerenciado. Quando um aplicativo cria o primeiro tipo de referência, a memória é alocada para o tipo no endereço base do heap gerenciado. Quando o aplicativo cria o próximo objeto, o coletor de lixo aloca memória para ele no espaço de endereço imediatamente após o primeiro objeto. Enquanto o espaço de endereço estiver disponível, o coletor de lixo continuará alocando espaço para novos objetos dessa maneira.

A alocação memória com base no heap gerenciado é mais rápida do que a alocação de memória não gerenciada. Como o runtime aloca memória para um objeto adicionando um valor a um ponteiro, ele é quase tão rápido quanto a alocação de memória com base na pilha. Além disso, como novos objetos alocados consecutivamente são armazenados contíguamente no heap gerenciado, um aplicativo pode acessar os objetos muito rapidamente.

Liberando memória

O mecanismo de otimização do coletor de lixo determina o melhor momento para executar uma coleta com base nas alocações que estão sendo feitas. Quando o coletor de lixo executa uma coleta, ele libera a memória para objetos que não estão mais sendo usados pelo aplicativo. Ele determina quais objetos não estão mais sendo usados examinando as raízes do aplicativo. Cada aplicativo tem um conjunto de raízes. Cada raiz refere-se a um objeto no heap gerenciado ou é definida como nula. As raízes de um aplicativo incluem campos estáticos, variáveis locais e parâmetros na pilha de um thread, além de registros de CPU. O coletor de lixo tem acesso à lista de raízes ativas mantidas pelo runtime e pelo compilador JIT (Just-In-Time). Usando essa lista, ela examina as raízes de um aplicativo e, no processo, cria um grafo que contém todos os objetos que podem ser acessados a partir das raízes.

Objetos que não estão no grafo são inacessíveis das raízes do aplicativo. O coletor de lixo considera objetos inacessíveis como lixo e liberará a memória alocada para eles. Durante uma coleta, o coletor de lixo examina o heap gerenciado, procurando os blocos de espaço de endereço ocupados por objetos inacessíveis. À medida que descobre cada objeto inacessível, ele usa uma função de cópia de memória para compactar os objetos acessíveis na memória, liberando os blocos de espaços de endereço alocados para objetos inacessíveis. Uma vez que a memória dos objetos acessíveis tenha sido compactada, o coletor de lixo faz as correções necessárias no ponteiro de forma que as raízes do aplicativo apontem para os objetos em seus novos locais. Ele também posiciona o ponteiro do heap gerenciado após o último objeto acessível. Observe que a memória só será compactada se uma coleção descobrir um número significativo de objetos inacessíveis. Se todos os objetos no heap gerenciado sobrevivem a uma coleta, não há necessidade de compactação de memória.

Para melhorar o desempenho, o runtime aloca memória para objetos grandes em um heap separado. O coletor de lixo libera automaticamente a memória para objetos grandes. No entanto, para evitar mover objetos grandes na memória, essa memória não é compactada.

Gerações e desempenho

Para otimizar o desempenho do coletor de lixo, o heap gerenciado é dividido em três gerações: 0, 1 e 2. O algoritmo de coleta de lixo do runtime baseia-se em várias generalizações que o setor de software de computador descobriu serem verdadeiras ao experimentar esquemas de coleta de lixo. Primeiro, é mais rápido compactar a memória para uma parte do heap gerenciado do que para o heap gerenciado inteiro. Em segundo lugar, os objetos mais recentes terão tempos de vida mais curtos e os objetos mais antigos terão tempos de vida mais longos. Por fim, os objetos mais recentes tendem a estar relacionados uns aos outros e acessados pelo aplicativo ao mesmo tempo.

O coletor de lixo do runtime armazena novos objetos na geração 0. Objetos criados no início do tempo de vida do aplicativo que sobrevivem às coleções são promovidos e armazenados nas gerações 1 e 2. O processo de promoção de objeto é descrito posteriormente neste tópico. Como é mais rápido compactar uma parte do heap gerenciado do que o heap inteiro, esse esquema permite que o coletor de lixo libere a memória em uma geração específica em vez liberar a memória para toda a memória gerenciada a cada vez que ele executa uma coleta.

Na realidade, o coletor de lixo executa uma coleta quando a geração 0 está cheia. Se um aplicativo tentar criar um novo objeto quando a geração 0 estiver cheia, o coletor de lixo descobrirá que não há espaço de endereço restante na geração 0 para alocar para o objeto. O coletor de lixo executa uma coleta em uma tentativa de liberar espaço de endereço na geração 0 para o objeto. O coletor de lixo inicia examinando os objetos na geração 0 em vez de todos os objetos no heap gerenciado. Essa é a abordagem mais eficiente, pois novos objetos tendem a ter tempos de vida curtos e espera-se que muitos dos objetos na geração 0 não estejam mais em uso pelo aplicativo quando uma coleção for executada. Além disso, uma coleção de geração 0 sozinha geralmente recupera memória suficiente para permitir que o aplicativo continue criando novos objetos.

Depois que o coletor de lixo executa uma coleção de geração 0, ele compacta a memória para os objetos acessíveis, conforme explicado em Liberar Memória anteriormente neste tópico. Assim, o coletor de lixo promove esses objetos e considera isso parte da geração 1 do heap gerenciado. Como os objetos que sobrevivem às coleções tendem a ter tempos de vida mais longos, faz sentido promovê-los a uma geração mais alta. Como resultado, o coletor de lixo não precisa reexaminar os objetos nas gerações 1 e 2 cada vez que executa uma coleção de geração 0.

Depois que o coletor de lixo executa sua primeira coleta de geração 0 e promove os objetos atingíveis para a geração 1, ele considera o restante do heap gerenciado a geração 0. Ele continua alocando memória para novos objetos na geração 0 até que a geração 0 esteja cheia e é necessário executar outra coleção. Neste ponto, o mecanismo de otimização do coletor de lixo determina se é necessário examinar os objetos em gerações mais antigas. Por exemplo, se uma coleção de geração 0 não recuperar memória suficiente para o aplicativo concluir com êxito sua tentativa de criar um novo objeto, o coletor de lixo poderá executar uma coleção de geração 1 e, em seguida, a geração 2. Se isso não recuperar memória suficiente, o coletor de lixo poderá executar uma coleção de gerações 2, 1 e 0. Após cada coleta, o coletor de lixo compacta os objetos acessíveis na geração 0 e os promove para a geração 1. Objetos na geração 1 que sobrevivem a coleções são promovidos à geração 2. Como o coletor de lixo dá suporte a apenas três gerações, os objetos na geração 2 que sobrevivem a uma coleção permanecem na geração 2 até que sejam determinados como inacessíveis em uma coleção futura.

Liberando memória para recursos não gerenciados

Para a maioria dos objetos que seu aplicativo cria, você pode contar com o coletor de lixo para executar automaticamente as tarefas de gerenciamento de memória necessárias. No entanto, recursos não gerenciados exigem limpeza explícita. O tipo mais comum de recurso não gerenciado é um objeto que encapsula um recurso do sistema operacional, como um identificador de arquivo, identificador de janela ou conexão de rede. Embora o coletor de lixo seja capaz de acompanhar o tempo de vida de um objeto gerenciado que encapsula um recurso não gerenciado, ele não tem conhecimento específico sobre como limpar o recurso. Quando você cria um objeto que encapsula um recurso não gerenciado, é recomendável que você forneça o código necessário para limpar o recurso não gerenciado em um método de Descarte público. Ao fornecer um método Dispose , você permite que os usuários do objeto liberem explicitamente sua memória quando terminarem de usar o objeto. Ao usar um objeto que encapsula um recurso não gerenciado, você deve estar ciente de Descartar e chamá-lo conforme necessário. Para obter mais informações sobre como limpar recursos não gerenciados e um exemplo de um padrão de design para implementar o Descarte, consulte Coleta de Lixo.

Consulte também