Cosas que deberías saber sobre los destructores en .NET


Este post es la continuación de Cosas que deberías saber sobre el Garbage Collector de .NET


 


¿Para qué necesitamos destructores en .NET? Como vimos en el anterior post, en .NET no tenemos que preocuparnos de destruir ni de liberar el espacio de los objetos dado que el GC se ocupa de esta tarea. No obstante, con mucha frecuencia nuestras aplicaciones mantienen referencias a objetos nativos (es decir, no .NET), como por ejemplo handles a ficheros, conexiones a bases de datos, etc. El GC sólo entiende de objetos .NET, y por lo tanto queda en manos del desarrollador asegurarse de que su código limpia y destruye adecuadamente los recursos nativos de los que hace uso.


 


Para asegurarnos que nuestros objetos .NET limpian toda la memoria no manejada cuando su vida finalice, tenemos que hacer dos cosas. La primera es que nuestra clase implemente la interfaz IDisposable y por tanto el método Dispose(). Este método debe ocuparse de realizar las tareas de limpieza de recursos no manejados, y finalmente llamar al método SuppressFinalize, enseguida veremos porqué.


 


GC.SuppressFinalize(this);


 


Para cierto tipo de objetos, el método Dispose() se le llama con otro nombre por motivos puramente semánticos. Por ejemplo, la clase SqlConnection tiene un método Close() y un método Dispose() que son funcionalmente equivalentes. En el caso de conexiones a base de datos, para la mayoría de los desarrolladores resulta más intuitivo llamara a Close() a la hora de cerrar la conexión y liberar los recursos asociados.


 


En C# (si no me equivoco VB no tiene nada equivalente) existe la instrucción using que nos permite establecer un contexto en el que se va a utilizar un objeto .NET. Al salir de dicho contexto (cuando llegamos a la llave de cierre ‘}’) se llamará automáticamente al método Dispose() del objeto en cuestión. En el siguiente ejemplo, al salir del contexto llamaríamos al método Dispose() del objeto fs:


 


string path = @”c:\temp\MyTest.txt”;


using (FileStream fs = File.Create(path))


{


    byte[] info = new UTF8Encoding(true).GetBytes(“This is some text”);


    fs.Write(info, 0, info.Length);


}


 


El problema de la interfaz IDisposable es que dependemos de que los desarrolladores que consuman nuestras clases se acuerden de llamar a los métodos Dispose() o Close(), porque de lo contrario todo el esfuerzo habrá sido inútil. En previsión de que a los desarrolladores a veces se les pueda olvidar deshacerse correctamente de sus objetos, existen los destructores (en C#) o método Finalize() (en VB). En adelante me referiré a ambos cómo método Finalize(), pero son equivalentes y simplemente tienen nombres distintos por motivos lingüísticos.


 


Por lo tanto, la segunda de las cosas que debemos hacer es precisamente implementar un método Finalize(). El método Finalize() es un mecanismo que nos proporciona .NET para ejecutar código de finalización de un objeto de forma automática, antes de que el GC libere su memoria.


 


Uno de los principios básicos de .NET es que el código del método Finalize() de una clase debe ser muy rápido, nunca puede lanzar una excepción ni tampoco bloquear la ejecución. Los métodos Finalize() de todos los objetos de un proceso se ejecutan en un mismo thread conocido como Finalizer. Cuando el GC determina que los objetos están listos para ser finalizados los mete en una cola y dicho thread ejecuta los métodos Finalize() de cada objeto secuencialmente. Hasta que no termina la ejecución del método Finalize() de un objeto, no comienza con el siguiente. Por lo tanto si se produce un bloqueo durante la finalización de alguno de los objetos, todo lo que venga detrás nunca será finalizado con todas las consecuencias que ello conlleva. Adicionalmente, en ASP.NET, este thread es el único que no tiene un manejador de excepciones predeterminado, por lo que cualquier excepción en este thread provocaría la finalización inmediata del proceso (también conocido como crash).


 


Para maximizar el rendimiento de nuestras aplicaciones .NET, debemos procurar que la menor cantidad posible de objetos sean finalizados por el GC. En el mejor de los casos, el método Finalize() no sería llamado nunca, sino que todos los objetos serían finalizados mediante la interfaz IDisposable. Para que os hagáis una idea de la penalización para el rendimiento que supone que un objeto sea finalizado por el GC, observad las diferencias en la tabla a continuación:

































 


Finalización mediante Dispose()


Finalización mediante Finalize()


Vida útil del objeto



Al crearse un nuevo objeto .NET, el CLR comprueba si este tiene un método Finalize(). Si es así, añade una referencia a dicho objeto en una estructura manejada por el GC llamada la cola de Finalización. Esta cola contiene todos los objetos que hay que finalizar, antes de que el GC libere su memoria.


 



Cuando finaliza la vida útil del objeto, desde el código de la aplicación se llama su método Dispose(). En este método se realizan
las tareas de limpieza de recursos no manejados, y finalmente se llama al método SuppressFinalize lo cuál elimina el objeto de la cola de Finalización.


Cuando finaliza la vida útil del objeto, desde el código de la aplicación no se llama su método Dispose().


Primera colección del GC



Cuando se desencadena una colección de basura, el GC recorre la memoria y determina qué objetos ya no están en uso y pueden ser colectados. Adicionalmente comprueba si estos objetos están referenciados en la cola de Finalización.


Si el objeto no necesita ser finalizado, su memoria es liberada.



Los objetos que se encuentran en la cola de Finalización son eliminados de esta y movidos a una nueva cola llamada Freachable. La cola Freachable contiene los objetos que están listos para que su método Finalize() sea llamado. Es decir, son basura pero todavía no han sido finalizados. Esta cola actúa como un objeto raíz e impide que los objetos a los que referencia sean colectados.



El objeto sobrevive la colección de basura y promociona a la siguiente generación (a no ser que ya estuviera en la generación 2).


Tiempo transcurrido entre colecciones


 



El thread del Finalizer comienza a llamar a los métodos Finalize() de todos los objetos que se encuentran en la cola Freachable. Una vez ha finalizado la llamada al método Finalize(), los objetos son eliminados de esta cola. Cuando la cola Freachable se vacía, el thread del Finalizer pasa en un estado de espera.


Segunda colección del GC


 


Cuando se desencadena una colección de basura, el GC recorre la memoria y determina qué objetos ya no están en uso y pueden ser colectados. Adicionalmente comprueba si estos objetos están referenciados en la cola de Finalización.


 


 


Dado que el objeto ya no se encuentra referenciado ni por la cola de Finalización ni por la cola Freachable, su memoria es liberada.


 


En definitiva, necesitamos implementar un destructor para nuestras clases, pero debemos procurar que dicho destructor no sea utilizado nunca. Espero que os haya convencido.


 


– Hasta el próximo post,


Daniel Mossberg

Comments (1)

  1. SrX says:

    Un código de ejemplo no estaria nada mal 😉