Anatomía de una "fuga de memoria"

173

En perspectiva .NET:

  • ¿Qué es una pérdida de memoria ?
  • ¿Cómo puede determinar si su aplicación tiene fugas? ¿Cuales son los efectos?
  • ¿Cómo se puede prevenir una pérdida de memoria?
  • Si su aplicación tiene pérdida de memoria, ¿desaparece cuando el proceso se cierra o se cierra? ¿O las pérdidas de memoria en su aplicación afectan otros procesos en el sistema incluso después de la finalización del proceso?
  • ¿Y qué hay del código no administrado al que se accede a través de COM Interop y / o P / Invoke?
huseyint
fuente

Respuestas:

110

La mejor explicación que he visto está en el Capítulo 7 del libro electrónico gratuito Fundamentos de la programación .

Básicamente, en .NET se produce una pérdida de memoria cuando los objetos referenciados están enraizados y, por lo tanto, no se pueden recolectar basura. Esto ocurre accidentalmente cuando mantiene referencias más allá del alcance previsto.

Sabrá que tiene fugas cuando comience a recibir OutOfMemoryExceptions o que el uso de su memoria supere lo que esperaría ( PerfMon tiene buenos contadores de memoria).

Comprender el modelo de memoria de .NET es su mejor manera de evitarlo. Específicamente, entendiendo cómo funciona el recolector de basura y cómo funcionan las referencias, nuevamente, lo remito al capítulo 7 del libro electrónico. Además, tenga en cuenta las dificultades comunes, probablemente los eventos más comunes. Si el objeto A se ha registrado para un evento en el objeto B , entonces el objeto A se quedará hasta el objeto B desaparece porque B contiene una referencia a una . La solución es anular el registro de sus eventos cuando haya terminado.

Por supuesto, un buen perfil de memoria le permitirá ver sus gráficos de objetos y explorar el anidamiento / referencia de sus objetos para ver de dónde provienen las referencias y qué objeto raíz es responsable ( perfil de hormigas de puerta roja , JetBrains dotMemory, memprofiler son realmente buenos opciones, o puede usar WinDbg y SOS de solo texto , pero recomiendo encarecidamente un producto comercial / visual a menos que sea un verdadero gurú).

Creo que el código no administrado está sujeto a sus pérdidas de memoria típicas, excepto que las referencias compartidas son administradas por el recolector de basura. Podría estar equivocado sobre este último punto.

Karl Seguin
fuente
11
Oh, te gusta el libro, ¿verdad? He visto al autor aparecer en stackoverflow de vez en cuando.
Johnno Nolan
Algunos objetos .NET también pueden enraizarse y volverse incobrables. Cualquier cosa que sea desechable debe eliminarse debido a esto.
kyoryu
1
@kyoryu: ¿Cómo se enraíza un objeto?
Andrei Rînea
2
@Andrei: Creo que un subproceso en ejecución es quizás el mejor ejemplo de un objeto enraizándose. Un objeto que pone una referencia a sí mismo en una ubicación no pública estática (como suscribirse a un evento estático o implementar un singleton por inicialización de campo estático) también puede haberse enraizado, ya que no hay una manera obvia de ... um ... "arrancarlo" de su amarre.
Jeffrey Hantin
@Jeffry, esta es una forma poco convencional de describir lo que está sucediendo y me gusta.
Exitos
35

Estrictamente hablando, una pérdida de memoria está consumiendo memoria que "ya no se usa" por el programa.

"Ya no se usa" tiene más de un significado, podría significar "no más referencias a él", es decir, totalmente irrecuperable, o podría significar, referenciado, recuperable, no utilizado, pero el programa mantiene las referencias de todos modos. Solo lo posterior se aplica a .Net para objetos perfectamente administrados . Sin embargo, no todas las clases son perfectas y, en algún momento, una implementación subyacente no administrada podría perder recursos de forma permanente para ese proceso.

En todos los casos, la aplicación consume más memoria de la estrictamente necesaria. Los efectos secundarios, dependiendo de la cantidad filtrada, podrían pasar de ninguno, a la desaceleración causada por la recolección excesiva, a una serie de excepciones de memoria y finalmente a un error fatal seguido de la terminación forzada del proceso.

Usted sabe que una aplicación tiene un problema de memoria cuando el monitoreo muestra que cada vez se asigna más memoria a su proceso después de cada ciclo de recolección de basura . En tal caso, está guardando demasiado en la memoria o se pierde alguna implementación subyacente no administrada.

Para la mayoría de las filtraciones, los recursos se recuperan cuando se termina el proceso, sin embargo, algunos recursos no siempre se recuperan en algunos casos precisos, los identificadores de cursor GDI son conocidos por eso. Por supuesto, si tiene un mecanismo de comunicación entre procesos, la memoria asignada en el otro proceso no se liberará hasta que ese proceso lo libere o finalice.

Coincoin
fuente
32

Creo que las preguntas "qué es una pérdida de memoria" y "cuáles son los efectos" ya se han respondido bien, pero quería agregar algunas cosas más en las otras preguntas ...

Cómo entender si su aplicación tiene fugas

Una forma interesante es abrir perfmon y agregar trazas para # bytes en todos los montones y colecciones de # Gen 2 , en cada caso mirando solo su proceso. Si el ejercicio de una función en particular aumenta el total de bytes, y esa memoria permanece asignada después de la próxima colección Gen 2, se podría decir que la función pierde memoria.

Como prevenir

Se han dado otras buenas opiniones. Solo agregaría que tal vez la causa más comúnmente ignorada de las pérdidas de memoria .NET es agregar controladores de eventos a los objetos sin eliminarlos. Un controlador de eventos adjunto a un objeto es una forma de referencia a ese objeto, por lo que evitará la recopilación incluso después de que todas las demás referencias hayan desaparecido. Recuerde siempre separar los controladores de eventos (usando la -=sintaxis en C #).

¿La fuga desaparece cuando finaliza el proceso, y qué pasa con la interoperabilidad COM?

Cuando se cierra el proceso, el sistema operativo recupera toda la memoria asignada a su espacio de direcciones, incluidos los objetos COM que se envían desde las DLL. Comparativamente raramente, los objetos COM pueden ser servidos desde procesos separados. En este caso, cuando finaliza su proceso, aún puede ser responsable de la memoria asignada en los procesos del servidor COM que utilizó.

Martín
fuente
19

Definiría las pérdidas de memoria como un objeto que no libera toda la memoria asignada después de que se haya completado. Descubrí que esto puede suceder en su aplicación si está utilizando Windows API y COM (es decir, código no administrado que tiene un error o no se administra correctamente), en el marco y en componentes de terceros. También he descubierto que no remarcar después de usar ciertos objetos como bolígrafos puede causar el problema.

Personalmente, he sufrido excepciones de falta de memoria que pueden ser causadas pero que no son exclusivas de pérdidas de memoria en aplicaciones de red de puntos. (OOM también puede provenir de pinning ver Pinning Artical ). Si no recibe errores de OOM o necesita confirmar si es una pérdida de memoria que lo está causando, entonces la única forma es perfilar su aplicación.

También trataría de asegurar lo siguiente:

a) Todo lo que implemente Idisposable se desecha usando un bloque final o la declaración de uso, que incluye pinceles, bolígrafos, etc. (algunas personas argumentan que no se debe agregar nada)

b) Cualquier cosa que tenga un método de cierre se cierra nuevamente usando finalmente o la declaración de uso (aunque he encontrado que usar no siempre se cierra dependiendo de si declaraste el objeto fuera de la declaración de uso)

c) Si está utilizando código no administrado / API de Windows que se tratan correctamente después (algunos tienen métodos de limpieza para liberar recursos)

Espero que esto ayude.

Juan
fuente
19

Si necesita diagnosticar una pérdida de memoria en .NET, consulte estos enlaces:

http://msdn.microsoft.com/en-us/magazine/cc163833.aspx

http://msdn.microsoft.com/en-us/magazine/cc164138.aspx

Esos artículos describen cómo crear un volcado de memoria de su proceso y cómo analizarlo para que primero pueda determinar si su fuga no se administra o administra, y si se administra, cómo averiguar de dónde viene.

Microsoft también tiene una herramienta más nueva para ayudar a generar volcados de memoria, para reemplazar ADPlus, llamado DebugDiag.

http://www.microsoft.com/downloads/details.aspx?FamilyID=28bd5941-c458-46f1-b24d-f60151d875a3&displaylang=en

Eric Z Beard
fuente
15

La mejor explicación de cómo funciona el recolector de basura está en Jeff Richters CLR a través del libro C # , (Cap. 20). Leer esto proporciona una gran base para comprender cómo persisten los objetos.

Una de las causas más comunes de enraizamiento accidental de objetos es mediante la conexión de eventos fuera de una clase. Si conecta un evento externo

p.ej

SomeExternalClass.Changed += new EventHandler(HandleIt);

y olvide desengancharlo cuando disponga, entonces SomeExternalClass tiene una referencia a su clase.

Como se mencionó anteriormente, el generador de perfiles de memoria SciTech es excelente para mostrarle las raíces de los objetos que sospecha que tienen fugas.

Pero también hay una forma muy rápida de verificar un tipo en particular: simplemente use WnDBG (incluso puede usar esto en la ventana inmediata de VS.NET mientras está conectado):

.loadby sos mscorwks
!dumpheap -stat -type <TypeName>

Ahora haga algo que cree que eliminará los objetos de ese tipo (por ejemplo, cerrar una ventana). Aquí es útil tener un botón de depuración en algún lugar que se ejecute System.GC.Collect()un par de veces.

Luego corre de !dumpheap -stat -type <TypeName>nuevo. Si el número no bajó, o no bajó tanto como esperaba, entonces tiene una base para una mayor investigación. (Obtuve este consejo de un seminario impartido por Ingo Rammer ).

Gus Paul
fuente
14

Supongo que en un entorno administrado, una fuga sería mantener una referencia innecesaria a una gran cantidad de memoria.

Bernardo
fuente
11

¿Por qué la gente piensa que una pérdida de memoria en .NET no es lo mismo que cualquier otra pérdida?

Una pérdida de memoria es cuando se adjunta a un recurso y no lo deja ir. Puede hacer esto tanto en codificación administrada como no administrada.

Con respecto a .NET y otras herramientas de programación, ha habido ideas sobre la recolección de basura y otras formas de minimizar las situaciones que harán que su aplicación se filtre. Pero el mejor método para evitar pérdidas de memoria es que necesita comprender su modelo de memoria subyacente y cómo funcionan las cosas en la plataforma que está utilizando.

Creer que GC y otras magias limpiarán tu desorden es el camino corto hacia las pérdidas de memoria, y será difícil de encontrar más adelante.

Al codificar sin administrar, normalmente se asegura de limpiar, sabe que los recursos que toma, serán su responsabilidad de limpiar, no los del conserje.

En .NET, por otro lado, mucha gente piensa que el GC limpiará todo. Bueno, hace algo por ti, pero debes asegurarte de que sea así. .NET envuelve muchas cosas, por lo que no siempre sabe si está tratando con un recurso administrado o no administrado, y necesita asegurarse de con qué está tratando. El manejo de las fuentes, los recursos GDI, el directorio activo, las bases de datos, etc. es lo que generalmente debe tener en cuenta.

En términos manejados, pondré el cuello en la línea para decir que desaparece una vez que se elimina / elimina el proceso.

Sin embargo, veo que mucha gente tiene esto, y realmente espero que esto termine. ¡No puede pedirle al usuario que finalice su aplicación para limpiar su desorden! Eche un vistazo a un navegador, que puede ser IE, FF, etc., luego abra, por ejemplo, Google Reader, deje que permanezca durante algunos días y observe lo que sucede.

Si luego abre otra pestaña en el navegador, navega a algún sitio, luego cierra la pestaña que albergaba la otra página que hizo que el navegador se filtrara, ¿cree que el navegador liberará la memoria? No es así con IE. En mi computadora, IE consumirá fácilmente 1 GiB de memoria en un corto período de tiempo (aproximadamente 3-4 días) si uso Google Reader. Algunas páginas de noticias son aún peores.

neslekkiM
fuente
10

Supongo que en un entorno administrado, una fuga sería mantener una referencia innecesaria a una gran cantidad de memoria.

Absolutamente. Además, no usar el método .Dispose () en objetos desechables cuando sea apropiado puede causar fugas de memoria. La forma más fácil de hacerlo es con un bloque de uso porque se ejecuta automáticamente .Dispose () al final:

StreamReader sr;
using(sr = new StreamReader("somefile.txt"))
{
    //do some stuff
}

Y si crea una clase que usa objetos no administrados, si no está implementando IDisposable correctamente, podría estar causando pérdidas de memoria para los usuarios de su clase.

Seibar
fuente
9

Todas las pérdidas de memoria se resuelven mediante la terminación del programa.

Pérdida de memoria suficiente y el sistema operativo puede decidir resolver el problema en su nombre.

Josh
fuente
8

Estaré de acuerdo con Bernard en cuanto a lo que sería una fuga de memoria en .net

Puede perfilar su aplicación para ver su uso de memoria y determinar que si está administrando mucha memoria cuando no debería, podría decir que tiene una fuga.

En términos administrados, pondré el cuello en la línea para decir que desaparece una vez que se elimina / elimina el proceso.

El código no administrado es su propia bestia y si existe una fuga dentro de él, seguirá una memoria estándar. definición de fuga

Palmadita
fuente
7

También tenga en cuenta que .NET tiene dos montones, uno es el montón de objetos grandes. Creo que se colocan objetos de aproximadamente 85k o más en este montón. Este montón tiene reglas de vida diferentes que el montón normal.

Si está creando grandes estructuras de memoria (Diccionario o Lista), sería prudente buscar cuáles son las reglas exactas.

En cuanto a recuperar la memoria al finalizar el proceso, a menos que esté ejecutando Win98 o su equivalente, todo se devuelve al sistema operativo al finalizar. Las únicas excepciones son las cosas que se abren entre procesos y otro proceso aún tiene el recurso abierto.

Los objetos COM pueden ser complicados. Si siempre usas elIDispose patrón, estará a salvo. Pero me he encontrado con algunos ensamblados de interoperabilidad que implementan IDispose. La clave aquí es llamar Marshal.ReleaseCOMObjectcuando haya terminado. Los objetos COM todavía usan el recuento de referencias COM estándar.

Joel Lucsy
fuente
6

Encontré .Net Memory Profiler una muy buena ayuda al encontrar pérdidas de memoria en .Net. No es gratis como el Microsoft CLR Profiler, pero en mi opinión es más rápido y más preciso. UNA

Lars Truijens
fuente