Gestión de memoria y Garbage Collector en C#
Aprende gestión de memoria y garbage collector en C# para comprender el ciclo de vida de objetos y la limpieza de memoria.
En las aplicaciones .NET, la gestión de memoria (Memory Management) se realiza automáticamente mediante el Garbage Collector (GC). El desarrollador no necesita administrar manualmente el ciclo de vida de los objetos. El GC detecta los objetos no utilizados y libera la memoria. Sin embargo, comprender cómo funciona la gestión de memoria es fundamental para optimizar el rendimiento.
¿Qué es la Gestión de Memoria?
La gestión de memoria es el proceso de rastrear el ciclo de vida de los objetos creados por una aplicación en la RAM. En el entorno .NET existen dos áreas principales de memoria:
- Pila (Stack): utilizada para tipos de valor (
int,bool,struct, etc.). Es rápida y se libera automáticamente. - Montón (Heap): utilizada para tipos de referencia (
class,string,array, etc.). Es administrada por el GC.
Diferencias entre Stack y Heap
| Propiedad | Stack | Heap |
|---|---|---|
| Área de Memoria | Pequeña y rápida | Grande, administrada |
| Tipos Almacenados | Tipos de valor | Tipos de referencia |
| Gestión | Automática (basada en el alcance) | Administrada por el Garbage Collector |
| Ciclo de Vida | Se limpia al finalizar el método | Permanece hasta que el GC lo detecta |
| Velocidad de Acceso | Muy rápida | Más lenta |
¿Qué es el Garbage Collector (GC)?
El Garbage Collector es un componente que limpia automáticamente los objetos no utilizados en el heap. Esto reduce el riesgo de fugas de memoria (memory leaks). El GC se activa periódicamente durante la ejecución del programa y libera los objetos que ya no están referenciados.
using System;
class Program
{
static void Main()
{
for (int i = 0; i < 1000; i++)
{
var data = new byte[1024 * 1024]; // 1 MB
}
Console.WriteLine("Antes de la limpieza de memoria...");
GC.Collect(); // Llamada manual al GC
Console.WriteLine("Después de la limpieza de memoria...");
}
}
Llamar manualmente al GC generalmente no se recomienda; solo debe hacerse en casos especiales (por ejemplo, pruebas o procesos de alta demanda de memoria).
¿Cómo Funciona el GC?
El Garbage Collector funciona con un sistema basado en generaciones. Esto significa que los objetos de corta y larga duración se almacenan en áreas separadas.
- Gen 0 (Generación 0): Objetos recién creados. Es la zona que se limpia con mayor frecuencia.
- Gen 1: Objetos que sobrevivieron a la Gen 0. Usada para objetos de vida media.
- Gen 2: Objetos de larga duración (por ejemplo, singletons, cachés, datos estáticos).
El GC analiza todo el heap y libera los objetos que ya no son accesibles desde las referencias raíz.
Diferencia entre Dispose y Finalize
El GC por sí solo no es suficiente para una gestión completa de la memoria.
Los recursos no administrados (como archivos, conexiones de red u objetos GDI) no se limpian automáticamente.
Para estos casos, se deben usar la interfaz IDisposable y el método Finalize (destructor).
class RecursoArchivo : IDisposable
{
private bool disposed = false;
public void Escribir(string data)
{
if (disposed)
throw new ObjectDisposedException(nameof(RecursoArchivo));
Console.WriteLine($"Escribiendo datos: {data}");
}
public void Dispose()
{
if (!disposed)
{
Console.WriteLine("Recurso liberado (Dispose).");
disposed = true;
GC.SuppressFinalize(this);
}
}
~RecursoArchivo()
{
Console.WriteLine("Finalize llamado.");
}
}
// Uso:
using (var archivo = new RecursoArchivo())
{
archivo.Escribir("Prueba");
}
El método Dispose() se usa para la limpieza manual,
mientras que Finalize (destructor) es llamado por el GC.
SuppressFinalize() evita que el GC vuelva a invocar el destructor.
¿Qué es una Fuga de Memoria?
Aunque el GC es automático, las fugas de memoria pueden ocurrir si los recursos se gestionan incorrectamente. Los controladores de eventos grandes o las referencias estáticas pueden impedir que los objetos se liberen.
// Ejemplo clásico de fuga de memoria:
class Proceso
{
public event EventHandler DatosListos;
}
class Program
{
static Proceso p = new Proceso();
static void Main()
{
p.DatosListos += (s, e) => Console.WriteLine("¡El evento sigue suscrito!");
p = null; // El GC no puede liberar porque el evento mantiene una referencia.
}
}
En estos casos, los eventos deben cancelarse manualmente con -=.
Modos del GC y Configuración de Rendimiento
El GC de .NET tiene dos modos principales de operación:
- Workstation GC: optimizado para aplicaciones de escritorio (usuario único, prioridad de interfaz gráfica).
- Server GC: optimizado para recolección paralela en servidores multinúcleo.
// Ejemplo en app.config o runtimeconfig.json:
<configuration>
<runtime>
<gcServer enabled="true"/>
</runtime>
</configuration>
El método GC.TryStartNoGCRegion() puede evitar que el GC se ejecute durante un periodo determinado.
Esto garantiza una ejecución continua en aplicaciones en tiempo real.
Monitorear Eventos del GC
Puede usar métodos como GCNotification y GC.GetTotalMemory() para supervisar cuándo se ejecuta el GC o cuánta memoria se ha liberado.
using System;
class Program
{
static void Main()
{
long antes = GC.GetTotalMemory(false);
var data = new byte[10_000_000];
long despues = GC.GetTotalMemory(false);
Console.WriteLine($"Diferencia: {(despues - antes) / 1024 / 1024} MB");
GC.RegisterForFullGCNotification(10, 10);
GC.Collect();
Console.WriteLine("¡GC activado!");
}
}
Ejemplo: Procesamiento de Datos Masivos
En aplicaciones que manejan grandes volúmenes de datos, crear objetos innecesarios aumenta la carga del GC.
El siguiente ejemplo muestra la reutilización de objetos mediante ArrayPool<T> para optimizar la memoria.
using System;
using System.Buffers;
class Program
{
static void Main()
{
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024 * 1024); // Alquilar un buffer de 1 MB
for (int i = 0; i < buffer.Length; i++)
buffer[i] = 255;
Console.WriteLine("Datos procesados.");
pool.Return(buffer); // No genera carga adicional al GC
}
}
ArrayPool permite reutilizar objetos frecuentes y reduce la presión sobre el GC.
Rendimiento y Buenas Prácticas
- Evite llamar manualmente al GC; .NET lo maneja automáticamente.
- Limpie siempre los recursos no administrados con
Disposeousing. - Evite asignaciones grandes (
>85KB); el Large Object Heap (LOH) se limpia más lentamente. - No olvide cancelar eventos (
-=); de lo contrario, el GC no podrá recolectarlos. - Utilice
ArrayPooloObjectPoolen lugar de crear repetidamente objetos pequeños.
TL;DR
- El Garbage Collector limpia automáticamente los objetos no utilizados en el heap.
- El stack almacena tipos de valor rápidos y de corta duración; el heap contiene tipos de referencia.
- El GC funciona según un modelo de generaciones (0, 1, 2).
- IDisposable permite limpiar manualmente los recursos no administrados.
- Las fugas de memoria suelen originarse en eventos o referencias estáticas.
- Use pools de objetos (
ArrayPool,ObjectPool) para mejorar el rendimiento.
Artículos relacionados
Código no seguro y punteros en C#
Aprende código no seguro y punteros en C# para trabajar con direcciones de memoria y operaciones de bajo nivel.
IDisposable y el patrón using en C#
Aprende IDisposable y el patrón using en C# para gestionar recursos correctamente y evitar fugas de memoria.
Optimización del rendimiento con Span<T> y Memory<T> en C#
Aprende optimización de rendimiento en C# con Span<T> y Memory<T> para gestionar memoria de forma eficiente.