Fugando memoria con .Net

Una de la grandes maravillas de los lenguajes manejados es que es imposible fugar memoria… al menos en teoria. Pero la verdad es que aunque es mucho más dificil fugar memoria, no es imposible. Es imposible fugar memoria en el sentido clásico del termino, en el sentido de reservar memoria, con un new, un malloc, un SysAllocString, etc y no recordar liberarla con el correspondiente delete, free, o SysFreeString, algo muy común en C o C++, esta es la buena noticia y es más por si sola justifica el uso de lenguajes manejados. Buscar fugas de memoria puede ser muy divertido, pero, desde luego no es lo más productivo que podemos hacer como desarrolladores.


La mala noticia es que nada nos libra de otro tipo de fugas de memoria, igualmente perniciosas pero, cierto es, mucho menos frecuentes, por citar algunos ejemplos: podemos añadir objetos a una colección que actue de cache y nunca sacar objetos de manera que la colección crezca indefinidamente (esta fuga suele ser facil de cazar…) o algunas un poco más sutiles como el ejemplo que hoy propongo. Otra forma habitual de fugar memoria en .Net es cuando se utilizan recusos nativos mediante COM Interop o P/Invoke.


Hacía bastantes años, desde que deje Panda Software y la programación en C/C++ y COM que no me enfrentaba a un caso serio de perdida de memoria. A pesar de haber escrito mucho código en .Net. Pero desde hace algún tiempo un proyecto en el que estoy trabajando presentaba una seria perdida de memoria en un servicio que se ejecuta de manera continua antendiendo peticiones del resto de componentes de la aplicación. El resultado es que al cabo de una semana y tras agonizar durante un día en condiciones de baja memoria, en un servidor de producción con 4Gb de memoria, la aplicación cascaba con una agónica OutOfMemoryException. Las entradas en el registro de eventos eran bastante elocuente (esta es un ejemplo de una de ellas, pero variaban según el componente que tratase de hacer la asignación de memoria ‘definitiva’):


EventType clr20r3, P1 AppQueFuga.exe, P2 1.0.0.137, P3 45d6968e, P4 mscorlib, P5 2.0.0.0, P6 4333ab80, P7 116e, P8 29, P9 system.outofmemoryexception, P10 NIL


Buscar fugas de memoria siempre me ha apasionado, es un tipo de bug realmente interesante, y la verdad me ha traido grandes recuerdos…


El reto que os planteo hoy es ¿alguien sabe por qué el siguiente código, caso mínimo extraido desde le problema real y a simple vista totalmente inofensivo, fuga memoria?. Para ver que fuga memoria solo tenéis que compilar el código (lo tenéis como adjunto a este post), ponerlo a ejecutar y ver el consumo de memoria del proceso. Fijaros que solo uso clases manejadas, lo que hace aun más interesante esta fuga de memoria:




using System;


using System.Diagnostics;


 


namespace Leaker


{


  static class Leaker


  {


    public static TraceSwitch CurrentTraceSwitch


    {


      get { return new TraceSwitch(“Test”, “Test”, “Test”); }


    }


  }


 


  class Program


  {


    static void Main(string[] args)


    {


      //Bucle infinito


      while (true)


      {


        //Cada vez que se ejecuta esta línea perdemos memoria


        Console.WriteLine(Leaker.CurrentTraceSwitch.Level);


      }


    }


  }


}


En proximos post voy a comentar cómo diagnostique esta fuga de memoria y daré la solución a por qué se produce, estad atentos, os aseguro que será interesante.

24 comentarios en “Fugando memoria con .Net”

  1. Es una clase estática con una función estática que devuelve un objeto. El GC no puede garantizar que ese objeto quede fuera de ámbito con lo cual lo mantiene en memoria. Esto no lo clasifico yo como una fuga sino un mal diseño.

  2. Siento meter “la pulla”, pero recuerdo una discusión sobre el tema de .NET y el uso de memoria donde 3 grandes (tamaño y peso) programadores me increpaban sobre mi manía por diseñar un código sensible, cuidadoso y respetuoso con memoria, ya que para mí no es ninguna pérdida de tiempo, sin embargo, ellos (dos compañeros y un profesor de un curso sobre tfs) defendían que el software fabricado con .NET podía y de hecho, debía consumir toda la memoria que necesitase, ya que si se hacía con ella es porque nadie más (en el SO) la necesitaba y si no ya estaba el GC, jajajajajajaja. Pues cuando pasan estas cosas, me acuerdo mucho de esa discusión (muy sana, eh) y me confirmo en mi postura de “perder” algo de tiempo en “poner a punto” el uso de memoria de lo que estés desarrollando.

    Un saludo.

    PD: Voy a probar el código haber que pasa.

  3. Anonimo, has pinchado hueso. Tu hipótesis es erronea, aunque he de decir que yo también fue lo primero que pense… Si tu hipotesis fuese cierta, ese código fugaría fuese lo que fuese que devolviese el método estático, pero no es así. Pero no es cierto, si cambias el método para que devuelva un object, la fuga no se produce. ¿Curioso no?.

    VAS, la discusión era sobre optimización temprana, más que sobre memory leaks… pero he de reconocer que también tenías tu punto de razón. El lunes lo hablamos jejejejej…

  4. Claro que no porque si creas un objeto manejado no habrá problema, pero el objeto en cuestión tiene recursos no manejados que deben ser liberados.

  5. Anonimo, vuelves a errar, el TraceSwitch es un objeto totalmente manejado, sino implementaría IDisposable ¿no?… Quiza usando reflector te acerques un poco más a la solución… o quizás te despistes más aun…

  6. No tengo ni idea de esto, pero creo que la linea new TraceSwitch(“Test”, “Test”, “Test”); esta creando un objeto nuevo en memoria cada vez que se solicita el valor de la propiedad, quizas el new habria que situarlo en una propiedad privada fuera de la propiedad publica…

  7. El problema es el constructor de la clase padre (Switch), almacena las instancias creadas en una lista estática (llamada switches) actuando como caché. Por lo tanto guarda una referencia a cada objeto creado y éstos nunca se liberan debido a esa referencia.
    Dejo el snippet de código del constructor de switch que realiza esa llamada.

    lock (switches)
    {
    switches.Add(new WeakReference(this));
    }

    PD: muy buen post!

  8. Adrian… has dado en el clavo. La clave esta en el uso que se hace de WeakReference.

    Se añade a un lista que actua como cache una referecia debil (que no será recolectada por el recolector de basura) cada vez que se construye un Switch. Esto funciona perfectamente, salvo que tengas la idea de crear un nuevo Switch cada vez que quieres comprobar si esta establecido o no.

    Lo que por más que miro con reflector no he acabado de entender es porque usa ahí una WeakReference en lugar de usar una simple referencia.

    En breve contaré que herramientas y técnicas use para diagnosticar el problema…

    Saludos!

  9. Hola!

    (rodrigo da mas tiempo tioooooooo, acabo de ver que ya has respondido)

    Ahi va mi apuesta….

    He corrido la aplicacion y la he ido parando para ver el estado de la memoria… he visto estos resultados:

    790fd8c4 345980 System.String
    790fd8c4 345980 System.String
    790fd8c4 345980 System.String

    79104c38 1638048 System.WeakReference
    79104c38 3090592 System.WeakReference
    79104c38 3824736 System.WeakReference

    Si bien el numero de strings se mantiene constante, crece el de weakreferences… Las he volcado y he mirado a ver porque no se están liberando. Parece que mantienen una referencia viva y no se recogen por eso

    DOMAIN(00344B18):HANDLE(Pinned):2813f0:Root:02b23030(System.Object[])->
    01b24300(System.Collections.Generic.List`1[[System.WeakReference, mscorlib]])->
    02c052b0(System.Object[])->
    01c86738(System.WeakReference)

    Con el codigo que hemos visto del constructor de Adrian sabemos que es el array ese de switch el que tiene las referencias… de modo que tiene que ir creciendo

    Vemos su tamaño en dos paradas consecutivas de la ejecución:

    0:003> !do 01b24300
    79102290 40009c8 c System.Int32 1 instance 722088 _size

    0:003> !do 01b24300
    79102290 40009c8 c System.Int32 1 instance 882340 _size

    De modo que el array sigue creciendo pq se sigue llenando de weakreferences :_)

    pq?
    Que haya una weakreference apuntando a los strings me parece buena (asi no crecen indefinidamente), pero que no haya control sobre el tamaño del array… mirare a ver si es un fallo reportado

    ciao!

  10. Ese David!!!! Estás hecho un crack… tirando de WinDbg y SOS…

    Quizás debí dar más tiempo…

    Decirte que yo no lo cazé con el WinDbg y SOS, sino con una herramienta para meros mortales ;).

    Estaría de puta madre que explicases en un post como has usado estas herramientas, completaría de manera redonda el tema.

    Informanos sobre si se trata de un fallo reportado… yo también estaba pensando que se puede tratar de un bug.

  11. Andrechi, es cierto que el reciclado de los AppPools por uso de memoria que puede hacer IIS enmascara muchas fugas de memoria.

    Ya me he encontrado en alguna ocasión de gente que se quejaba de que perdía la sesion o la cache, o de que se presenta la ventana de login de la aplicación ‘sin motivo’, o de que su aplicación Asp.net va perdiendo rendimiento y luego lo recupera de repente… sin darse cuenta que IIS está reciclando el proceso de trabajo.

  12. Hecho… comentado en esta dirección… no le he podido meter mucha más detalle porque me tiro todo el dia escribiendo, pero promerto retomar los temas de internals en el blog O=)

  13. David… cuanto tiempo… jode, me pasan el link, lo miro “jode, interesante…” y llego al final y veo “David Salgado” :O, juas! si es david !!! Bueno verte por ahi 😉 y bueno verte con WinDbg ( sabes que yo era softicero… pero ahora practicamente el 100% windbg… son otros tiempos, otros requisitos 😉 )

    Salud 😀 !

  14. Rodrigo,

    Interesante el post, y tambien me interesa mucho un comentario que le haces a Andrechi.

    # re: Fugando memoria con .Net

    Andrechi, es cierto que el reciclado de los AppPools por uso de memoria que puede hacer IIS enmascara muchas fugas de memoria.

    Ya me he encontrado en alguna ocasión de gente que se quejaba de que perdía la sesion o la cache, o de que se presenta la ventana de login de la aplicación ‘sin motivo’, o de que su aplicación Asp.net va perdiendo rendimiento y luego lo recupera de repente… sin darse cuenta que IIS está reciclando el proceso de trabajo.

    jueves, 24 de abril de 2008 10:30 by Rodrigo Corral

    Nosotros actualmente estamos teniendo este problema con una aplicación web y no hemos podido determinar a que se debe, pero es igual a lo que comentas; tu me podrias dar alguna luz de como solucionar esto.

    Mil gracias

  15. Ya tengo la respuesta del grupo de producto… es un bug corregido en la próxima major release de .NET Framework

    🙂

    …esta vez nos han ganadado y ya lo sabian… pero la próxima reportaremos un bug no conocido 😛

  16. CesVer, la verdad es que cada caso es un mundo… es muy dificil dar consejo sobre que puede esta pasando sin poner las manos sobre el sistema que está dando problemas…

    Siento no poderte ayudar más… pero entiende que con tampoca información…

  17. Un bug!!! Joder… y me tiene que tocar a mi sufrirlo… 😉

    Bueno, la verdad es que es el primer bug que sufro directamente en un motón de años trabajando con .Net… no me puedo quejar…

    Gracias por la información titán!!!

  18. Conosco una forma de liberar la memoria con .NET sin importar los objetos que no se encuentren manejados y sin utilizar el GC. Veamos un poco que es el Garbaje Colector o recolector de basura. La idea del mismo es liberar todas las instancias de objetos no utilizadas, pero encontre que aun liberada esa memoria, la misma queda reservada al proceso de .NET con lo cual varias veces las aplicaciones tienden a acumular memoria consumida. Con una funcion del SO se puede liberar un poco de esa memoria sin usar y reservada a .NET.

    ‘Declaración de la API
    Private Declare Auto Function SetProcessWorkingSetSize Lib “kernel32.dll” (ByVal procHandle As IntPtr, ByVal min As Int32, ByVal max As Int32) As Boolean

    ‘Funcion de liberacion de memoria
    Public Sub ClearMemory()

    Try

    Dim Mem As Process
    Mem = Process.GetCurrentProcess() SetProcessWorkingSetSize(Mem.Handle, -1, -1)
    Catch ex As Exception
    ‘Control de errores
    End Try

    End Sub

    marcelo8690@hotmail.com

  19. Mmmm… lamento decirte que tu planteamiento no es del todo correcto.

    Lo primero de todo es que el Task Monitor no es la mejor herramienta para detectar el consumo de memoria de una aplicación .NET. Lo que tu nos propones es el uso de una funcion de la API de Win32 que nos permite cambiar el valor del WorkingSet del proceso, pero no va a liberarte ninguna memoria (aunque aparentemente, a través del Task Monitor, te lo pueda parecer).

    No me puedo extender en las explicaciones en este pequeño comentario del blog de Rodrigo, pero te animo a que hagas la prueba. Haz una aplicacion que consuma memoria y establece una llamada al Depurador desde ella: mete WinDbg y consulta la memoria .NET con un “!eeheap -gc”. Despues repite la misma prueba, pero antes de levantar el depurador invoca al SetProcessWorkingSetSize, y comprobaras que el tamaño del managed heap es identico.

    Si te interesa una explicacion mas detallada hazmelo saber, e intentaré sacar tiempo para hacer una entrada en mi blog; eso si, no se de donde lo sacaré porque tengo pendiente una entrada apasionante sobre un bug en el CLR que aun no pude escribir!!

Deja un comentario

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