Gestão Automática 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 para 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 tempo de execução 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 onde o próximo objeto no heap será alocado. Inicialmente, esse ponteiro é definido como o endereço base 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á a alocar espaço para novos objetos dessa maneira.
A alocação de memória do heap gerenciado é mais rápida do que a alocação de memória não gerenciada. Como o tempo de execução aloca memória para um objeto adicionando um valor a um ponteiro, é quase tão rápido quanto alocar memória da 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 realizar 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 aplicação tem um conjunto de raízes. Cada raiz refere-se a um objeto no heap gerenciado ou é definida como null. As raízes de um aplicativo incluem campos estáticos, variáveis locais e parâmetros na pilha de um thread e registradores de CPU. O coletor de lixo tem acesso à lista de raízes ativas que o compilador just-in-time (JIT) e o tempo de execução mantêm. Usando essa lista, ele examina as raízes de um aplicativo e, no processo, cria um gráfico que contém todos os objetos que podem ser acessados a partir das raízes.
Os objetos que não estão no gráfico são inacessíveis a partir das raízes do aplicativo. O coletor de lixo considera objetos inacessíveis lixo e liberará a memória alocada para eles. Durante uma coleta, o coletor de lixo examina a pilha gerenciada, 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 alcançáveis na memória, liberando os blocos de espaços de endereço alocados para objetos inacessíveis. Uma vez que a memória para os objetos acessíveis tenha sido compactada, o coletor de lixo faz as correções de ponteiro necessárias para 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 alcançável. Observe que a memória é compactada somente se uma coleção descobrir um número significativo de objetos inacessíveis. Se todos os objetos no heap gerenciado sobreviverem a uma coleção, não haverá necessidade de compactação de memória.
Para melhorar o desempenho, o tempo de execução 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, a pilha gerenciada é dividida em três gerações: 0, 1 e 2. O algoritmo de coleta de lixo do tempo de execução é baseado em várias generalizações que a indústria de software de computador descobriu ser verdadeira ao experimentar esquemas de coleta de lixo. Primeiro, é mais rápido compactar a memória para uma parte da pilha gerenciada do que para toda a pilha gerenciada. Em segundo lugar, os objetos mais novos terão vidas mais curtas e os objetos mais antigos terão vidas mais longas. Por fim, objetos mais novos tendem a ser relacionados entre si e acessados pelo aplicativo ao mesmo tempo.
O coletor de lixo do tempo de execução armazena novos objetos na geração 0. Os objetos criados no início da vida útil do aplicativo que sobrevivem às coleções são promovidos e armazenados nas gerações 1 e 2. O processo de promoção de objetos é descrito mais adiante neste tópico. Como é mais rápido compactar uma parte da pilha gerenciada do que a pilha inteira, esse esquema permite que o coletor de lixo libere a memória em uma geração específica, em vez de liberar a memória para toda a pilha gerenciada cada vez que executa uma coleta.
Na realidade, o coletor de lixo realiza 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 na tentativa de liberar espaço de endereçamento na geração 0 para o objeto. O coletor de lixo começa examinando os objetos na geração 0 em vez de todos os objetos na pilha gerenciada. Essa é a abordagem mais eficiente, porque novos objetos tendem a ter vidas curtas, 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 coleta de geração 0, ele compacta a memória para os objetos alcançáveis, conforme explicado em Liberando memória anteriormente neste tópico. Em seguida, o coletor de lixo promove esses objetos e considera essa parte da geração 1 da pilha gerenciada. Como os objetos que sobrevivem às coleções tendem a ter uma vida útil mais longa, faz sentido promovê-los para 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 realiza uma coleta da geração 0.
Depois que o coletor de lixo realiza sua primeira coleta da geração 0 e promove os objetos alcançáveis para a geração 1, ele considera o restante da geração 0 da pilha gerenciada. Ele continua a alocar memória para novos objetos na geração 0 até que a geração 0 esteja cheia e seja necessário realizar outra coleção. Neste ponto, o mecanismo de otimização do coletor de lixo determina se é necessário examinar os objetos nas gerações mais velhas. Por exemplo, se uma coleção de geração 0 não recuperar memória suficiente para que o aplicativo conclua com êxito sua tentativa de criar um novo objeto, o coletor de lixo poderá executar uma coleta de geração 1 e, em seguida, geração 2. Se isso não recuperar memória suficiente, o coletor de lixo pode realizar uma coleta de gerações 2, 1 e 0. Após cada coleta, o coletor de lixo compacta os objetos alcançáveis na geração 0 e os promove para a geração 1. Os objetos da geração 1 que sobrevivem às coleções são promovidos para a geração 2. Como o coletor de lixo suporta apenas três gerações, os objetos da geração 2 que sobrevivem a uma coleta permanecem na geração 2 até que sejam determinados como inalcançáveis em uma coleta futura.
Liberando memória para recursos não gerenciados
Para a maioria dos objetos que seu aplicativo cria, você pode confiar no 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 controlar 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 fornecer o código necessário para limpar o recurso não gerenciado em um método Dispose público. Ao fornecer um método Dispose , você permite que os usuários do seu objeto liberem explicitamente sua memória quando terminarem de usá-lo. Ao usar um objeto que encapsula um recurso não gerenciado, você deve estar ciente de Eliminar 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 Dispose, consulte Coleta de lixo.