¿Por qué la recolección de basura se extiende solo a la memoria y no a otros tipos de recursos?

12

Parece que las personas se cansaron del manejo manual de la memoria, por lo que inventaron la recolección de basura y la vida fue razonablemente buena. Pero, ¿qué pasa con todos los otros tipos de recursos? ¿Descriptores de archivo, sockets o incluso datos creados por el usuario como conexiones de bases de datos?

Esto se siente como una pregunta ingenua, pero no puedo encontrar ningún lugar donde alguien lo haya hecho. Consideremos los descriptores de archivo. Digamos que un programa sabe que solo se le permitirá tener 4000 fds disponibles cuando se inicie. Cada vez que realiza una operación que abrirá un descriptor de archivo, ¿qué pasaría si lo hiciera?

  1. Verifique para asegurarse de que no esté a punto de agotarse.
  2. Si es así, active el recolector de basura, que liberará un montón de memoria.
  3. Si parte de la memoria liberada contiene referencias a descriptores de archivos, ciérrelos de inmediato. Sabe que la memoria pertenecía a un recurso porque la memoria vinculada a ese recurso se registró en un "registro de descriptor de archivo", a falta de un término mejor, cuando se abrió por primera vez.
  4. Abra un nuevo descriptor de archivo, cópielo en una nueva memoria, registre esa ubicación de memoria en el 'registro de descriptor de archivo' y devuélvala al usuario.

Por lo tanto, el recurso no se liberaría de inmediato, pero se liberaría cada vez que se ejecutara el gc, lo que incluye, como mínimo, justo antes de que el recurso estuviera a punto de agotarse, suponiendo que no se esté utilizando por completo.

Y parece que eso sería suficiente para muchos problemas de limpieza de recursos definidos por el usuario. Me las arreglé para encontrar un solo comentario aquí que hace referencias a hacer una limpieza similar a esto en C ++ con un hilo que contiene una referencia a un recurso y lo limpia cuando solo tiene una sola referencia restante (del hilo de limpieza), pero puedo ' No encuentre ninguna evidencia de que esto sea una biblioteca o parte de un idioma existente.

lector de mente
fuente

Respuestas:

4

GC trata con un recurso predecible y reservado . La VM tiene control total sobre ella y tiene control total sobre qué instancias se crean y cuándo. Las palabras clave aquí son "reservadas" y "control total". El sistema operativo asigna los identificadores, y los punteros son ... indicadores de recursos asignados fuera del espacio administrado. Debido a eso, los identificadores y punteros no están restringidos para usarse dentro del código administrado. Se pueden usar, y a menudo lo son, mediante código administrado y no administrado que se ejecuta en el mismo proceso.

Un "Recopilador de recursos" podría verificar si un controlador / puntero se está utilizando dentro de un espacio administrado o no, pero por definición no es consciente de lo que está sucediendo fuera de su espacio de memoria (y, para empeorar las cosas, se pueden utilizar algunos controladores a través de los límites del proceso).

Un ejemplo práctico es el .NET CLR. Se puede usar C ++ con sabor para escribir código que funciona con espacios de memoria administrados y no administrados; los identificadores, punteros y referencias se pueden pasar entre código administrado y no administrado. El código no administrado debe usar construcciones / tipos especiales para permitir que el CLR mantenga el seguimiento de las referencias que se hacen a sus recursos administrados. Pero eso es lo mejor que puede hacer. No puede hacer lo mismo con identificadores y punteros, y debido a eso, dicho Recopilador de recursos no sabría si está bien liberar un identificador o puntero en particular.

editar: En cuanto a .NET CLR, no tengo experiencia con el desarrollo de C ++ con la plataforma .NET. Tal vez existen mecanismos especiales que permiten que el CLR mantenga el seguimiento de las referencias a identificadores / punteros entre el código administrado y el no administrado. Si ese es el caso, el CLR podría encargarse de la vida útil de esos recursos y liberarlos cuando se borren todas las referencias a ellos (bueno, al menos en algunos escenarios podría). De cualquier manera, las mejores prácticas dictan que los identificadores (especialmente aquellos que apuntan a archivos) y los punteros deben liberarse tan pronto como no sean necesarios. Un Recopilador de recursos no estaría cumpliendo con eso, esa es otra razón para no tener uno.

edición 2: es relativamente trivial en CLR / JVM / VM-en-general escribir algún código para liberar un identificador particular si se usa solo dentro del espacio administrado. En .NET sería algo así 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);
Marcelo De Zen
fuente
3

Esta parece ser una de las razones por las que los lenguajes con recolectores de basura implementan finalizadores. Los finalizadores están destinados a permitir que un programador limpie los recursos de un objeto durante la recolección de basura. El gran problema con los finalizadores es que no se garantiza que se ejecuten.

Aquí hay una buena reseña sobre el uso de finalizadores:

Finalización y limpieza de objetos.

De hecho, utiliza específicamente el descriptor de archivo como ejemplo. Debe asegurarse de limpiar ese recurso usted mismo, pero existe un mecanismo que PUEDE restaurar los recursos que no se liberaron correctamente.

Brian Hibbert
fuente
No estoy seguro si esto responde a mi pregunta. Falta la parte de mi propuesta en la que el sistema sabe que está a punto de quedarse sin un recurso. La única forma de forzar esa parte es asegurarse de ejecutar manualmente el gc antes de asignar nuevos descriptores de archivo, pero eso es extremadamente ineficiente, y no sé si puede hacer que el gc se ejecute en java.
lector de mente
Está bien, pero los descriptores de archivos generalmente representan un archivo abierto en el sistema operativo que implica (dependiendo del sistema operativo) utilizar recursos a nivel del sistema como bloqueos, agrupaciones de almacenamiento intermedio, agrupaciones de estructura, etc. Francamente, no veo el beneficio de dejar estas estructuras abiertas para una recolección de basura posterior y veo muchos perjuicios para dejarlas asignadas más tiempo del necesario. Los métodos Finalize () están destinados a permitir una última limpieza de zanjas en caso de que un programador pase por alto las llamadas para limpiar recursos, pero no se debe confiar en ellos.
Brian Hibbert
Tengo entendido que la razón por la que no se debe confiar es que si asignara una tonelada de estos recursos, como si estuviera descendiendo por una jerarquía de archivos que abre cada archivo, puede abrir demasiados archivos antes de que ocurra el gc correr, causando una explosión. Lo mismo sucedería con la memoria, excepto que el tiempo de ejecución verifica para asegurarse de que no se quede sin memoria. Me gustaría saber por qué no se puede implementar un sistema para reclamar recursos arbitrarios antes de la explosión, casi de la misma manera que se hace la memoria.
lector de ideas
Un sistema PODRÍA escribirse en recursos de GC distintos de la memoria, pero tendría que realizar un seguimiento de los recuentos de referencia o tener algún otro método para determinar cuándo un recurso ya no está en uso. NO desea desasignar y reasignar recursos que todavía están en uso. Toda mansión de caos puede sobrevenir si un hilo tiene un archivo abierto para escritura, el sistema operativo "reclama" el identificador de archivo y otro hilo abre un archivo diferente para escribir usando el mismo identificador. Y también sugeriría que es un desperdicio de recursos significativos dejarlos abiertos hasta que un hilo similar a GC se libere para liberarlos.
Brian Hibbert
3

Existen muchas técnicas de programación para ayudar a administrar este tipo de recursos.

  • Los programadores de C ++ a menudo usan un patrón llamado Adquisición de recursos es Inicialización , o RAII para abreviar. Este patrón asegura que cuando un objeto que retiene recursos queda fuera de alcance, cerrará los recursos a los que se aferraba. Esto es útil cuando la vida útil del objeto corresponde a un alcance particular en el programa (por ejemplo, cuando coincide con el momento en que un marco de pila particular está presente en la pila), por lo que es útil para los objetos que apuntan las variables locales (puntero variables almacenadas en la pila), pero no tan útiles para los objetos que apuntan los punteros almacenados en el montón.

  • Java, C # y muchos otros lenguajes proporcionan una forma de especificar un método que se invocará cuando un objeto ya no esté vivo y esté a punto de ser recogido por el recolector de basura. Ver, por ejemplo, finalizadores dispose(), y otros. La idea es que el programador pueda implementar dicho método para cerrar explícitamente el recurso antes de que el objeto sea liberado por el recolector de basura. Sin embargo, estos enfoques tienen algunos problemas que puede leer en otro lugar; por ejemplo, el recolector de basura podría no recolectar el objeto hasta mucho más tarde de lo que desea.

  • C # y otros lenguajes proporcionan una usingpalabra clave que ayuda a garantizar que los recursos se cierren después de que ya no sean necesarios (para que no se olvide de cerrar el descriptor de archivo u otro recurso). Esto suele ser mejor que confiar en el recolector de basura para descubrir que el objeto ya no está vivo. Ver, por ejemplo, /programming//q/75401/781723 . El término general aquí es un recurso gestionado . Esta noción se basa en RAII y finalizadores, y los mejora de alguna manera.

DW
fuente
Estoy menos interesado en la asignación inmediata de recursos, y más interesado en la idea de la asignación justo a tiempo. RIAA es excelente, pero no es súper aplicable a muchos idiomas de recolección de basura. A Java le falta la capacidad de saber cuándo está a punto de quedarse sin un determinado recurso. El uso y las operaciones de tipo corchete son útiles y tratan errores, pero no estoy interesado en ellos. Simplemente quiero asignar recursos y luego se limpiarán ellos mismos cuando sea conveniente o necesario, y hay pocas formas de arruinarlo. Supongo que nadie realmente ha investigado esto.
lector de mente
2

Toda la memoria es igual, si pido 1K, no me importa de dónde viene el 1K en el espacio de direcciones.

Cuando solicito un identificador de archivo, quiero un identificador para el archivo que deseo abrir. Tener un identificador de archivo abierto en un archivo, a menudo bloquea el acceso al archivo por otros procesos o máquinas.

Por lo tanto, los identificadores de archivos deben cerrarse tan pronto como no sean necesarios, de lo contrario, bloquean otros accesos al archivo, pero la memoria solo necesita recuperarse cuando comienza a quedarse sin él.

Ejecutar un pase de GC es costoso y solo se hace "cuando sea necesario", no es posible predecir cuándo otro proceso necesitará un identificador de archivo que su proceso ya no esté utilizando, pero aún está abierto.

Ian Ringrose
fuente
Su respuesta llega a la clave real: la memoria es fungible, y la mayoría de los sistemas tienen suficiente para que no sea necesario recuperarla especialmente rápido. Por el contrario, si un programa adquiere acceso exclusivo a un archivo, eso bloqueará cualquier otro programa en cualquier parte del universo que pueda necesitar usar ese archivo, sin importar cuántos otros archivos puedan existir.
supercat
0

Supongo que la razón por la cual no se ha abordado mucho esto para otros recursos es exactamente porque se prefiere que la mayoría de los otros recursos se publiquen lo antes posible para que cualquiera pueda reutilizarlos.

Tenga en cuenta, por supuesto, que su ejemplo podría proporcionarse ahora utilizando descriptores de archivo "débiles" con las técnicas de GC existentes.

Mark Hurd
fuente
0

Verificar si la memoria ya no es accesible (y, por lo tanto, se garantiza que ya no se usará) es bastante fácil. La mayoría de los otros tipos de recursos pueden manejarse con más o menos las mismas técnicas (es decir, la adquisición de recursos es inicialización, RAII y su contrapartida de liberación cuando el usuario es destruido, lo que lo vincula con la administración de memoria). En general, es imposible hacer algún tipo de liberación "justo a tiempo" (verifique el problema de detención, tendría que descubrir que se utilizó algún recurso por última vez). Sí, a veces se puede hacer automáticamente, pero es un caso mucho más desordenado como la memoria. Por lo tanto, se basa en la intervención del usuario en su mayor parte.

vonbrand
fuente