En una palabra
La finalización no es un asunto simple que deben manejar los recolectores de basura. Es fácil de usar con GC de conteo de referencias, pero esta familia de GC a menudo es incompleta, lo que requiere que las fugas de memoria sean compensadas por un disparo explícito de destrucción y finalización de algunos objetos y estructuras. Los recolectores de basura de rastreo son mucho más efectivos, pero hacen que sea mucho más difícil identificar el objeto que se finalizará y destruirá, en lugar de solo identificar la memoria no utilizada, lo que requiere una administración más compleja, con un costo en tiempo y espacio, y en complejidad de la implementación.
Introducción
Supongo que lo que está preguntando es por qué los idiomas recolectados de basura no manejan automáticamente la destrucción / finalización dentro del proceso de recolección de basura, como lo indica el comentario:
Me parece extremadamente escaso que estos lenguajes consideren la memoria como el único recurso que vale la pena administrar. ¿Qué pasa con los sockets, identificadores de archivos, estados de aplicación?
No estoy de acuerdo con la respuesta aceptada dada por kdbanman . Si bien los hechos indicados son en su mayoría correctos, aunque están fuertemente sesgados hacia el conteo de referencias, no creo que expliquen adecuadamente la situación de la que se queja en la pregunta.
No creo que la terminología desarrollada en esa respuesta sea un gran problema, y es más probable que confunda las cosas. De hecho, como se presenta, la terminología está determinada principalmente por la forma en que se activan los procedimientos y no por lo que hacen. El punto es que, en todos los casos, existe la necesidad de finalizar un objeto que ya no se necesita con algún proceso de limpieza y liberar cualquier recurso que haya estado utilizando, la memoria es solo uno de ellos. Idealmente, todo debe hacerse automáticamente cuando el objeto ya no se use, por medio de un recolector de basura. En la práctica, GC puede estar ausente o tener deficiencias, y esto se compensa con la activación explícita del programa de finalización y recuperación.
El trigerring explícito por parte del programa es un problema, ya que puede permitir errores de programación difíciles de analizar, cuando un objeto que todavía está en uso se termina explícitamente.
Por lo tanto, es mucho mejor confiar en la recolección automática de basura para recuperar recursos. Pero hay dos problemas:
alguna técnica de recolección de basura permitirá pérdidas de memoria que evitarán la recuperación total de los recursos. Esto es bien conocido para el conteo de referencias GC, pero puede aparecer para otras técnicas de GC cuando se usan algunas organizaciones de datos sin cuidado (punto no discutido aquí).
Si bien la técnica GC puede ser buena para identificar los recursos de memoria que ya no se utilizan, finalizar los objetos contenidos en él puede no ser simple, y eso complica el problema de recuperar otros recursos utilizados por estos objetos, que a menudo es el propósito de la finalización.
Finalmente, un punto importante que a menudo se olvida es que los ciclos de GC pueden ser activados por cualquier cosa, no solo por la escasez de memoria, si se proporcionan los ganchos adecuados y si se considera que el costo de un ciclo de GC vale la pena. Por lo tanto, está perfectamente bien iniciar un GC cuando falta algún tipo de recurso, con la esperanza de liberar algunos.
Colectores de basura de conteo de referencia
El conteo de referencias es una técnica de recolección de basura débil , que no manejará los ciclos correctamente. De hecho, sería débil al destruir estructuras obsoletas y reclamar otros recursos simplemente porque es débil al reclamar memoria. Pero los finalizadores se pueden usar más fácilmente con un recolector de basura de conteo de referencia (GC), ya que un GC de recuento de ref reclama una estructura cuando su recuento de ref se reduce a 0, momento en el que su dirección se conoce junto con su tipo, ya sea estáticamente o dinámicamente Por lo tanto, es posible recuperar la memoria con precisión después de aplicar el finalizador adecuado y llamar de forma recursiva al proceso en todos los objetos puntiagudos (posiblemente a través del procedimiento de finalización).
En pocas palabras, la finalización es fácil de implementar con Ref Counting GC, pero sufre de la "incompletitud" de ese GC, de hecho debido a estructuras circulares, precisamente en la misma medida que sufre la recuperación de memoria. En otras palabras, con el recuento de referencias, la memoria está precisamente tan mal gestionada como otros recursos como sockets, identificadores de archivos, etc.
De hecho, la incapacidad de Ref Count GC para recuperar estructuras en bucle (en general) puede verse como una pérdida de memoria . No puede esperar que todos los GC eviten pérdidas de memoria. Depende del algoritmo de GC y de la información de estructura de tipo disponible dinámicamente (por ejemplo, en
GC conservador ).
Rastreando recolectores de basura
La familia más poderosa de GC, sin tales fugas, es la familia de rastreo que explora las partes vivas de la memoria, comenzando por punteros de raíz bien identificados. Todas las partes de la memoria que no se visitan en este proceso de rastreo (que en realidad se pueden descomponer de varias maneras, pero tengo que simplificar) son partes no utilizadas de la memoria que se pueden recuperar 1 . Estos recolectores reclamarán todas las partes de la memoria a las que el programa ya no puede acceder, sin importar lo que haga. Reclama estructuras circulares, y los GC más avanzados se basan en alguna variación de este paradigma, a veces altamente sofisticado. Se puede combinar con el recuento de referencias en algunos casos y compensar sus debilidades.
Un problema es que su declaración (al final de la pregunta):
Los idiomas que ofrecen recolección de basura automática parecen ser los principales candidatos para apoyar la destrucción / finalización de objetos, ya que saben con 100% de certeza cuando un objeto ya no está en uso.
es técnicamente incorrecto para rastrear colectores.
Lo que se sabe con 100% de certeza es qué partes de la memoria ya no están en uso . (Más precisamente, debe decirse que ya no son accesibles , porque algunas partes, que ya no se pueden usar de acuerdo con la lógica del programa, todavía se consideran en uso si todavía hay un puntero inútil en el programa datos.) Pero se necesitan más procesamiento y estructuras apropiadas para saber qué objetos no utilizados pueden haberse almacenado en estas partes de la memoria ahora no utilizadas . Esto no se puede determinar a partir de lo que se conoce del programa, ya que el programa ya no está conectado a estas partes de la memoria.
Por lo tanto, después de un paso de recolección de basura, le quedan fragmentos de memoria que contienen objetos que ya no están en uso, pero a priori no hay forma de saber cuáles son estos objetos para aplicar la finalización correcta. Además, si el colector de rastreo es del tipo de marcado y barrido, puede ser que algunos de los fragmentos puedan contener objetos que ya se hayan finalizado en un pase de GC anterior, pero que no se utilizaron desde entonces por razones de fragmentación. Sin embargo, esto puede tratarse con una escritura explícita extendida.
Si bien un simple recopilador solo reclamaría estos fragmentos de memoria, sin más preámbulos, la finalización requiere un pase específico para explorar esa memoria no utilizada, identificar los objetos allí contenidos y aplicar los procedimientos de finalización. Pero tal exploración requiere la determinación del tipo de objetos que se almacenaron allí, y la determinación del tipo también es necesaria para aplicar la finalización adecuada, si corresponde.
Eso implica costos adicionales en el tiempo de GC (el pase adicional) y posiblemente costos adicionales de memoria para hacer que la información de tipo adecuada esté disponible durante ese pase mediante diversas técnicas. Estos costos pueden ser significativos, ya que a menudo uno solo querrá finalizar unos pocos objetos, mientras que el tiempo y el espacio de arriba podrían afectar a todos los objetos.
Otro punto es que la sobrecarga de tiempo y espacio puede referirse a la ejecución del código del programa, y no solo a la ejecución del GC.
No puedo dar una respuesta más precisa, señalando problemas específicos, porque no conozco los detalles de muchos de los idiomas que enumera. En el caso de C, escribir es un tema muy difícil que conduce al desarrollo de coleccionistas conservadores. Supongo que esto también afecta a C ++, pero no soy un experto en C ++. Esto parece ser confirmado por Hans Boehm, quien realizó gran parte de la investigación sobre GC conservador. El GC conservador no puede reclamar sistemáticamente toda la memoria no utilizada precisamente porque puede carecer de información de tipo precisa sobre los datos. Por la misma razón, no podría aplicar sistemáticamente los procedimientos de finalización.
Por lo tanto, es posible hacer lo que está pidiendo, como sabe de algunos idiomas. Pero no viene gratis. Dependiendo del idioma y su implementación, puede implicar un costo incluso si no utiliza la función. Se pueden considerar varias técnicas y compensaciones para abordar estos problemas, pero eso está más allá del alcance de una respuesta de tamaño razonable.
1: esta es una presentación abstracta de la colección de rastreo (que abarca tanto GC como copiar y marcar y barrer), las cosas varían según el tipo de recopilador de rastreo, y explorar la parte no utilizada de la memoria es diferente, dependiendo de si copiar o marcar y Se utiliza el barrido.
finalize
/destroy
es una mentira? No hay garantía de que alguna vez se ejecute. E, incluso si, no sabe cuándo (dada la recolección automática de basura), y si es necesario, el contexto todavía está allí (puede que ya se haya recolectado). Por lo tanto, es más seguro garantizar un estado coherente de otras maneras, y uno podría obligar al programador a hacerlo.