Liberación de memoria en código manejado (¿Dispose, Finalize, Object = Nothing, GC.Collect?)

Published Wed, Mar 28 2007 5:54

Para quienes venimos del desarrollo utilizando Visual Basic 6.0, una de las primeras cosas que nos enseñan al empezar a utilizar código manejado (framework), es que ya no es necesario liberar la memoria porque “.net lo hace por ti”. Esta última parte entre comillas, además de ser incorrecta en su definición, es muy engañosa/confusa para quién es nuevo utilizando el framework.

¿Por qué está incorrecta en su definición?

De partida, el decir que .net lo hace es tan amplio que pierde el enfoque y no queda claro quiénes son los actores involucrados.

El actor principal es el garbage collector (GC). Éste está encargado de reservar la memoria desde el sistema operativo (grandes pedazos de memoria) y administrar los requerimientos de memoria de nuestra aplicación (pequeños pedazos de memoria). Esta administración comprende las tareas de asignar la memoria que es requerida por nuestra aplicación, por ejemplo las variables, para posteriormente reclamarla una vez que se ha dejado de utilizar. Para más información, ver el siguiente post sobre el manejo de memoria.

¿Por qué es engañosa/confusa para desarrolladores novatos en código manejado?

Si el problema se mira desde 10.000 metros de altura y si todos los componentes que se usan están correctamente programados, podemos decir que el GC si libera la memoria por ti.

Veamos las sutilezas que hacen que lo anterior pueda no cumplirse.

El GC es no-determinístico. ¿Qué significa esto? Significa que éste limpia la memoria cuando ÉL estima que es necesario y no cuanto TÚ quieres que lo haga; su ejecución no está determinada por ti, ni se ejecuta siguiendo un patrón detectable desde código. Sí lo hace siguiendo un algoritmo de optimización/adiestramiento interno, pero no es fácilmente detectable por uno como desarrollador. Además, al ser un algoritmo que se va adiestrando con el tiempo, su frecuencia de ejecución no es siempre repetible o predecible.

Entonces, si nos vamos a 10.000 metros de altura y para un tiempo T >> 0, podemos decir que la memoria será reclamada (liberada) por el GC, un poco más tarde de lo que se haría en VB 6.0, pero será reclamada.

Por otro lado, el GC no libera la memoria conocida como no manejada, es decir, la memora que él no administró. ¿Quién libera esta memoria? La respuesta tiene matices, primero el desarrollador, pero si éste no lo hace, alguien debe hacerlo.

Cuando se programan componentes que manejan recursos no manejados, el programador DEBE implementar el patrón Dispose, que incluye la interfaz IDisposable. Esto debe hacerlo sí o sí.

Programadores expertos podrán argumentar que no es necesario implementar el patrón Dispose o que se puede implementar a medias (ver sección de los problemas más abajo referente al mito del finalizador). Esto es cierto, pero dependerá del control que ellos tengan sobre el uso de los tipos (clases) generados por ellos. Desde el momento en que ellos no controlen quién usará sus tipos, será entonces obligación hacerlo ya que es un estándar esperable.

Yo, como desarrollador, debiera ser el responsable de liberar la memoria no manejada llamando al método Dispose una vez que he dejado de utilizar el objeto de ese tipo. Esto, a diferencia del funcionamiento del GC, liberará inmediatamente la memoria no manejada utilizada por el tipo.

¿Qué sucede si el desarrollador no llama a Dispose?

Entonces dependerá de varios factores el que afecte o no a mi aplicación. Veamos algunos de ellos.

Si quién desarrolló el tipo que estoy utilizando implementó el patrón Dispose de forma correcta, los recursos manejados serán liberados cuando se ejecute el finalizador. Éste, de la misma forma que el GC, es no-determinístico.

Al no ser determinístico, la liberación de los recursos no manejados podrá hacerse tan tarde que podríamos encontrarnos con excepciones por falta de memoria (Out Of Memory) o quedarnos sin conexiones a SQL Server (esto yo lo he visto en algunos casos donde he trabajado).

Tristemente, si el código no implementó Finalize porque el desarrollador novato no supo que había que hacerlo o porque el experto confió en que quién lo iba a usar llamaría a Dispose, definitivamente nos encontraremos con excepciones por falta de memoria (Out Of Memory).

¿Dónde empiezan los problemas?

Uno de ellos se da porque rara vez un programador implementa el finalizador. Esto se debe a la existencia de un mito/costo asociado a éste. Es totalmente cierto que tener un tipo que implementa el finalizador tiene un sobrecosto en rendimiento comparado a un tipo que no lo implementa. Ahora, es muy cierto también que si se implementa el patrón Dispose de forma correcta, existe una línea de código en la función Dispose que quita parte de ese sobrecosto. Veamos el código de ésta:

public void Dispose()

{

    Dispose(true);

    GC.SuppressFinalize(this);

}

 

Sin entrar en demasiados detalles, el sobrecosto mencionado de tener un tipo que implementa Finalize se debe a que cada vez que se crea una instancia de éste tipo, este nuevo objeto se agrega a una cola especial de procesamiento. Bueno, esta función SuppressFinalize saca la instancia de la cola. Entonces, al final, el único sobrecosto está en agregar y sacar una instancia de la cola. No se cuantificar este costo, pero puedo asegurar que es mucho menor a los problemas producto de memoria no liberada. Se debe entender que una vez que se ha hecho Dispose de los recursos no manejados, ya no hay necesidad de llamar a Finalize porque no hay nada que finalizar.

Por eso es importante que el desarrollador llame al método Dispose, ya que además de garantizar la correcta liberación de recursos no manejados, también se produce esta optimización. Ese es otro de los problemas. El desarrollador no sabe que tiene que llamar a Dispose porque le dijeron que .net libera la memoria automáticamente.

El último de los problemas se da por que el desarrollador escucha o lee recomendaciones malas o poco precisas y decide usar el mismo código que estaba tan bien justificado (es en sentido irónico) en este otro sitio. Esto se refiere al uso de GC.Collect.

¿Y si llamo a GC.Collect?

Llamar a GC.Collect no tiene ningún efecto positivo; más aún, los efectos negativos producto de la ejecución de ésta podrían impactar severamente el rendimiento y consumo de recursos. Esto tiene que quedar claro ¡ya!

Jamás debe llamarse a GC.Collect. Existen excepciones contadas con menos dedos que los que tiene una mano, en las cuales el hacerlo podría (potencialmente) tener beneficios, pero esas no están dentro de las labores de desarrollo tradicional. Para el trabajo día a día, GC.Collect no está dentro de las funciones a utilizar.

Si no puedo recolectar la memoria, igualo los objetos a nothing/null

El igualar los objetos a nulo tiene, en la práctica, un impacto mínimo en la liberación de memoria. MSDN define esto como:

In Visual Basic .NET, setting an object to Nothing marks the object for garbage collection, but the object is not immediately destroyed.

 

Es correcto. Si se iguala un objeto a nulo, lo único que se está logrando es marcándolo para que sea liberado, la próxima vez que se ejecute el GC, algo que ya sabemos que ocurre de forma no determinística y en una frecuencia no conocida por los desarrolladores. Entonces, ¿qué gano haciéndolo?

Existen, al igual que el llamado a GC.Collect, contadas ocasiones donde asignar nulo a variables puede ayudar, pero todo dependerá de las probabilidades.

Supongamos que estamos en una función donde se han creado objetos que consumen mucha memoria (datasets, colecciones, hashtables, etc.), y que en alguna de las líneas que vienen más abajo, se hará una llamada a una función que demorará bastante (Web Service, SQL Server, etc.). Si llegase a ocurrir una recolección de memoria mientras se ejecuta esta función larga, todos estos objetos grandes envejecerán (pasarán de generación en el GC) y no serán liberados ya que aún se están “usando”.

Ahí es donde se puede hacer la diferencia. Si yo sé que no se usarán más adelante en la función, entonces puedo igualarlos a nulo y en la eventualidad que se produzca una recolección, estos serán efectivamente recolectados por el GC (no envejecerán) y la memoria será liberada.

¿Qué otras opciones existen donde valga la pena hacerlo?

Yo al menos no conozco ninguna otra, lo que por cierto no significa que no haya.

¿Y si igualo a nulo y llamo a GC.Collect?

Nuevamente, no se debe llamar a GC.Collect salvo contadísimas excepciones, las que no hemos discutido aquí. Si tú has vivido una situación donde hayas podido demostrar fehacientemente que el llamado a GC.Collect produjo un resultado positivo, te ruego compartirla.

Ahora, si tienes otro punto de vista o situación vivida que difiera de lo que acabamos de ver, también te ruego compartirla.

Patrick.

by pmackay
Filed under: ,

Comments

# la visión de un ingeniero de campo said on Sunday, April 29, 2007 8:16 PM

Hablaremos de cómo se auto adiestra el GC para realizar recolecciones de memoria en una frecuencia medianamente determinada, con el fin de impactar lo menos posible el rendimiento de la aplicación.

# Leandro said on Tuesday, May 22, 2007 3:31 PM

Felicitaciones! Excelente artículo, me abrió los ojos.

# pmackay said on Tuesday, May 22, 2007 8:30 PM

Siempre un placer.

Patrick.

# Javi said on Tuesday, June 19, 2007 11:10 AM

Muy bueno.

Con artículo como este se hace más fácil el paso de Visual Basic 6 a 2005

Gracias

# pmackay said on Sunday, June 24, 2007 2:57 PM

Muchas gracias. Siempre es un gusto aportar...

# Sergio said on Tuesday, July 17, 2007 5:51 PM

Buen artículo,pero...sigo teniendo una duda.

Como programador, debería de usar el dispose de los objetos que creo.

try

{

 SqlConnection oConn = ...

{

catch...

finally

{

 oConn.Close();

 oConn.Dispose();

}

Saludos. Sergio.

# pmackay said on Tuesday, July 17, 2007 7:57 PM

Sergio,

cuando dices "usar" y "objetos que creo" ¿te refieres a implementar Dispose en tipos definidos por ti o en llamar a dispose de instancias de tipos creadas?

Para los primeros, si vas a manejar recursos no manejados, si debes implementar IDisposable, aunque los recursos no manejados los maneje algun tipo que tu estes manejando. Ejemplo: Si tengo una clase que tiene una propiedad del tipo de conexión a base de datos, tu clase debe implementar Dispose y éste debe llamar al Dispose de la conexión.

Para el segundo caso, que es parecido al ejemplo que posteaste ahí, tienes que garantizar que se llame Dispose, ya sea usando un using o try/finally.

Con esto basta. Dispose llama a close internamente.

try

{

SqlConnection oConn = ...

{

catch...

finally

{

oConn.Dispose();

}

Saludos,

Patrick

# Max said on Wednesday, January 16, 2008 8:24 AM

en el ultimo comentario respecto al codigo de dispose, indicas que al llamar a Dispose internamente tambien se llama a close (esto refiriendome a un código que se conecta a una BD).

Entonces que diferencias hay al usar close() y dispose()? o cuando usar uno de ellos o en que casos?

atte.

# pmackay said on Wednesday, January 16, 2008 1:21 PM

Max,

Excelente pregunta. Dispose en algunos casos implementados hace algo similar a close, sin embargo, no debes basarte solo en eso. Te cuento.

En la teoría (no refiriéndome a objetos de conexión a BD sino que a reglas generales), Close permite liberar algunos recursos, pero deja el objeto en un estado sobre el cual se pueden seguir ejecutando acciones como Abrir nuevamente la conexión.

Siguiendo la teoría, si se utiliza Dispose, se entiende que el objeto no será utilizado nuevamente, y cualquier llamada a algún método de este, como tratar de abrirlo nuevamente, no debiera tener éxito y una excepción debiera lanzarse.

Si te animas, en este caso hay un ejemplo de una mala implementación de Dispose en Sharepoint que lleva a problemas en Webparts de usuario. Te aviso que el post está muy denso y enredado.

msmvps.com/.../sharepoint1.aspx

Saludos,

Patrick

# Fer said on Tuesday, March 25, 2008 1:01 PM

Gracias, muy útil y bien explicado, mi servidor te lo agradece ;)

Saludos.

# pmackay said on Tuesday, March 25, 2008 2:09 PM

Muchas gracias....

¿estaban con problemas de mucho uso de memoria?

Saludos,

Patrick.

# Diego Armando gomez said on Wednesday, April 16, 2008 9:28 AM

Como estan.

Super adecuada la explicacion. Sobre todo que la ejecucion del GC se da en algun momento del tiempo no determinado, y que este libera las porciones de memoria que no estan referenciadas por ningun objeto de algun contexto. ( lo que se hace al hacer un objeto igual a nothing.)

Pero tengo una pregunta:

Para las entidades de negocio que transportan valores entre capas (un Entity), se justifica el desgaste para el programador de ponerlas a implemetar IDisposable?

Quedo atento a sus comentarios, Gracias.

# Mike Villegas said on Wednesday, April 16, 2008 12:25 PM

Tengo una aplicacion en silvelight en el cual creo diferentes controles, traté de liberar memoria eliminando los objetos, no se veia la memoria lierada en el IE7, hasta que usé el GC.Collect().

# pmackay said on Wednesday, April 16, 2008 5:49 PM

Diego,

si las entidades tienen solo tipos básicos, entendiendose como básico tipos como strings, ints, booleans, etc., no es necesario y sería una perdida de tiempo. Preguntate lo siguiente. Si la entidad implementara Dispose, ¿qué liberarías cuando alguien llame a dispose?

Por otra parte, si tu entidad tiene algun campo que sea un dataset o un stream, debieras implementar IDisposable, y en el método dispose, llamar al dispose del dataset y al close del stream.

Saludos,

Patrick

# pmackay said on Wednesday, April 16, 2008 5:51 PM

Mike,

eso es esperado. Revisa este post y conversemos.

msmvps.com/.../post0b.aspx

Saludos,

Patrick

# Ruben said on Thursday, September 04, 2008 5:23 AM

Buenas,

Me ha encantado el artículo, una vez estuve buscando información sobre este mismo tema y desgraciadamente no te encontré, hoy me lo han pasado y me ha abierto el apetito; ¿Podrías pasarme algún enlace con mas información sobre este tema? Yo buscaré también por mi cuenta pero quizás tengas algo que valga la pena por ahí... ;)

Un saludo y muchas gracias.

# pmackay said on Saturday, September 06, 2008 9:45 AM

Ruben,

estos links tienen una explicación de cómo funciona el garbage collector y otro sobre cómo implementar dispose correctamente.

msdn.microsoft.com/.../bb985010(en-us).aspx

msdn.microsoft.com/.../bb985011(en-us).aspx

www.bluebytesoftware.com/.../PermaLink.aspx

Te comento que este último está para leerlo bien despierto. No lo dejes como lectura de noche..... aunque realmente los otros dos tampoco....

Saludos,

Patrick

# Iván said on Saturday, January 03, 2009 1:10 PM

Muchas gracias por el articulo. Aunque llevo tiempo programando a nivel profesional en C++ y Java, aún soy novato con .net y he caído en el error de leer donde no debía.

Tenía un problema de memoria con un programa que estoy realizando y realmente usar GC.Collect no ha resultado ser una buena opción, mi proceso comenzaba a consumir memoria, poco a poco, hasta colapsar el sistema. Uno de los errores que comentí parece ser que no liberé bien los recursoso de un socket y por otro lado el trabajo con XML de forma integrá no resultó ser tampoco muy recomendable así que lo he sustituido por listas de XmlElemnts que luego he limpiado a mando como tu recomiendas.

Tras esto, eliminé el uso del GC.Collect y de momento se ha estabilizado en unos 14,76 Mb. Antes sobrepasaba los 200 Mb antes de caer.

Lo dicho muchas gracias y habrá que tener cuidado con lo que dejamos en mandos del GC.

# pmackay said on Saturday, January 03, 2009 7:42 PM

excelentes palabras. Lo mejor que se puede hacer con el garbage collector es dejarlo trabajar tranquilo.... y preocuparse de liberar los recursos no administrados.....

Saludos y gracias por tus palabras

# Forzar el borrado de un objeto de memoria | hilpers said on Sunday, January 18, 2009 5:40 AM

Pingback from  Forzar el borrado de un objeto de memoria | hilpers

# rudrom said on Thursday, February 19, 2009 12:54 PM

Genial, desde su publicación hace dos años, este artículo continua estando vigente al másimo.

Gracias a él, un novato como yo, he podido concretar por qué se partía mi programa con Native error 0xc0000005.

Es una función que graba los datos de un form en la .sdf, tanto el form lo cerraba con close() y llamaba a otra instancia limpia, la conexión la cerraba tras grabar los datos también con close() tras unos minutos de uso partía sin razón aparente.

GRACIAS EN MAYÚSCULAS

# Felipe said on Monday, April 06, 2009 2:32 PM

pmackay:

Primero darte las gracias por esto, ya q no toda la gente se da el tiempo para explicar con tanta buena voluntad.

te cuento... tengo una aplicacion en .net con c# y la parte grafica esta implementada en flash, estamos ademas cargando .mht en webbrowser, el problema mas grande es que llamando al metodo dispose y asignando null a los objetos, solo consigo bajarle al uso de memoria, pero el uso de la memoria virtual sube, sube y no para de subir hasta que se cae el programa. e usado detodo, hasta el GC.collect() y solo consigo bajarle lo comentado... la memoria virtual se me escapa...

espero una ayudita de tu parte...

de antemano muchas gracias.

Felipe B.

# pmackay said on Monday, April 06, 2009 9:56 PM

Felipe,

si estás utilizando componentes no manejados (com), después de usarlos, debes liberar el RCW utilizando Marshal.ReleaseComObject(objeto)

dotnetdebug.net/.../marshalreleasecomobject-and-cpu-spinning

Otra cosa que se me ocurre es preguntarte si estan usando consutlas con ado.net, con cursores del lado del cliente?

Saludos

# Felipe said on Tuesday, April 07, 2009 8:48 AM

MIRA AUN NO UTILIZO Marshal.ReleaseComObject(objeto) PARA HACER PRUEBAS PERO RESPECTO A TU PREGUNTA TE PUEDO COMENTAR QUE EL PROYECTO ES LOCAL (PC AUTOSERVICIO PARA IMPRIMIR INFORMACION QUE ESTA DENTRO DEL PROYECTO EN FORMATO .MHT), Y AL REALIZARLO SIN IMPLEMENTAR FLASH, NO EXISTE NINGUN PROBLEMA, EL PROBLEMA ES FLASH.

SALUDOS

# Felipe said on Tuesday, April 07, 2009 12:12 PM

hola.

e estado todos estos dias haciendo pruebas y mi conclusion es q el objeto axShockwaveFlash de flash cada vez que lo llamo de una pagina a otra se aloja en la memoria virtual y el uso de memoria casi a la par y utilizando el close, dispose gc.collect() no baja nada... estoy tratando de utilizar Marshal.ReleaseComObject(objeto) y nose si lo estoy utilizando mal o que, pero no logro bajarle a la virtual y al parecer no estoy eliminado el objeto.

me tiene preocupado este tema.

espero puedas ayudarme y gracias otra vez.

saludos

# pmackay said on Wednesday, April 08, 2009 9:03 AM

mmmm, en estos casos, te recomiendo bajar la ultima versión del componente que estas usando.....

si el problema persiste, ver en los foros de Adobe.... con un poco de suerte, podría estar ahí la respuesta (o al menos el consuelo de que no eres el único)...

Finalmente, si te animas a tomar un dump de la memoria del proceso, puedo revisarlo y ver si hay algo por ahí......

saludos...

# Felipe said on Thursday, April 09, 2009 8:34 AM

Solo queria contarte que consegui solucionarlo... el metodo para solucionarlo es el siguiente,el problema es q el flash genera los controles de los botones cada vez que se crea un form... la forma era no crear el new objeto flash, sino que pasar los controles por parametros... eso.

saludos y gracias.

# pmackay said on Thursday, April 09, 2009 5:21 PM

gracias por la información....

Patrick

Leave a Comment

(required) 
(required) 
(optional)
(required)