Memoria en C# bajo el microscopio: cómo funciona el recolector de basura y por qué es importante

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Magia que estás obligado a entender

\n\n

Cualquiera que haya escrito en C# al menos un par de semanas sabe: la memoria es gestionada por el entorno de ejecución (CLR). Simplemente creas objetos con new, y cuando ya no son necesarios, el recolector de basura (GC) llega y lo limpia todo. Suena a magia, ¿verdad? Pero detrás de esa magia hay una ingeniería rigurosa, y si no la entiendes, tu aplicación empezará a «ir lenta», «consumir» gigabytes de RAM o incluso fallará con un OutOfMemoryException.

\n\n

En este artículo, miraremos bajo el capó de CLR y entenderemos exactamente cómo funciona la gestión de memoria en C#. Aprenderás la diferencia entre la pila y el montón, qué son las generaciones de objetos, por qué IDisposable es tu mejor amigo y cómo evitar problemas con Large Object Heap. ¡Allá vamos!

\n\n

Pila y montón: el fundamento del que no se habla en los libros de texto

\n\n

Cuando tu programa se ejecuta, CLR divide la memoria en dos áreas principales: la pila (Stack) y el montón (Heap). Entender la diferencia entre ellas es la base sin la cual todo lo demás pierde sentido.

\n\n

La pila: rápida pero pequeña

\n

La pila es un área de memoria donde se almacenan los value types (int, bool, struct, enum) y las referencias a objetos. Funciona bajo el principio LIFO (Last In, First Out). Cuando llamas a un método, se asigna un marco en la pila. Tan pronto como el método finaliza, el marco se destruye instantáneamente. Aquí no se necesita ningún recolector de basura. La velocidad de acceso a la pila es máxima, pero su tamaño es limitado (normalmente 1-4 MB por hilo).

\n\n
void Example()\n{\n    int x = 42;          // x está en la pila\n    string s = "Hola";   // la referencia s está en la pila, el objeto en sí está en el montón\n}\n
\n\n

El montón: más lento pero más espacioso

\n

El montón (Managed Heap) es la memoria «a largo plazo». Aquí viven todos los reference types (clases, cadenas, arrays, delegados). La asignación de memoria en el montón ocurre con cada new. Y aquí es donde entra en juego el recolector de basura. Es él quien decide cuándo se puede liberar la memoria y cuándo esperar.

\n\n

Importante: La pila no es «mala» ni el montón es «bueno». Cada uno tiene su función. Los value types son rápidos, pero no se pueden pasar por referencia sin la palabra clave ref. Los reference types son flexibles, pero requieren GC.

\n\n

Cómo funciona el recolector de basura en C#: generaciones y algoritmo

\n\n

El recolector de basura en .NET no es simplemente «eliminar y olvidar». Es un mecanismo inteligente que utiliza generaciones (Generations) para minimizar las pausas en la ejecución de la aplicación.

\n\n

Las tres generaciones de objetos

\n
    \n
  • Gen 0 — objetos recién creados. Viven poco tiempo. La recolección de Gen 0 ocurre con frecuencia y es muy rápida.
  • \n
  • Gen 1 — «supervivientes» después de una recolección. Es una zona de amortiguación entre objetos jóvenes y viejos.
  • \n
  • Gen 2 — objetos de larga duración. La recolección de Gen 2 es la más costosa y rara. Si un objeto llega a Gen 2, lo más probable es que permanezca allí durante mucho tiempo.
  • \n
\n\n

El algoritmo de funcionamiento del GC, de forma simplificada, es el siguiente:

\n
    \n
  1. La aplicación solicita memoria. Si no hay espacio en el montón, se inicia una recolección.
  2. \n
  3. El GC construye un gráfico de accesibilidad de objetos, comenzando desde las raíces (campos estáticos globales, variables locales en las pilas de los hilos, registros de la CPU).
  4. \n
  5. Todos los objetos inaccesibles se marcan como «basura».
  6. \n
  7. Se produce una compactación: los objetos vivos se desplazan para evitar la fragmentación.
  8. \n
  9. La memoria liberada se devuelve al pool.
  10. \n
\n\n

Aquí tienes un ejemplo claro de cómo el GC afecta al rendimiento:

\n\n
class HeavyObject\n{\n    public byte[] Data = new byte[1024 * 1024]; // 1 MB\n}\n\nvoid TestGC()\n{\n    for (int i = 0; i < 1000; i++)\n    {\n        var obj = new HeavyObject(); // Cada vez asignamos 1 MB en el montón\n        // obj sale del ámbito — se convierte en basura\n    }\n    // Después del bucle, el GC puede recolectar Gen 0 no                

Blogs

Book Recommendations