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 [patronDispose], que incluye la interfaz [IDisposable]. Esto debe hacerlo sí o sí.
Programadores expertos podrán argumentar que no es necesario implementar el [patronDispose] 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 [patronDispose] 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 [OOM] 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 [OOM].
¿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 [patronDispose] 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 [recmalas] 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/nullEl 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.