Cómo cazar una fuga de memoria en .Net (I)

Hablaba en un post anterior de como me las he tenido que ver con una fuga de memoria en .Net recientemente, e incluso ponía un pequeño caso mínimo representativo del problema que he tenido. Hoy toca hablar de como llegue a la conclusión de que estaba ante una fuga de memoria y a la conclusión de que se trataba de una fuga de memoria manejada.

Para diagnosticar una fuga de memoria en .Net debemos comenzar por responder dos preguntas, que a continuación veremos. Si diré que la herramienta para contestar ambas preguntas es el monitor de rendimiento.

La primera cuestión que tendremos que dilucidar es: ¿Estamos ante una fuga de memoria o ante un consumo excesivo de memoria?

No es extraño encontrar aplicaciones de .Net que por un mal diseño hacen un uso excesivo de memoria. Una situación habitual es por ejemplo, convertir la sesión de las aplicaciones Asp.net en un cajón de sastre en el que todo cabe y en el que se acumula un motón de información que lógicamente tiene un coste en consumo de memoria. Pero esto es diferente de una fuga de memoria y tiene mejor solución: añadir más memoria a nuestro servidor puede ser una medida paliativa suficiente e incluso más rentable, en términos económicos, que dedicar esfuerzo a optimizar el consumo de memoria. Cuando nos enfrentamos a una fuga de memoria la solución de añadir más memoria al servidor solo es prolongar la agonía.

Para saber si estamos ante una fuga de memoria debemos observar los bytes privados del proceso sospecho. Para ello basta establece una traza del monitor de rendimiento que recoja la evolución del contador de bytes privados que se encuentra en la categoría proceso.

El contador Proceso/Bytes privados recoge el número de bytes que nuestro proceso ha reservado y que no pueden ser compartidos con ningún otro proceso. Por lo tanto si nuestro proceso se está guardando memoria de manera egoísta este contador será nuestro chivato.

Si tras un periodo de muestreo razonable (lo que es razonable depende de la naturaleza de las actividades que lleve a cabo el proceso) vemos que la tendencia de ese contador es siempre creciente, sin duda nos encontramos ante una fuga de memoria. Si este contador no muestra una tendencia creciente, aunque sea elevado o puntualmente elevado, no nos encontraremos ante una fuga de memoria.

Fijaros en la traza de bytes privados que dejaba el servicio en el que yo sufría la fuga de memoria.

Bytes privados

Viendo esa traza de casi dos días se hace evidente que estamos fugando memoria, sobre todo teniendo en cuenta que como ya comenté, con el paso del tiempo, acababa sufriendo un ‘casque’.

La segunda cuestión a desvelar es: ¿Se trata de una fuga de memoria nativa o manejada?

Sin duda saber si lo que estamos fugando es un recurso no manejado o una manejada nos va a dar muchas pistas sobre donde hemos de buscar en nuestro código la fuga de memoria.

Para esto vamos a usar otros dos contadores, que nos dan, para el caso que nos ocupa una información similar: cuanta memoria se está consumiento en la pila manejada del proceso (Memoria de .Net CLR/# Bytes en todos los montones)  y cuantas colecciones de generación 2 tenemos (Memoria de .Net CLR/# de colecciones de gen. 2), es decir cuantos objetos de nuestro proceso están sobreviviendo durante demasiado tiempo al recolector de basura. Al final los estos dos contadores nos informa de si estamos dejando atrás recursos maneajados sin liberar.

Logicamente, si estos contadores crecen de manera paralela a los bytes privados de nuestro proceso, estarémos ante una fuga de memoria manejada, sin embargo si crecen los bytes privados pero no crecen los bytes en la pila manejada lo que estaremos fugando es un recurso no manejado.

En la siguiente imagen podéis ver que en el caso que a mí me ocupaba claramente estabamos fugando memoria manejada, pues hay una clarísima correlación entre los tres contadores.

Bytes privados vs Bytes .Net

Ahora ya tengo claro que estoy fugando memoria y que es memoria manejada ¿que puedo hacer?.

Básicamente tengo dós opciones, usar WinDBG para ir obtieniendo la situación de la memoria de mi proceso y ver que objetos son los que estoy fugando opción que David Salgado explico explendidamente y que exije un dominio de WinDBG notable o usar una opción para meros mortales: el CLR Profiler.

WinDBG es una herramienta potentísima y que sin duda todo desarrollador que quiera poder resolver problemas peliagudos con aplicaciones de Windows tiene que saber manejar. Cuando las cosas se ponen realmente feas, WinDBG es nuestro mejor aliado y a menudo el único que nos puede sacar de atolladero. Os recomiendo los post sobre WinDBG de mi compañero en Plain Concepts, Pablo Alvarez (el tiempo que paso en Microsoft dando soporte ha dejado su huella), y los de David Salgado sobre el mismo tema. Otro recurso impresionante, eso sí en inglés, sobre depuración en .Net con WinDBG son las .NET Debugging Demos de Tess Ferrandez, Escalation Engineer en Microsof Suiza.

En un próximo post, veremos como usé el CLR Profiler (opte por la opción facil) para diagnosticar donde, exactamente, se estaba produciendo la fuga de memoria.

Si queréis aun más información sobre los temas tratados en este post, os dejo la siguiente lectura recomendada:

Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework I y II 
Investigating Memory Issues

Identify And Prevent Memory Leaks In Managed Code
.NET Debugging Demos Lab 3: Memory
.NET Debugging Demos Lab 3: Memory – Review

4 comentarios sobre “Cómo cazar una fuga de memoria en .Net (I)”

  1. Hola Rodrigo,

    gracias por el artículo, me está sirviendo mucho.

    He hecho los tres contadores que comentas en el artículo y lo que a mi me pasa és que me aumentan los bytes privados y los bytes en todos los montones de manera correlativa, pero en cambio el número de colecciones de generación 2 va a su olla, aumentado muy poco a poco (http://img29.imageshack.us/img29/1056/memoria.png). En este escenario, donde se está consumiendo la memoria?

    En mi caso lo que me preocupan són estos incrementos de utilización de memoria, que destrozan mi aplicación y empieza a funcionar mal. Cómo podria detectar dónde se producen?

    Muchisimas gracias,

    Vicenç

  2. @Vicenç: He mirado la imagen, la verdad es que no logro ver muy clara la situación. Es dificil diagnosticar cúal puede ser el problema sin ver en detalle, no hay dos fugas de memoria iguales.

    Sobre los incrementos de memoria, si que suelen sintoma de algúan problema. El tema pasa por ver que objetos están asignando esa memoria.

    Es dificil comentar por escrito y sin ver la situación el como hacerlo. Si te manejas con windbg y con volcados, podrías coger un volcado de la aplicación y ver que está pasando. También podrías ver en la segunda parte del post, como ver que objetos se están llevando la memoria con el .Net Profiler.

    ¡Un saludo!

  3. Gracias Rodrigo,

    te tengo que decir que a partir de hoy amo fervorosamente al CLRProfiler.

    Nuestro problema era que una rutina para hacer un MD5 de un fichero, cargaba todos los bytes del fichero en una lista. Como lo haciamos a unos 50 ficheros a la vez, esto hacia que la memoria subiera muchísimo. Hemos tuneado un poco la función para calcular el MD5 de otra manera ( por partes ) y se ha solventado el tema.

    Muchas gracias por la ayuda pq me has salvado de una buena!!

    Saludos!

  4. @Vicenç: Me alegro de que hayas solucionado el problema. La verdad es que muchas veces tiramos por la calle de en medio y cargamos todo el fichero en memoria, aunque sabemos que no debemos, para hacer alguna operación y luego vienen las consecuencias en el momento más inoportuno.

    ¡Un saludo!

Responder a rcorral Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *