Manejando Memory Leaks in .Net

Un tema muy interesante que los programadores de .Net debemos tener en cuenta son los Memory Leaks. Nosotros, como programadores .Net, sabemos que existe el recolector de basura. Esto quiere decir que los Memory Leaks o fugas de memoria, se producen todo el tiempo y se dejan en manos del recolector de basura encargarse de liberar los objetos de la memoria. No significa que el recolector tiene errores, simplemente, nuestro código puede generarlos perdiendo la ayuda del recolector.

Introducción

Memory Leaks, fugas de memoria en español, muchas veces son ignoradas por un largo tiempo. Esto hace que a medida que pasa el tiempo nuestra aplicación comience a funcionar cada vez más lentamente, se incremente el consumo de memoria, hacemos que trabaje más el recolector de basura trabaje más y haciéndolo menos eficiente. Imaginemos que a medida que pasa el tiempo, nuestra aplicación consume más y más memoria, sin cambiar la cantidad de usuarios, llegará un momento donde se bloqueara por falta de recursos.

Entonces, ¿Como puedo tener fugas de memoria si el recolector de basura se encarga de eso? Es una pregunta simple de responder:

Básicamente hay 2 causas principales que no dejan al recolector de basura hacer correctamente su trabajo. 

  • Un Objeto el cual se hace referencia a otro en el momento de ser recolectado, y que en realidad no está siendo utilizado. El recolector detecta que aún tiene una referencia hacia él y no lo elimina.
  • Cuando asignamos a memoria no administrada (Esto quiere decir que no actúa el recolector de basura) y no liberamos correctamente la memoria. Los escenarios más comunes son: Transmisión de datos, acceso a sistemas de archivos o llamadas de red, por ejemplo.

Veamos las maneras más comunes de generar Memory Leaks.

Eventos

Los eventos de .Net son los más conocidos de causar pérdidas de memoria. La razón es que una vez que nos suscribimos a un evento, ese objeto tiene una referencia a la clase.

public class WatchFile
{
    public WatchFile(FileManeger fileManeger)
    {
        fileManeger.FileChanged += OnChanged;
    }
 
    private void OnChanged(object sender, WifiEventArgs e)
    {
        //...
    }
}

Si miramos en detalle, FileManager hace referencia a una instancia WatchFile. Cuando el recolector de basura haga su trabajo, no podrá eliminarlo porque aún tiene una referencia. No entraremos en detalle, pero existen varios patrones para evitar este problema:

  • Desuscribirse de los eventos manualmente.
  • Usar Weak-handler patterns.
  • Usar funciones anónimas que no capturen miembros de una clase.

Dar de baja controladores de eventos

Si bien, un método handler significa que se hará referencia a un objeto, supongamos que un evento solo debe ejecutarse una vez. Debemos poder dar de baja el código la suscripción al evento:

public class WatchFile
{
    private readonly FileManager _fileManager;
 
    public WatchFile(FileManager fileManager)
    {
        _fileManager = fileManager;
        _fileManager.Changed += OnChanged;
    }
 
    private void OnChanged(object sender, WifiEventArgs e)
    {
        //...
        _fileManager.Changed -= OnChanged;
    }
}

Otra manera de eliminar el handler del evento es por medio de una expresión lambda. Veamos:

public class WatchFile
{
    public WatchFile(FileManager fileManager)
    {
        var someObject = GetSomeObject();
        EventHandler<FileManagerArgs> handler = null;
        handler = (sender, args) =>
        {
            Console.WriteLine(someObject);
            fileManager.Changed -= handler;
        };
        fileManager.Changed += handler;
    }
}

Lo mejor de este último código es que es simple, legible, sin probabilidades de una fuga de memoria y el evento se dispara al menos una vez. Pero, solamente puede ser utilizado cuando el evento solo se debe disparado una vez.

Eventos débiles con agregados

Cuando hacemos referencia a un objeto, básicamente, le decimos al Garbage Collector que este está en uso y no será recolectado. ¿Hay alguna manera de hacerle referencia sin decirle que los estamos usando? si, a esto se lo llama referencia débil. Esto quiere decir que el Garbage Collector lo liberara de la memoria cuando lo tome. Para poder hacerlo usamos WeakReference de .Net.

El camino más común para eliminar este problema es usar el patrón EventAggregator. Básicamente, cualquiera puede suscribirse a eventos del tipo T, y cualquiera puede publicar eventos del tipo T. Esto quiere decir que cuando es publicado un evento, se invocaran todos los handlers que están suscritos. Por lo tanto, aunque un objeto esté suscrito a un evento, este podrá ser recolectado.

Veamos un ejemplo con Prism, el más popular de los nuggets para EventAgreggator:

public class FileManager
{
    private readonly IEventAggregator _eventAggregator;
 
    public FileManager(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;
    }
 
    public void PublishEvent()
    {
        _eventAggregator.GetEvent<FileEvent>().Publish(new FileEventArgs());
    }
}
public class WatchFile
{
    public MyClass(IEventAggregator eventAggregator)
    {
        eventAggregator.GetEvent<Filevent>().Subscribe(OnChanged);
 
    }
 
    private void OnChanged(FileEventArgs args)
    {
        //...
    }
}
public class FileEvent : PubSubEvent<FileEventArgs>
{
    // ...
}

Con este ejemplo podemos prevenir las fugas de memoria de una forma relativamente fácil. Debemos tener en cuenta que tendremos un contenedor global. Cualquiera puede suscribirse, esto hace que el sistema sea difícil de entender cuando tengamos mucha cantidad de eventos.

Utilizar handlers con eventos débiles con eventos regulares.

Veamos un ejemplo:

public class WatchFile
{
    public WatchFile(FileManager fileManager)
    {
        fileManager.Changed += new WeakEventHandler<FileEventArgs>(OnChanged).Handler;
    }
 
    private void OnChanged(object sender, FileEventArgs e)
    {
        //...
    }
}
public class FileManager
{
    public event EventHandler<FileEventArgs> FileChanged;
    // ...
}
public void SomeOperation(FileManager fileManager)
{
    var watchFile = new WatchFile(fileManager);
    watchFile.DoSomething();
    
    //... watchFile is not used again
}

El objeto FileManager se mantiene como un evento estándar de C#. Al utilizar eventos estándar todo es más sencillo, no hay fugas de memoria y no debemos preocuparnos de las separaciones de responsabilidades como en ejemplo anterior. Lo que debemos tener en cuenta que estamos creando un objeto que nunca será recolectado.

El uso de WeakReference hace que el Garbage Collector pueda recopilar la clase suscripta cuando sea posible. Sin embargo, el Garbage Collector no limpia objetos sin referencia de inmediato. Lo hará al azar. Entonces, con eventos débiles, es posible que se invoquen controladores de eventos en objetos que no deberían existir en ese punto.

El controlador de eventos podría hacer una actualización de un estado interno. O podría cambiar el estado del programa hasta que el Garbage Collector decida recolectar en algún momento aleatorio. Este tipo de comportamiento es realmente peligroso. Hay que manejarlo con mucho cuidado.

Uso de variables estáticas

Para algunos desarrolladores, usar variables estáticas, es una mala práctica. No estoy del todo de acuerdo con este punto, pero si es cierto que están asociadas a fugas de memoria de memoria.

El garbage collector funciona de la siguiente manera. Este revisa todos los objetos de la raíz,  Garbage Collector Root, los marca como recolectable o no. El siguiente paso es ir a todos los objetos que hacen referencia y los marca como no recolectables. Esto lo hace hasta finalizar.

¿Qué es la raíz del garbage collector? Todo lo que esté vivo en el hilo de ejecución, o variables estáticas u objetos administrado, como por ejemplo objetos COM. Esto quiere decir que las variables estáticas no serán nunca recolectadas o eliminadas de la memoria.

public class WatchFile
{
    static List<WatchFile> _instances = new List<WatchFile>();
    public WatchFile()
    {
        _instances.Add(this);
    }
}

En el código anterior es un ejemplo de una clase que nunca, nunca, pero nunca será eliminada causando una fuga de memoria.

Uso de Cache

La mayoría del tiempo nos gusta guardar objetos en caché. Con esto nos evitamos hacer operaciones repetitivas consumiendo recursos innecesarios. Por ejemplo, llamamos a un método una vez y guardamos los resultados en caché. El gran problema es que lo guardamos sin un tiempo definido.

public class Customers
{
    private IList<int, Orders> CustomerOrderCache { get; set; } 
 
    public IList<Orders> GetOrderByCustomerID(int customerId)
    {
        if (!CustomerOrderCache.ContainsKey(customerId))
        {
            var orders = GetOrderFromDatabase(customerId);
            CustomerOrderCache[customerId] = orders;
        }
        return CustomerOrderCache[customerId];
    }
 
    private byte[] GetOrderFromDatabase(int customerId)
    {
        // ...
    }
}

En este fragmento de código, podemos ver que nos ahorraremos algunos viajes a la base de datos a coste de guardar resultados en memoria. Para no tener problema de fuga siempre debemos pensar en:

  • Borrar el caché cuando no se usa por un tiempo.
  • Limitar el tamaño del uso de memoria del caché.
  • Los objetos que tengamos en caché debemos usar WeakReference, como vimos anteriormente.

Hilos sin fin

Hablamos Garbage Collector Root y lo que se denomina Live Stack o que se toma como vivo. Este incluye todas las variables locales y los miembros que llaman a los subprocesos en ejecución.

Si creamos un subproceso que tenga una ejecución infinita que no hace nada, pero mantiene referencia a objetos nos dará como resultado una fuga de memoria.

public class Clock
{
    public Clock()
    {
        Timer timer = new Timer(HandleTick);
        timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
    }
 
    private void HandleTick(object state)
    {
        // do something
    }
//...

Agregar Dispose y nunca invocarlo

Una buena práctica es agregar el método Dispose para liberar algunos recursos no administrados. ¿Pero qué pasa si no lo invocamos? tenemos una fuga de memoria.

La mejor práctica para evitar esta fuga es usar la instrucción using que nos brinda c#:

using (var customer = new Customer())
{
    // ... 
}

Esto hará que el Dispose sea llamado automáticamente. Incluso si la ejecución lanza una excepción, el Dispose será invocado igualmente.

Conclusión

Es importante reconocer estos momentos los cuales podemos ocasionar fugas de memoria mientras estamos construyendo nuestra aplicación. También, es importante reconocerlos en aplicaciones existentes. Para ello tenemos varias herramientas que veremos en próximos posts.

0 0 votos
Valora la Publicación
Suscribirse
Notificación de
guest
0 Comentarios
Feedback en línea
Ver todos los Comentarios

Comentarios Recientes

0
Nos encantaría conocer tu opinión: ¡comenta!x
Ir a la barra de herramientas