Recolección de basura

La recolección de basura [ 1] en la programación es una  forma de gestión automática de la memoria . Un proceso especial , llamado recolector de elementos no utilizados , libera memoria periódicamente eliminando objetos que se han vuelto innecesarios . 

La recolección de basura automática mejora la seguridad del acceso a la memoria .

Historia

La recolección de basura fue aplicada por primera vez por John McCarthy en 1959 en un entorno de programación en el lenguaje de programación funcional que desarrolló, Lisp . Posteriormente, fue utilizado en otros sistemas y lenguajes de programación, principalmente en los funcionales y lógicos . La necesidad de recolección de basura en este tipo de lenguajes se debe al hecho de que la estructura de dichos lenguajes hace que sea extremadamente inconveniente realizar un seguimiento de la vida útil de los objetos en la memoria y administrarlos manualmente. Las listas ampliamente utilizadas en estos lenguajes y las estructuras de datos complejas basadas en ellas se crean, agregan, amplían y copian constantemente durante la operación de los programas, y es difícil determinar correctamente el momento de la eliminación de un objeto.

Los lenguajes de procedimientos y objetos industriales no utilizaron la recolección de basura durante mucho tiempo. Se dio preferencia a la gestión manual de la memoria, por ser más eficiente y predecible. Pero desde la segunda mitad de la década de 1980, la tecnología de recolección de basura se ha utilizado tanto en lenguajes de programación de directivas ( imperativo ) como de objetos, y desde la segunda mitad de la década de 1990, un número creciente de lenguajes creados y entornos enfocados en la programación de aplicaciones incluyen un mecanismo de recolección de basura ya sea como el único o como uno de los mecanismos de administración de memoria dinámica disponibles. Actualmente se utiliza en Oberon , Java , Python , Ruby , C# , D , F# , Go y otros lenguajes.

Manejo manual de memoria

La forma tradicional en que los lenguajes directivos gestionan la memoria es manual. Su esencia es la siguiente:

En cualquier lenguaje que permita la creación de objetos en memoria dinámica, existen dos problemas potenciales: referencias colgantes y pérdidas de memoria .

Enlaces colgantes

Un  puntero colgante es una referencia a un objeto que ya se ha eliminado de la memoria. Después de eliminar un objeto, todas las referencias guardadas en el programa se vuelven "colgantes". La memoria previamente ocupada por un objeto puede pasar al sistema operativo y volverse inaccesible, o usarse para asignar un nuevo objeto en el mismo programa. En el primer caso, un intento de acceder a un enlace "colgante" activará el mecanismo de protección de la memoria y bloqueará el programa, y ​​en el segundo caso, tendrá consecuencias impredecibles.

La aparición de referencias colgantes suele ser el resultado de una estimación incorrecta de la vida útil de un objeto: el programador llama al comando para eliminar el objeto antes de que cese su uso.

Pérdidas de memoria

Al crear un objeto en la memoria dinámica, el programador no puede eliminarlo una vez que se completa el uso. Si a una variable que hace referencia a un objeto se le asigna un nuevo valor y no hay otras referencias al objeto, se vuelve inaccesible mediante programación, pero sigue ocupando memoria porque no se llamó al comando de eliminación. Esta situación se denomina pérdida de memoria . 

Si los objetos, cuyas referencias se pierden, se crean constantemente en el programa, entonces una fuga de memoria se manifiesta en un aumento gradual en la cantidad de memoria utilizada; si el programa se ejecuta durante mucho tiempo, la cantidad de memoria que utiliza crece constantemente y, después de un tiempo, el sistema se ralentiza notablemente (debido a la necesidad de usar el intercambio para cualquier asignación de memoria ), o el programa agota el espacio de direcciones disponible y termina con un error.

Mecanismo de recolección de basura

Si la memoria de la computadora fuera infinita , sería posible simplemente dejar objetos innecesarios en la memoria. Gestión automática de memoria con recolección de basura: emulación de una computadora tan infinita en una memoria finita [2] . Muchas de las limitaciones de los recolectores de basura (no hay garantía de que un finalizador se ejecute; solo administra la memoria, no otros recursos) se derivan de esta metáfora.

Principios básicos

En un sistema de recolección de elementos no utilizados, es responsabilidad del entorno de ejecución del programa desasignar la memoria. El programador solo crea objetos dinámicos y los usa, puede que no le importe borrar objetos, ya que el entorno lo hace por él. Para hacer esto, se incluye un módulo de software especial llamado "recolector de basura" en el entorno de tiempo de ejecución. Este módulo se ejecuta periódicamente, determina cuáles de los objetos creados en la memoria dinámica ya no se utilizan y libera la memoria que ocupan.

La frecuencia de ejecución del recolector de basura está determinada por las características del sistema. El recopilador puede ejecutarse en segundo plano, comenzando cuando el programa está inactivo (por ejemplo, cuando el programa está inactivo, esperando la entrada del usuario). El recolector de basura se ejecuta incondicionalmente, deteniendo la ejecución del programa ( Stop- the  -world ) cuando no se puede realizar la siguiente operación de asignación de memoria debido a que se ha agotado toda la memoria disponible. Una vez que se libera la memoria, se reanuda la operación de asignación de memoria interrumpida y el programa continúa ejecutándose. Si resulta que no se puede liberar la memoria, el tiempo de ejecución finaliza el programa con un mensaje de error "Memoria insuficiente".

Accesibilidad de objetos

Sería óptimo eliminar de la memoria los objetos a los que no se accederá en el curso de la operación posterior del programa. Sin embargo, la identificación de tales objetos es imposible, ya que se reduce a un problema de detención algorítmicamente insoluble (para esto, basta con suponer que algún objeto X se usará si y solo si el programa P se completa con éxito ). Por lo tanto, los recolectores de basura usan estimaciones conservadoras para asegurarse de que un objeto no se use en el futuro.

Por lo general, el criterio de que un objeto todavía está en uso es la presencia de referencias a él: si no hay más referencias a este objeto en el sistema, entonces, obviamente, ya no puede ser utilizado por el programa y, por lo tanto, puede ser eliminado Este criterio es utilizado por la mayoría de los recolectores de basura modernos y también se denomina accesibilidad de objetos . No es teóricamente el mejor, ya que según él, los objetos alcanzables también incluyen aquellos objetos que nunca se usarán, pero a los que todavía hay referencias, pero garantiza la protección contra la aparición de referencias "colgantes" y puede implementarse de manera bastante eficiente. .

De manera informal, se puede dar la siguiente definición recursiva de un objeto alcanzable:

Algoritmo de bandera

Un algoritmo simple para determinar los objetos alcanzables, el algoritmo Mark and Sweep, es el siguiente:

  • para cada objeto, se almacena un bit que indica si este objeto es accesible desde el programa o no;
  • inicialmente, todos los objetos, excepto los raíz, se marcan como inalcanzables;
  • se escanean recursivamente y se marcan como objetos alcanzables, aún no marcados, y a los que se puede llegar desde los objetos raíz mediante referencias;
  • aquellos objetos para los que no se ha establecido el bit de accesibilidad se consideran inalcanzables.

Si dos o más objetos se refieren entre sí, pero no se hace referencia a ninguno de estos objetos desde el exterior, el grupo completo se considera inalcanzable. Este algoritmo le permite garantizar la eliminación de grupos de objetos cuyo uso ha cesado, pero en los que existen vínculos entre sí. Estos grupos a menudo se denominan "islas de aislamiento".

Algoritmo de conteo de referencias

Otra variante del algoritmo de accesibilidad es el habitual recuento de referencias . Su uso ralentiza las operaciones de asignación de referencias, pero la definición de objetos accesibles es trivial: todos estos son objetos cuyo valor de recuento de referencias es superior a cero. Sin aclaraciones adicionales, este algoritmo, a diferencia del anterior, no elimina cadenas cerradas cíclicamente de objetos obsoletos que tienen enlaces entre sí.

Estrategias de recolección de basura

Una vez que se define un conjunto de objetos inalcanzables, el recolector de basura puede desasignar la memoria ocupada por ellos y dejar el resto como está. También es posible mover todo o parte de los objetos restantes a otras áreas de memoria después de liberar memoria, actualizando todas las referencias a ellos junto con esto. Estas dos implementaciones se conocen como no reubicables y reubicables , respectivamente .

Ambas estrategias tienen ventajas y desventajas.

Velocidad de asignación y desasignación de memoria Un recolector de basura que no se reubica libera memoria más rápido (porque solo marca los bloques de memoria apropiados como libres), pero dedica más tiempo a asignarla (porque la memoria se fragmenta y la asignación tiene que encontrar la cantidad correcta de bloques de tamaño apropiado en la memoria ). El recolector de movimiento tarda relativamente más en recolectar basura (se necesita más tiempo para desfragmentar la memoria y cambiar todas las referencias a los objetos que se están moviendo), pero el movimiento permite un algoritmo de asignación de memoria extremadamente simple y rápido ( O(1) ). Durante la desfragmentación, los objetos se mueven para dividir toda la memoria en dos grandes áreas: ocupada y libre, y se guarda un puntero a su borde. Para asignar nueva memoria, basta con mover este límite, devolviendo una pieza desde el principio de la memoria libre. Velocidad de acceso a objetos en memoria dinámica Los objetos cuyos campos se comparten pueden ser colocados uno cerca del otro en la memoria por el colector de movimientos. Entonces es más probable que estén en la memoria caché del procesador al mismo tiempo, lo que reducirá la cantidad de accesos a la memoria RAM relativamente lenta . Compatibilidad con código extranjero La reubicación del recolector de elementos no utilizados genera problemas cuando se usa código que no está administrado por la administración automática de memoria (dicho código se denomina extraño en la terminología tradicional o no administrado en la terminología de Microsoft ) .  Un puntero a la memoria asignada en un sistema con un recopilador que no se reubica puede simplemente pasarse a un código externo para su uso, manteniendo al menos una referencia regular al objeto para que el recopilador no lo elimine. El colector en movimiento cambia la posición de los objetos en la memoria, cambiando sincrónicamente todas las referencias a ellos, pero no puede cambiar las referencias en el código externo, como resultado, las referencias pasadas al código externo después de mover el objeto se volverán incorrectas. Para trabajar con código externo, se utilizan varias técnicas especiales, por ejemplo, la fijación  es un bloqueo explícito de un objeto que prohíbe su movimiento durante la recolección de basura. 

Generaciones de objetos

Como muestra la práctica, los objetos creados recientemente se vuelven inalcanzables con más frecuencia que los objetos que existen desde hace mucho tiempo. De acuerdo con este patrón, muchos recolectores de basura modernos subdividen todos los objetos en varias generaciones  , una serie de objetos con una vida útil cercana. Tan pronto como se agota la memoria asignada a una de las generaciones, en esta generación y en todas las generaciones “más jóvenes”, se realiza una búsqueda de objetos inalcanzables. Todos ellos se eliminan y los restantes se transfieren a la generación "más antigua".

El uso de generaciones reduce el tiempo del ciclo de recolección de basura al reducir la cantidad de objetos que se escanean durante la recolección, pero este método requiere que el tiempo de ejecución realice un seguimiento de las referencias entre diferentes generaciones.

Otros mecanismos

objetos inmutables _ _  Las reglas de un lenguaje de programación pueden establecer que los objetos declarados de una manera especial o de ciertos tipos son fundamentalmente inmutables. Por ejemplo, estas son cadenas de caracteres en Java y otros lenguajes. Debido a la información de inmutabilidad, el sistema de administración de memoria puede ahorrar espacio. Por ejemplo, cuando a una variable de cadena se le asigna el valor "Hello", la cadena se coloca en la memoria y la variable obtiene una referencia a ella. Pero si posteriormente se inicializa otra variable con la misma cadena, el sistema encontrará la cadena creada previamente "Hello"en la memoria y le asignará una referencia a la segunda variable, en lugar de reasignar la cadena en la memoria. Dado que la cadena no cambia fundamentalmente, tal decisión no afectará la lógica del programa de ninguna manera, pero la cadena no se duplicará en la memoria, sin importar cuántas veces se use. Y solo cuando se eliminen todas las referencias, el recolector de basura destruirá la línea. Por regla general, tales objetos constantes se almacenan en áreas de memoria especialmente asignadas denominadas "grupos" (el área para almacenar cadenas sin cambios es el "grupo de cadenas"), para un trabajo eficiente con el que se pueden usar algoritmos bastante específicos. finalizadores Un finalizador es un código que se ejecuta automáticamente justo antes de que el recolector de elementos no utilizados elimine un objeto de la memoria. Los finalizadores se utilizan para comprobar si un objeto se ha limpiado y liberar memoria adicional si se asignó durante la creación o el funcionamiento del objeto, sin pasar por el sistema de gestión de memoria. Los programadores no calificados a menudo intentan usar finalizadores para liberar archivos , sockets de red y otros recursos del sistema utilizados por los objetos. Esta es una práctica extremadamente mala: dado que cuando un objeto se recolecta como basura depende de la cantidad de memoria disponible y de la cantidad de memoria utilizada por el programa, es imposible predecir cuándo se llamará al finalizador y si se llamará en absoluto. Los finalizadores no son adecuados para liberar ningún recurso del sistema que no sea RAM; el programador debe cerrar manualmente los archivos o sockets con un comando como close(), cuando el objeto ya no está en uso.

Idioma y requisitos del sistema

Para que un programa utilice la recolección de elementos no utilizados, se deben cumplir una serie de condiciones relacionadas con el lenguaje, el entorno de ejecución y la tarea en sí.

La necesidad de un tiempo de ejecución con un recolector de basura Naturalmente, la recolección de basura requiere un entorno dinámico que admita la ejecución del programa y la presencia de un recolector de basura en este entorno. Para los idiomas interpretados o los idiomas compilados en el código de bytes de la máquina virtual, el recolector de elementos no utilizados puede incluirse en el código del intérprete de idioma o código de bytes, pero para los idiomas compilados en el código objeto, el recolector de elementos no utilizados se ve obligado a formar parte del sistema. biblioteca, que se vincula (estática o dinámicamente) con el código del programa al crear un archivo ejecutable, aumentando el tamaño del programa y su tiempo de carga. Soporte de lenguaje de programación El recolector de basura solo puede funcionar correctamente cuando puede rastrear con precisión todas las referencias a todos los objetos creados. Obviamente, si el lenguaje permite la conversión de referencias (punteros) a otros tipos de datos (enteros, matrices de bytes, etc.), como C / C++ , se vuelve imposible rastrear el uso de dichas referencias convertidas y la recolección de basura no tiene sentido. - no protege de enlaces "colgantes" y pérdidas de memoria. Por lo tanto, los lenguajes orientados a la recolección de basura generalmente restringen significativamente la libertad de usar punteros, abordar la aritmética, las conversiones de tipos de punteros a otros tipos de datos. Algunos de ellos no tienen ningún tipo de datos de "puntero", otros sí, pero no permiten conversiones ni cambios de tipo. Admisibilidad técnica de los retrasos a corto plazo en el trabajo de los programas La recolección de basura se realiza periódicamente, generalmente en horarios desconocidos. Si suspender el programa por un tiempo comparable al tiempo de recolección de basura puede conducir a errores críticos , obviamente es imposible usar la recolección de basura en tal situación. Tener alguna reserva de memoria libre Cuanta más memoria esté disponible para el tiempo de ejecución, menos se ejecutará el recolector de elementos no utilizados y más eficiente será. Ejecutar un recolector de elementos no utilizados en un sistema donde la cantidad de memoria disponible para el recolector de elementos no utilizados se acerca a la demanda máxima del programa puede ser ineficiente y un desperdicio. Cuanto menos excedente de memoria, más a menudo se ejecuta el recopilador y más tiempo lleva ejecutarlo. La caída en el rendimiento del programa en este modo puede ser demasiado significativa.

Problemas de uso

Al contrario de lo que suele decirse, la presencia de recolección de basura no libera al programador de todos los problemas de gestión de memoria.

Liberar otros recursos ocupados por el objeto. Además de la memoria dinámica, un objeto puede poseer otros recursos, a veces más valiosos que la memoria. Si un objeto abre un archivo al crearlo, debe cerrarlo al finalizar el uso; si se conecta a un DBMS, debe desconectarse. En los sistemas con gestión de memoria manual, esto se hace inmediatamente antes de que el objeto se elimine de la memoria, con mayor frecuencia en los destructores de los objetos correspondientes. En los sistemas con recolección de basura, suele ser posible ejecutar algún código justo antes de borrar un objeto, los llamados finalizers , pero no son adecuados para liberar recursos, ya que no se sabe de antemano el momento del borrado, y puede convertirse que el recurso se libera mucho más tarde de que el objeto deja de ser utilizado. En tales casos, el programador todavía tiene que rastrear el uso del objeto manualmente y realizar operaciones manualmente para liberar los recursos ocupados por el objeto. En C# , existe una interfaz específica para este propósito , IDisposableen Java-  .AutoCloseable Pérdida de memoria En sistemas con recolección de basura, también pueden ocurrir pérdidas de memoria, aunque tienen una naturaleza ligeramente diferente. Una referencia a un objeto no utilizado se puede almacenar en otro objeto que se está utilizando y se convierte en una especie de "ancla" que retiene el objeto innecesario en la memoria. Por ejemplo, el objeto creado se agrega a la colección utilizada para operaciones auxiliares, luego deja de usarse, pero no se elimina de la colección. La colección contiene la referencia, el objeto permanece accesible y no se recolecta basura. El resultado es la misma pérdida de memoria. Para eliminar tales problemas, el tiempo de ejecución puede admitir una característica especial: las llamadas referencias débiles . Las referencias débiles no retienen el objeto y se convierten nulltan pronto como el objeto desaparece, por lo que el código debe estar preparado para el hecho de que un día la referencia no apuntará a ninguna parte. Pérdida de eficiencia en operaciones con asignación y desasignación de memoria frecuente Algunas acciones que son bastante inofensivas en sistemas con administración de memoria manual pueden generar una sobrecarga desproporcionadamente grande en sistemas con recolección de elementos no utilizados. Un ejemplo clásico de tal problema se muestra a continuación. Cadena fuera = "" ; // Se supone que las cadenas contienen una gran cantidad de cadenas cortas, // de las cuales debe recopilar una cadena grande en la variable de salida. for ( String str : strings ) { out += str ; // Este código creará // una nueva variable de cadena en cada iteración y le asignará memoria. } Este código Java parece como si la variable out, creada una vez, se "añadiera" con una nueva línea cada vez en el ciclo. De hecho, las cadenas en Java son inmutables, por lo que en este código, en cada paso del ciclo, ocurrirá lo siguiente:
  1. Cree una nueva variable de cadena de longitud suficiente.
  2. Copiando los contenidos antiguos de out a una nueva variable.
  3. Copie a una nueva variable de contenido str.
  4. Asignar a la variable de salida una referencia a una nueva variable de cadena.
En este caso, cada vez que el bloque de memoria, que anteriormente contenía el valor de la variable out, quedará fuera de uso y esperará hasta que se inicie el recolector de basura. Si se combinan 100 cadenas de 100 caracteres de esta manera, en total se asignarán más de 500 000 bytes de memoria para esta operación, es decir, 50 veces más que el tamaño de la cadena "larga" final. Tales operaciones, cuando a menudo se crean objetos suficientemente grandes en la memoria y luego dejan de usarse inmediatamente, conducen a un llenado improductivo muy rápido de toda la memoria disponible y al inicio frecuente del recolector de elementos no utilizados, lo que, bajo ciertas condiciones, puede ralentizar considerablemente el proceso. programa o, al menos, requieren que se le asigne para trabajar inadecuadamente gran cantidad de memoria. Para evitar estos problemas, el programador debe tener un buen conocimiento del mecanismo de gestión automática de la memoria. A veces también se pueden utilizar medios especiales para llevar a cabo operaciones peligrosas de manera eficiente. Entonces, para optimizar el ejemplo anterior, debe usar la clase especial StringBuilder, que le permite asignar memoria inmediatamente para toda la cadena en una sola acción, y en el bucle solo agregar el siguiente fragmento al final de esta cadena. Problemas de interacción con código foráneo y trabajo directo con memoria física En la programación práctica en lenguajes con recolección de basura, es casi imposible prescindir de la interacción con el llamado código extranjero: las API del sistema operativo, los controladores de dispositivos, los módulos de programas externos escritos en otros idiomas no están controlados por el recolector de basura . A veces se hace necesario trabajar directamente con la memoria física de la computadora; el sistema de administración de memoria también limita esto, si es que lo hace. La interacción con el código externo se proporciona de una de dos maneras: se escribe un envoltorio para el código externo en un lenguaje de bajo nivel (generalmente en C), ocultando detalles de bajo nivel, o se agrega una sintaxis directamente al lenguaje que proporciona el capacidad de escribir código "inseguro" (inseguro): fragmentos o módulos separados para los cuales el programador tiene un mayor control sobre todos los aspectos de la administración de la memoria. Tanto la primera como la segunda solución tienen sus inconvenientes. Los envoltorios tienden a ser complejos, requieren mucha habilidad para desarrollarse y es posible que no sean portátiles. (Sin embargo, su creación se puede automatizar. Por ejemplo, hay un generador SWIG multilingüe que, utilizando los archivos de encabezado C/C++ disponibles, crea automáticamente contenedores para varios idiomas que admiten la recolección de basura). Están sujetos a obsolescencia: un contenedor escrito para la implementación de un idioma puede volverse inutilizable en otro, como cuando se cambia de un recolector de basura que no se reubica a uno que se reubica. La sintaxis especial del código inseguro es un "agujero legal" en el mecanismo de administración de la memoria y una fuente de errores difíciles de encontrar; al mismo tiempo, por su sola presencia, provoca que el programador eluda las restricciones del idioma. Además, cualquier interferencia con el trabajo del recolector de basura (y es inevitable cuando se interactúa con código extranjero) reduce potencialmente la eficiencia de su trabajo. Por ejemplo, arreglar una determinada región en la memoria, lo cual es necesario para que el recolector de elementos no utilizados no elimine ni mueva código extraño mientras trabaja con esta memoria, puede limitar la capacidad de desfragmentar la memoria y, por lo tanto, dificultar la asignación posterior de fragmentos de la memoria. tamaño deseado, incluso si hay suficiente espacio total memoria libre.

Ventajas y desventajas

En comparación con la gestión manual de la memoria, la recolección de elementos no utilizados es más segura porque evita las fugas de memoria y los enlaces colgantes que provocan la eliminación prematura de objetos. También simplifica el proceso de programación en sí .

Se cree que la recolección de basura reduce significativamente la sobrecarga de administración de memoria en comparación con los lenguajes que no la implementan. Según un estudio [3] , los programadores de C dedican entre el 30 % y el 40 % de su tiempo total de desarrollo (excluyendo la depuración) solo a la gestión de la memoria. Sin embargo, existen estudios con conclusiones opuestas, por ejemplo, en [4] se afirma que la diferencia real en la velocidad de desarrollo de software en C++, donde no existe recolección automática de basura, y en Java, donde se implementa , es pequeño.

La presencia de un recolector de basura en un desarrollador sin experiencia puede crear la falsa creencia de que no necesita prestar atención a la gestión de la memoria en absoluto. Si bien el recolector de basura reduce los problemas de mala administración de la memoria, no los elimina por completo, y los que persisten no se muestran como errores obvios, como un error de protección general , sino como memoria desperdiciada cuando se ejecuta un programa. Un ejemplo típico: si el programador ha perdido de vista el hecho de que queda al menos un puntero no anulable en el objeto en el ámbito global, dicho objeto nunca se eliminará; encontrar una pseudofuga de este tipo puede ser muy difícil.

A menudo, es fundamental no solo asegurarse de que el recurso se libere, sino también asegurarse de que se libere antes de que se llame a algún otro procedimiento, por ejemplo, archivos abiertos, entradas en secciones críticas. Los intentos de otorgar el control de estos recursos al recolector de basura (a través de finalizadores ) serán ineficientes o incluso incorrectos, por lo que deberá administrarlos manualmente. Recientemente, incluso en lenguajes con un recolector de basura, se ha introducido una sintaxis que garantiza la ejecución de "código de limpieza" (por ejemplo, un método especial de "destructor") cuando una variable que hace referencia a un objeto queda fuera del alcance.

En muchos casos, los sistemas con recolección de basura son menos eficientes, tanto en términos de velocidad como de uso de memoria (lo cual es inevitable, ya que el recolector de basura en sí mismo consume recursos y necesita un exceso de memoria libre para funcionar correctamente). Además, en sistemas con recolección de basura, es más difícil implementar algoritmos de bajo nivel que requieran acceso directo a la memoria RAM de la computadora, ya que el uso libre de punteros es imposible y el acceso directo a la memoria requiere interfaces especiales escritas en lenguajes de bajo nivel. . Por otro lado, los sistemas modernos de recolección de basura utilizan algoritmos de administración de memoria muy eficientes con una sobrecarga mínima. También es imposible no tener en cuenta el hecho de que ahora la memoria RAM es relativamente barata y está disponible. Bajo tales condiciones, las situaciones en las que son los costos de recolección de basura los que se vuelven críticos para la eficiencia del programa son extremadamente raras.

La ventaja significativa de la recolección de basura es cuando los objetos creados dinámicamente viven durante mucho tiempo, se duplican muchas veces y las referencias a ellos se pasan entre diferentes partes del programa. En tales condiciones, es bastante difícil determinar el lugar donde el objeto ha dejado de usarse y puede eliminarse. Dado que esta es precisamente la situación con el uso generalizado de estructuras de datos que cambian dinámicamente (listas, árboles, gráficos), la recolección de basura es necesaria en lenguajes funcionales y lógicos que usan ampliamente tales estructuras, como Haskell , Lisp o Prolog . El uso de la recolección de basura en los lenguajes imperativos tradicionales (basados ​​en un paradigma estructural, quizás complementado por las facilidades de los objetos) está determinado por el equilibrio deseado entre la simplicidad y la velocidad del desarrollo del programa y la eficiencia de su ejecución.

Alternativas

El soporte en algunos lenguajes imperativos para llamar automáticamente al destructor cuando un objeto sale del alcance sintáctico ( C++ [5] , Ada , Delphi ) le permite colocar el código de liberación de memoria en el destructor y asegurarse de que se llamará de todos modos . Esto le permite concentrar lugares peligrosos dentro de la implementación de la clase y no requiere recursos adicionales, aunque impone requisitos más altos en las calificaciones del programador. Al mismo tiempo, es posible liberar de forma segura otros recursos ocupados por el objeto en el destructor.

Una alternativa a la recolección de basura es la tecnología de uso de " referencias inteligentes ", cuando una referencia a un objeto dinámico en sí mismo realiza un seguimiento del número de usuarios y elimina automáticamente el objeto cuando este número se convierte en cero. Un problema bien conocido con las "referencias inteligentes" es que en condiciones en las que el programa crea constantemente muchos objetos pequeños de corta duración en la memoria (por ejemplo, al procesar estructuras de listas), pierden rendimiento frente a la recolección de elementos no utilizados.

Desde la década de 1960, existe la gestión de memoria basada en regiones , una  tecnología en la que la memoria se divide en fragmentos relativamente grandes llamados regiones , y ya dentro de las regiones, la memoria se asigna a objetos individuales. Con el control manual, las regiones son creadas y eliminadas por el propio programador, con el control automático, se utilizan varios tipos de estimaciones conservadoras para determinar cuándo dejan de usarse todos los objetos asignados dentro de la región, después de lo cual el sistema de administración de memoria elimina toda la región. Por ejemplo, se crea una región en la que se asigna memoria para todos los objetos creados dentro de un determinado ámbito, no pasados ​​fuera, y esta región se destruye con un comando cuando la ejecución del programa sale de este ámbito. La transición en la gestión de la memoria (ya sea manual o automática) de objetos individuales a unidades más grandes en muchos casos nos permite simplificar la contabilidad de la vida útil de los objetos y, al mismo tiempo, reducir los costos generales. Existen implementaciones (de diversos grados de automatización) de gestión de memoria regional para muchos lenguajes de programación, incluidos ML , Prolog , C , Cyclone .

El lenguaje de programación Rust ofrece el concepto de "propiedad" basado en el estricto control del compilador sobre la vida útil y el alcance de los objetos. La idea es que cuando se crea un objeto, la variable a la que se le asigna una referencia se convierte en el "propietario" de ese objeto, y el alcance de la variable de propietario limita la vida útil del objeto. Al salir del alcance del propietario, el objeto se elimina automáticamente. Al asignar una referencia de objeto a otra variable, se puede "tomar prestado", pero el préstamo siempre es temporal y debe completarse durante la vida del propietario del objeto. La "propiedad" se puede transferir a otra variable (por ejemplo, se puede crear un objeto dentro de una función y devolverlo como resultado), pero el propietario original pierde el acceso al objeto. En conjunto, las reglas están diseñadas para garantizar que un objeto no se pueda modificar sin control a través de referencias extrañas. El compilador realiza un seguimiento estático de la vida útil de los objetos: cualquier operación que incluso pueda conducir potencialmente a guardar una referencia a un objeto después de que su propietario quede fuera del alcance conduce a un error de compilación, lo que elimina la aparición de "referencias colgantes" y pérdidas de memoria. Este enfoque complica la técnica de programación (respectivamente, dificultando el aprendizaje del lenguaje), pero elimina la necesidad tanto de la asignación manual como de la desasignación de memoria, y el uso de la recolección de elementos no utilizados.

Gestión de memoria en lenguajes y sistemas específicos

La recolección de basura como un atributo indispensable del entorno de ejecución del programa se utiliza en lenguajes basados ​​​​en el paradigma declarativo , como LISP , ML , Prolog , Haskell . Su necesidad en este caso se debe a la propia naturaleza de estos lenguajes, que no contienen herramientas para gestionar manualmente el tiempo de vida de los objetos y no tienen la posibilidad de integración natural de dichas herramientas. La estructura básica de datos complejos en dichos lenguajes suele ser una lista dinámica de enlaces simples que consta de celdas de lista asignadas dinámicamente. Las listas se crean, copian, duplican, combinan y dividen constantemente, lo que hace que sea casi imposible administrar manualmente la vida útil de cada celda de la lista asignada.

En lenguajes imperativos, la recolección de basura es una opción, junto con el manual y algunas técnicas de administración de memoria alternativas Aquí se considera como un medio para simplificar la programación y prevenir errores . Uno de los primeros lenguajes imperativos compilados con recolección de basura fue Oberon , que demostró la aplicabilidad y la eficiencia bastante alta de este mecanismo para este tipo de lenguaje, pero el lenguaje Java trajo gran popularidad y popularidad a este enfoque . Posteriormente, el enfoque de Java se repitió en el entorno .NET y en casi todos los lenguajes que trabajan en él, comenzando con C# y Visual Basic .NET . Al mismo tiempo, aparecieron muchos lenguajes interpretados (JavaScript, Python, Ruby, Lua), donde se incluyó la recolección de basura por razones de accesibilidad del lenguaje para los no programadores y simplificación de la codificación. El aumento de la potencia del hardware, que se produjo simultáneamente con la mejora de los propios recolectores, llevó al hecho de que la sobrecarga adicional para la recolección de basura dejó de ser significativa. La mayoría de los lenguajes imperativos modernos recolectados en basura no tienen ninguna forma de eliminar objetos explícitamente de forma manual (como el operador de eliminación). En los sistemas que usan un intérprete o compilan a código de bytes, el recolector de elementos no utilizados es parte del tiempo de ejecución; en los mismos lenguajes que compilan a código objeto de procesador, se implementa como una biblioteca de sistema requerida.

También hay una pequeña cantidad de idiomas ( nim , Modula-3 , D ) que admiten la gestión de memoria tanto manual como automática, para lo cual la aplicación utiliza dos montones separados.

Notas

  1. Un término establecido, desde el punto de vista del idioma ruso , "recolección de basura" es más correcto ( extracto de los diccionarios ABBYY Lingvo Copia de archivo fechada el 25 de abril de 2017 en Wayback Machine , diccionario de Ushakov : construir Copia de archivo fechada el 25 de abril de 2017 en Wayback Machine , colección Copia de archivo fechada el 25 de abril de 2017 en Wayback Machine , recopilación Archivado el 25 de abril de 2017 en Wayback Machine ; Gramota.ru : discusión Archivado el 25 de abril de 2017 en Wayback Machine ). Según el diccionario, ensamblar es “juntar partes separadas, detalles, hacer, crear algo, convertirlo en algo listo” y es “recolección” la que se aplica al resto de los significados de la palabra “ensamblar”.
  2. Raymond Chen . Debes estar pensando en la recolección de basura de manera incorrecta. Archivado el 19 de julio de 2013 en Wayback Machine .
  3. Boehm H. Ventajas y desventajas de la recolección de basura conservadora . Archivado desde el original el 24 de julio de 2013.
    (enlace de Raymond, Eric . El arte de la programación Unix.. - 2005. - p. 357. - 544 p. - ISBN 5-8459-0791-8 . )
  4. Lutz Prechelt. Una comparación empírica de C, C++, Java, Perl, Python, Rexx y  Tcl . Instituto de Tecnología de Karlsruhe . Consultado el 26 de octubre de 2013. Archivado desde el original el 3 de enero de 2020.
  5. RAII, objetos dinámicos y fábricas en C++, Roland Pibinger, 3 de mayo de 2005 . Fecha de acceso: 14 de febrero de 2016. Archivado desde el original el 5 de marzo de 2016.