sábado, 12 de abril de 2008

Pérdidas de memoria en .NET

Una de las mejores cosas de las plataformas "manejadas" como Java y .NET es la posibilidad de no tener que preocuparnos con los detalles del control del uso de la memoria, sin embargo hay situaciones que pueden provocar pérdidas de memoria si no las tenemos en cuenta como intenta explicar Calvin Hsia en Examine .Net Memory Leaks .

Este es un tema que muchas veces confunde, asi que me tomé la libertad de copiar su ejemplo y llevarlo a C#.


using System;

namespace Test
{
class Program
{
static void Main(string[] args)
{
var oldPeak = 0L;
for (int i = 0; i < 100; i++)
{
var oWatcher = new MyWatcher();
// oWatcher.UnSubscribe(); // Quitar comentario para quitar el controlador
oWatcher = null;
GC.Collect(); // Recolectar
GC.WaitForPendingFinalizers(); // Esperar por los finalizadores ( destructures )
GC.Collect(); // Recolectar los que tienen destructores
var newPeack = System.Diagnostics.Process.GetCurrentProcess().PeakWorkingSet64;
if (i > 0)
Console.WriteLine("Todo liberado? {0} WorkingSet ={1} Pico ={2} Delta ={3}",
i, System.Diagnostics.Process.GetCurrentProcess().WorkingSet64,
newPeack, (newPeack - oldPeak));
oldPeak = newPeack;
}
GC.Collect(); // Recolectar
GC.WaitForPendingFinalizers(); // Esperar por los finalizadores ( destructures )
GC.Collect(); // Recolectar los que tienen destructores
Console.ReadKey();
}
}

class MyWatcher
{
string[] _myLargeMemoryEater = new string[100000];
System.IO.FileSystemWatcher _fsw;
public MyWatcher()
{
_fsw = new System.IO.FileSystemWatcher();
_fsw.Path = "c:\\";
_fsw.Filter = "*.*";
_fsw.Created += _fsw_Created;
_fsw.EnableRaisingEvents = true;
}
public void UnSubscribe()
{
_fsw.Created -= _fsw_Created;
_fsw.Dispose();
}

void _fsw_Created(object sender, System.IO.FileSystemEventArgs e)
{
Console.WriteLine(e.FullPath);
}
~MyWatcher()
{
Console.WriteLine("Terminado en hilo "+System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
}
}
}


Al ejecutar el programa vemos como va consumiendo mas y mas memoria aun cuado en cada iteración estamos forzando la recolección de basura y la instancia que estamos creando no tiene aparentemente ninguna referencia. Aparentemente.

En realidad la referencia si existe porque al "enganchar" un evento a la clase FileSystemWatcher es como si estuviéramos enganchando un evento a un objeto del sistema operativo y mientras tal exista la referencia continuará ahi, no importa que no tengamos en el programa una referencia directa aparente al objeto.

Ahora si quitamos el comentario a la linea que dice
// oWatcher.UnSubscribe();

cada llamada al recolector de basura libera la memoria debido a que la llamada a UnSubscribe "desconecta" nuestro objeto del FileSystemWatcher, sin embargo es bueno notar que dos cosas se hacen aqui: una desconectar el evento, la otra liberar el FileSystemWatcher, por que ? El FileSystemWatcher como había dicho representa un objeto del sistema operativo, de hecho en su implementación interna debe tener un handle a un objeto del sistema operativo y no va a ser liberado hasta que se llame a Dispose, podemos hacer la prueba comentando esa linea y ver los resultados.

Como podemos ver aunque .NET maneja la memoria por nosotros, somos responsables cuando desde nuestro programa colocamos referencias a elementos externos, como en este caso objetos del sistema operativo.

No hay comentarios: