¿Por qué Garbage Collection toma mucha RAM?

No hay una sola cosa llamada recolección de basura, hay varios algoritmos y estrategias, y la respuesta depende tanto de la implementación real como de los patrones de uso de memoria de cada aplicación. Es por eso que algunos sistemas realmente admiten múltiples algoritmos y permiten que el desarrollador decida cuál usar. Y casi todos los sistemas, incluso aquellos que utilizan un solo algoritmo, admiten algún tipo de API de configuración / administración de GC para casos especiales.

Una razón por la cual GC usa más memoria que otras técnicas de administración de memoria es que la memoria no se recupera inmediatamente cuando no se utiliza. La estrategia para decidir cuándo comenzar un pase de GC varía según el sistema y puede ser diferente para los objetos asignados recientemente y para los objetos que han estado en uso durante algún tiempo. En todos los casos, pasa un tiempo desde que un objeto queda “sin usar” y desde el momento en que se recupera su memoria, lo que implica que, en un sistema GC, al menos parte de la memoria que actualmente está “asignada” no se utiliza: es la basura que finalmente se liberará la próxima vez que se active el GC.

Otra razón es que los llamados “colectores de copia” copiarán efectivamente todos los objetos que actualmente están “vivos” en un área recientemente asignada cada vez que se ejecute el GC. Esto significa que la aplicación usa al menos 2 veces más memoria de la que necesitaría: cada vez que el GC comienza a ejecutarse, debe haber guardado una cantidad de memoria libre que es tan grande como la memoria actualmente en uso, incluida la memoria activa y la basura objetos (porque todavía no sabe lo que está vivo y lo que es basura).

Uno puede pensar que tal estrategia es absurda: ¿por qué usar el doble de memoria? La respuesta es que para algunas aplicaciones esta estrategia es extremadamente efectiva. La asignación de memoria es extremadamente rápida (mucho más rápida que con cualquier otro mecanismo) y la recolección de basura también puede ejecutarse relativamente rápido si la tasa de supervivencia de los objetos es baja. De hecho, para una aplicación en la que se asigna una cantidad extremadamente grande de objetos pero su supervivencia es muy baja (casi todos se asignan, se usan inmediatamente y luego se abandonan) un recolector de basura de copiado es posiblemente la solución más rápida: podría ser mucho más rápido que la gestión manual de memoria.

En la mayoría de los casos (no todos, siempre hay ejemplos contrarios para cada estrategia: no existe una estrategia que sea siempre mejor que cualquier otra) una estrategia de gestión de memoria basada en GC proporciona un mejor rendimiento general que otros sistemas, incluida la gestión manual. Los inconvenientes son que usa más memoria (las estadísticas dicen que aproximadamente 1.6 a 1.8 veces más para la aplicación promedio en una implementación moderna de GC) y que la ejecución de la aplicación no es “uniforme” de una manera no predecible, lo que puede ser un gran problema a tiempo sistemas críticos y en aplicaciones como juegos que necesitan garantizar una velocidad de fotogramas uniforme.

La recolección de elementos no utilizados es el proceso mediante el cual se asigna la memoria asignada pero ahora no utilizada para su reutilización. El objetivo de la recolección de basura es recuperar la memoria que ya no se usa para poder reutilizarla (o liberarla al sistema operativo). La mayoría de los recolectores de basura usan algo de memoria como sobrecarga, pero todos los diseños razonables buscan mantener esto lo más bajo posible.

Por lo tanto, no es que la recolección de basura use mucha memoria, sino que usar mucha memoria es lo que causa la necesidad de la recolección de basura.

La recolección de basura en sí misma no usa mucha RAM (bueno, la copia de los recolectores de basura sí). Sin embargo, la recolección de basura generalizada es un proceso costoso que se ejecuta periódicamente, en función del tiempo o de un disparador basado en el uso de RAM. Como resultado, se acumula cierta cantidad de basura antes de que finalmente se recolecte.

Esto difiere de la asignación de memoria administrada explícitamente o la recolección de basura de conteo de referencias, que generalmente libera memoria inmediatamente después de que ya no es necesaria, reduciendo así la memoria desperdiciada considerablemente. Esto también elimina los problemas de programación que resultan de ejecuciones de GC inusualmente costosas. El resultado es un software mucho más fluido que usa mucha menos memoria … y muchos más errores.

La respuesta de Andrea Ferro es mucho más completa que la mía, aunque discutiría la afirmación de que un sistema con un GC necesariamente se ejecuta más rápido que un sistema sin uno. Se ejecuta un poco más rápido hasta que ejecute el GC. En promedio, a menudo será más lento. Sin embargo, la asignación manual y gratuita se puede realizar de manera deficiente y, a menudo, puede resultar en un código más lento solo debido a la incompetencia relativa.

Como otros han señalado, la recolección de basura no necesariamente usa una gran cantidad de memoria. Pero hay una gran variedad de algoritmos de recolección de basura. Cada uno de ellos debe poder rastrear asignaciones y referencias / recuentos de uso a cualquier asignación dada, y tener un medio (recolección de basura) de reclamar memoria que ya no está en uso. Algunos sistemas realizan una recolección masiva de basura: solo ejecutan el recolector de basura real cuando la memoria se está agotando. Por lo tanto, el sistema muestra un gran uso de memoria, pero no necesariamente significa que toda esa memoria se esté utilizando realmente. Dado que la recolección de basura lleva tiempo, otros sistemas realizan una recolección de basura incremental, en la que ejecutan un recolector de basura de vez en cuando sin ninguna advertencia de memoria, y probablemente pasan una cantidad fija (y por lo tanto predecible) de tiempo en el recolector de basura.

La recolección de basura también puede ahorrar memoria. Como GC suele ser una instalación de nivel de sistema, tiende a funcionar. Cuando los programadores individuales hacen su propio seguimiento de memoria, no necesariamente lo hacen sin errores, por lo que los programas muestran fugas de memoria: siguen asignando memoria que no se liberará hasta que se elimine el programa. Incluso sin errores, no necesariamente prestan mucha atención al uso de la memoria, y pueden asignar grandes fragmentos para uso temporal que se mantienen hasta que el programa salga. Eso rara vez es un problema en un sistema VM, ya que esa asignación no utilizada se paginaría de la memoria y nunca se recuperaría durante la ejecución de un programa. Pero es un asesino si estás en un sistema que usa solo memoria física.

Los sistemas GC también se ajustan para la eficiencia, tanto en el uso de la memoria como en el uso del tiempo, y pueden ser superiores a los algoritmos de seguimiento de memoria mucho más simples que un programador individual puede usar en cualquier programa dado. Pero no siempre, por supuesto.

Porque la eficiencia de un ciclo de recolección de basura depende de la proporción de objetos encontrados muertos. Por lo tanto, debemos esperar a que se acumule la basura antes de que valga la pena.

Los recolectores automáticos de basura también tienen el problema de que no tienen idea de cuánta basura hay realmente hasta que la recogen. Por lo general, es mejor errar por el lado del desperdicio de memoria que ralentizar el sistema o causar pausas innecesarias. Entonces pierden memoria.

Podemos solucionar esto un poco con GC generacionales, que pueden limitar su colección a objetos más nuevos, ya que es probable que estos mueran rápidamente. Esto nos permite recolectar basura de manera más eficiente sin desperdiciar más RAM, pero la compensación sigue ahí. Recolectar el vivero solo vale la pena cuando el vivero tiene mucha basura, y recolectar la generación poseída solo vale la pena una vez que un número razonable de objetos tenencia han muerto.

La eliminación manual, aunque consume mucho tiempo y es propensa a errores, obliga a los programadores a pensar cuándo sus objetos ya no serán necesarios y tales programas tienen muy poca basura. Al menos eso es cierto si son correctos. Sin embargo, si no pueden eliminar todos los objetos no utilizados, su huella de memoria aumentará continuamente hasta que se queden sin memoria, y no habrá un recolector de basura para guardarlos.

La recolección de basura no requiere mucha RAM.

Como anécdota, una vez traduje una base de código 50kLOC C ++ a OCaml y noté que su consumo máximo de memoria era 5 veces menor en OCaml.