Por que a coleta de lixo se estende apenas à memória e não a outros tipos de recursos?


12

Parece que as pessoas se cansaram do gerenciamento manual de memória e inventaram a coleta de lixo, e a vida era razoavelmente boa. Mas e todos os outros tipos de recursos? Descritores de arquivos, soquetes ou mesmo dados criados pelo usuário, como conexões com o banco de dados?

Parece uma pergunta ingênua, mas não consigo encontrar nenhum lugar onde alguém tenha perguntado. Vamos considerar os descritores de arquivo. Digamos que um programa saiba que ele só terá 4000 fds disponíveis quando iniciar. Sempre que ele executa uma operação que abrirá um descritor de arquivo, o que aconteceria se

  1. Verifique se não está prestes a acabar.
  2. Se estiver, ative o coletor de lixo, que liberará um monte de memória.
  3. Se parte da memória liberada continha referências aos descritores de arquivos, feche-as imediatamente. Ele sabe que a memória pertencia a um recurso porque a memória vinculada a esse recurso foi registrada em um 'registro do descritor de arquivo', por falta de um termo melhor, quando foi aberto pela primeira vez.
  4. Abra um novo descritor de arquivo, copie-o para a nova memória, registre esse local da memória no 'registro do descritor de arquivo' e devolva-o ao usuário.

Portanto, o recurso não seria liberado imediatamente, mas seria liberado sempre que o gc fosse executado, o que inclui, no mínimo, pouco antes do esgotamento do recurso, supondo que ele não esteja sendo totalmente utilizado.

E parece que isso seria suficiente para muitos problemas de limpeza de recursos definidos pelo usuário. Eu consegui encontrar um único comentário aqui que faz referência à limpeza semelhante a esta no C ++ com um thread que contém uma referência a um recurso e o limpa quando apenas ele tem uma única referência restante (do thread de limpeza), mas eu posso ' não encontre nenhuma evidência de que isso seja uma biblioteca ou parte de qualquer idioma existente.

Respostas:


4

O GC lida com um recurso previsível e reservado . A VM tem controle total sobre ela e controle total sobre quais instâncias são criadas e quando. As palavras-chave aqui são "reservadas" e "controle total". Os identificadores são alocados pelo sistema operacional e os ponteiros são ... indicadores de recursos alocados fora do espaço gerenciado. Por esse motivo, identificadores e ponteiros não são restritos para serem usados ​​dentro do código gerenciado. Eles podem ser usados ​​- e geralmente são - por código gerenciado e não gerenciado em execução no mesmo processo.

Um "Coletor de Recursos" seria capaz de verificar se um identificador / ponteiro está sendo usado dentro de um espaço gerenciado ou não, mas, por definição, desconhece o que está acontecendo fora do espaço de memória (e, para piorar, alguns identificadores podem ser usados através dos limites do processo).

Um exemplo prático é o .NET CLR. Pode-se usar C ++ com sabor para escrever código que funciona com espaços de memória gerenciados e não gerenciados; identificadores, ponteiros e referências podem ser passados ​​entre código gerenciado e não gerenciado. O código não gerenciado deve usar construções / tipos especiais para permitir que o CLR mantenha o rastreamento das referências feitas aos seus recursos gerenciados. Mas é o melhor que pode fazer. Ele não pode fazer o mesmo com alças e ponteiros e, por isso, o Coletor de Recursos não saberia se está certo liberar uma alça ou ponteiro específico.

editar: Em relação ao .NET CLR, não tenho experiência no desenvolvimento de C ++ com a plataforma .NET. Talvez existam mecanismos especiais que permitam ao CLR acompanhar as referências a identificadores / ponteiros entre código gerenciado e não gerenciado. Se esse for o caso, o CLR poderá cuidar do tempo de vida desses recursos e liberá-los quando todas as referências a eles forem limpas (bem, pelo menos em alguns cenários). De qualquer maneira, as práticas recomendadas determinam que manipuladores (especialmente aqueles que apontam para arquivos) e ponteiros devem ser liberados assim que não forem necessários. Um coletor de recursos não estaria em conformidade com isso, esse é outro motivo para não ter um.

editar 2: É relativamente trivial no CLR / JVM / VMs-em-geral escrever algum código para liberar um identificador específico, se for usado apenas dentro do espaço gerenciado. No .NET seria algo como:

// This class offends many best practices, but it would do the job.
public class AutoReleaseFileHandle {
    // keeps track of how many instances of this class is in memory
    private static int _toBeReleased = 0;

    // the threshold when a garbage collection should be forced
    private const int MAX_FILES = 100;

    public AutoReleaseFileHandle(FileStream fileStream) {
       // Force garbage collection if max files are reached.
       if (_toBeReleased >= MAX_FILES) {
          GC.Collect();
       }
       // increment counter
       Interlocked.Increment(ref _toBeReleased);
       FileStream = fileStream;
    }

    public FileStream { get; private set; }

    private void ReleaseFileStream(FileStream fs) {
       // decrement counter
       Interlocked.Decrement(ref _toBeReleased);
       FileStream.Close();
       FileStream.Dispose();
       FileStream = null;
    }

    // Close and Dispose the Stream when this class is collected by the GC.
    ~AutoReleaseFileHandle() {
       ReleaseFileStream(FileStream);
    }

    // because it's .NET this class should also implement IDisposable
    // to allow the user to dispose the resources imperatively if s/he wants 
    // to.
    private bool _disposed = false;
    public void Dispose() {
      if (_disposed) {
        return;
      }
      _disposed = true;
      // tells GC to not call the finalizer for this instance.
      GC.SupressFinalizer(this);

      ReleaseFileStream(FileStream);
    }
}

// use it
// for it to work, fs.Dispose() should not be called directly,
var fs = File.Open("path/to/file"); 
var autoRelease = new AutoReleaseFileHandle(fs);

3

Esse parece ser um dos motivos pelos quais os idiomas com coletores de lixo implementam finalizadores. Os finalizadores destinam-se a permitir que um programador limpe os recursos de um objeto durante a coleta de lixo. O grande problema dos finalizadores é que eles não são garantidos.

Há uma boa descrição sobre o uso de finalizadores aqui:

Finalização e limpeza de objetos

De fato, ele usa especificamente o descritor de arquivo como exemplo. Você deve limpar esse recurso sozinho, mas existe um mecanismo que PODE restaurar recursos que não foram lançados corretamente.


Não tenho certeza se isso responde à minha pergunta. Está faltando a parte da minha proposta em que o sistema sabe que está prestes a ficar sem um recurso. A única maneira de abordar essa parte é garantir que você execute manualmente o gc antes de alocar novos descritores de arquivo, mas isso é extremamente ineficiente e não sei se você pode fazer com que o gc seja executado em java.
LeitorMental

OK, mas os descritores de arquivo geralmente representam um arquivo aberto no sistema operacional, o que implica (dependendo do sistema operacional) usar recursos no nível do sistema, como bloqueios, conjuntos de buffers, conjuntos de estruturas, etc. Francamente, não vejo o benefício de deixar essas estruturas abertas para uma coleta de lixo posterior e vejo muitos prejuízos em deixá-las alocadas por mais tempo do que o necessário. Os métodos Finalize () destinam-se a permitir uma última limpeza de vala, caso um programador ignore as chamadas para limpar recursos, mas não deve ser invocado.
9788 Brian Hibbert #

Meu entendimento é que a razão pela qual eles não devem ser invocados é que, se você alocar uma tonelada desses recursos, como se estivesse descendo uma hierarquia de arquivos abrindo cada arquivo, poderá abrir muitos arquivos antes que o gc aconteça correr, causando uma explosão. O mesmo aconteceria com a memória, exceto que o tempo de execução verifica para garantir que não ficará sem memória. Eu gostaria de saber por que um sistema não pode ser implementado para recuperar recursos arbitrários antes da explosão, quase da mesma maneira que a memória é feita.
LeitorMental

Um sistema PODE ser gravado em recursos do GC que não sejam memória, mas você teria que rastrear contagens de referência ou ter algum outro método para determinar quando um recurso não está mais em uso. Você NÃO deseja desalocar e realocar recursos que ainda estão em uso. Todo manor de confusão pode ocorrer se um thread tiver um arquivo aberto para gravação, o SO "recupera" o identificador de arquivo e outro thread abre um arquivo diferente para gravação usando o mesmo identificador. E eu ainda sugeriria que é um desperdício de recursos significativos deixá-los abertos até que um encadeamento semelhante ao GC chegue para liberá-los.
9788 Brian Hibbert #

3

Existem muitas técnicas de programação para ajudar a gerenciar esses tipos de recursos.

  • Os programadores de C ++ geralmente usam um padrão chamado Resource Acquisition is Initialization , ou RAII, para abreviar. Esse padrão garante que, quando um objeto que se apega aos recursos fica fora do escopo, ele fecha os recursos nos quais estava se apegando. Isso é útil quando a vida útil do objeto corresponde a um escopo específico no programa (por exemplo, quando corresponde ao horário em que um quadro de pilha específico está presente na pilha), portanto, é útil para objetos apontados por variáveis ​​locais (ponteiro variáveis ​​armazenadas na pilha), mas não é tão útil para objetos apontados por ponteiros armazenados na pilha.

  • Java, C # e muitas outras linguagens fornecem uma maneira de especificar um método que será chamado quando um objeto não estiver mais ativo e prestes a ser coletado pelo coletor de lixo. Veja, por exemplo, finalizadores dispose(), e outros. A idéia é que o programador possa implementar esse método para fechar explicitamente o recurso antes que o objeto seja liberado pelo coletor de lixo. No entanto, essas abordagens têm alguns problemas, sobre os quais você pode ler em outros lugares; por exemplo, o coletor de lixo pode não coletar o objeto até muito mais tarde do que você deseja.

  • C # e outros idiomas fornecem uma usingpalavra - chave que ajuda a garantir que os recursos sejam fechados depois que não forem mais necessários (para que você não esqueça de fechar o descritor de arquivo ou outro recurso). Isso geralmente é melhor do que confiar no coletor de lixo para descobrir que o objeto não está mais ativo. Consulte, por exemplo, /programming//q/75401/781723 . O termo geral aqui é um recurso gerenciado . Essa noção se baseia no RAII e nos finalizadores, melhorando-os de algumas maneiras.


Estou menos interessado na desalocação rápida de recursos e mais interessado na idéia de desalocação just in time. O RIAA é ótimo, mas não super aplicável a muitos idiomas de coleta de lixo. O Java está sem a capacidade de saber quando está prestes a ficar sem um determinado recurso. As operações de uso e tipo colchete são úteis e lidam com erros, mas não estou interessado nelas. Eu simplesmente quero alocar recursos e eles se limparão sempre que for conveniente ou necessário, e há poucas maneiras de estragar tudo. Eu acho que ninguém realmente olhou para isso.
LeitorMental

2

Toda a memória é igual, se eu pedir 1K, não me importo de onde no espaço de endereço o 1K vem.

Quando solicito um identificador de arquivo, quero um identificador para o arquivo que desejo abrir. Ter um identificador de arquivo aberto em um arquivo, geralmente bloqueia o acesso ao arquivo por outros processos ou máquina.

Portanto, os identificadores de arquivo devem ser fechados assim que não forem necessários, caso contrário, eles bloqueiam outros acessos ao arquivo, mas a memória só precisa ser recuperada quando você começar a ficar sem ele.

A execução de um passe de GC é dispendiosa e é feita apenas "quando necessário", não é possível prever quando outro processo precisará de um identificador de arquivo que seu processo talvez não esteja mais usando, mas ainda esteja aberto.


Sua resposta é a chave real: a memória é fungível e a maioria dos sistemas possui o suficiente para que não precise ser recuperada especialmente rapidamente. Por outro lado, se um programa adquirir acesso exclusivo a um arquivo, ele bloqueará outros programas em qualquer parte do universo que precisem usar esse arquivo, independentemente de quantos outros arquivos existam.
supercat

0

Eu acho que a razão pela qual isso não foi abordado muito para outros recursos é exatamente porque a maioria dos outros recursos prefere ser liberada o mais rápido possível para alguém reutilizar.

Observe, é claro, que seu exemplo pode ser fornecido agora usando descritores de arquivo "fracos" com técnicas existentes de GC.


0

Verificar se a memória não está mais acessível (e, portanto, garantimos que não será mais usada) é bastante fácil. A maioria dos outros tipos de recursos pode ser manipulada mais ou menos pelas mesmas técnicas (ou seja, a aquisição de recursos é a inicialização, RAII e sua contrapartida de liberação quando o usuário é destruído, o que o vincula à administração da memória). Em geral, é impossível realizar algum tipo de liberação "just in time" (verifique o problema de interrupção, você precisará descobrir que algum recurso foi usado pela última vez). Sim, às vezes isso pode ser feito automaticamente, mas é um caso muito mais confuso que a memória. Portanto, ele depende, em grande parte, da intervenção do usuário.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.