Cómo encontrar y evitar pérdidas de memoria y recursos en aplicaciones .net
En este artículo, explicaré por qué todavía existen pérdidas de memoria y cómo evitarlas. No se preocupe, este artículo no cubre el funcionamiento interno de GC y otras características avanzadas de la administración de memoria y recursos .net.
Es importante comprender las fugas en sí y cómo evitarlas, especialmente porque las fugas no se detectan fácilmente de forma automática. Las pruebas unitarias no pueden hacer nada al respecto. Una vez que su programa falla en producción, necesita encontrar una solución de inmediato. Así que tómate un tiempo para estudiar este artículo antes de que sea demasiado tarde.
Índice
- Introducción
- ¿Fuga? ¿recurso? ¿Qué significa?
- Cómo detectar fugas y encontrar recursos filtrados
- Causas comunes de pérdidas de memoria
- Demostración de causas comunes de pérdidas de memoria
- Cómo evitar fugas
- Herramientas relacionadas
- Conclusión
- Recursos
Introducción
Recientemente , Participé en un gran proyecto .Net (llamémoslo "fuga"). En este proyecto, soy responsable de rastrear las pérdidas de memoria y recursos. La mayor parte de mi tiempo lo dedico a filtraciones relacionadas con la GUI, más específicamente filtraciones relacionadas con el bloque de aplicaciones de interfaz de usuario compuesta (CAB) basado en .Net. Una aplicación de Windows Forms basada en el bloque de aplicaciones de interfaz de usuario compuesta (CAB). La mayoría de los conocimientos que analizaré y que se aplican directamente a winforms también se pueden aplicar a otras aplicaciones .net (como WPF, Silverlight, ASP.NET, servicios de Windows, aplicaciones de consola, etc.).
No soy un experto en lidiar con fugas, así que tuve que profundizar en la aplicación para hacer una limpieza. El propósito de este artículo es compartir con ustedes lo que aprendí mientras resolvía el problema. Espero que sea útil para los usuarios que necesitan detectar y resolver problemas de pérdida de memoria y recursos. La sección de descripción general a continuación describirá primero qué es una fuga, luego repasará cómo detectar fugas y recursos filtrados, así como cómo resolver y evitar fugas similares, y finalmente proporcionaré una lista de herramientas y recursos que pueden ayudar en esto. proceso.
¿Fuga? ¿recurso? ¿Qué quiere decir esto?
Pérdida de memoria
Antes de continuar, definamos lo que quiero decir con "pérdida de memoria". Simplemente cite la definición de Wikipedia. Esta definición encaja perfectamente con el problema que pretendo ayudar a resolver con este artículo:
En el campo de la informática, una pérdida de memoria es un tipo específico de pérdida de memoria que se produce cuando un programa informático no logra liberar con éxito recuerdo no deseado causado. Normalmente, un error en el programa impide que se libere la memoria no deseada.
Todavía tomado de Wikipedia: "Los siguientes lenguajes proporcionan administración automática de memoria, pero no pueden evitar pérdidas de memoria. Como Java, C#, VB.NET o LISP".
GC solo recupera la memoria que ya no se utiliza. La memoria en uso no se puede liberar. En .net, el GC no publicará ningún objeto que tenga referencias a él.
Manejadores y Recursos
La memoria no es lo único que se considera un recurso. Cuando su aplicación .net se ejecuta en Windows, consume un conjunto completo de recursos del sistema. Microsoft define tres categorías de objetos del sistema: usuario, GUI y kernel. No daré una lista completa de objetos clasificados aquí, pero señalaré algunos de los importantes:
- El sistema soporta la administración de Windows mediante el uso de objetos de usuario.
Los objetos relacionados incluyen acelerómetros, Carets?
Los objetos incluyen: acelerómetros, Carets, cursores, ganchos, iconos, menús y formularios (Windows).
- Los objetos GDI admiten el dibujo de gráficos: mapas de bits, pinceles, contextos de dispositivo (DC), fuentes, memorias DC, metarchivos, plumas, regiones, etc.
- Los objetos GDI admiten dibujo de gráficos.
- Los objetos del kernel soportan la gestión de memoria, la ejecución de procesos y la comunicación entre procesos (IPC): archivos, procesos, hilos, señales (Semáforos), temporizadores, tokens de acceso, sockets, etc.
Para obtener información detallada sobre todos los objetos del sistema, consulte MSDN.
Además de los objetos del sistema, también encontrará identificadores. Según MSDN, las aplicaciones no pueden acceder directamente a los datos del objeto ni a los recursos del sistema que representa el objeto. En cambio, la aplicación debe obtener un identificador de objeto antes de poder utilizarlo para inspeccionar o modificar los recursos del sistema. Independientemente, en la mayoría de los casos en .NET, el uso de los recursos del sistema es transparente porque los objetos y identificadores del sistema están representados directa o indirectamente por clases .NET.
Recursos no administrados
Los recursos como los objetos del sistema no son un problema en sí mismos, sino porque los sistemas operativos como Windows limitan la cantidad de sockets, archivos, etc. que se pueden abrir simultáneamente , este artículo proporciona una introducción a estos recursos. Por eso es importante prestar atención a la cantidad de objetos del sistema utilizados por su aplicación.
También existen límites de cuota en la cantidad de usuarios y objetos GDI que un proceso puede usar en un momento dado. Los valores predeterminados son 10.000 objetos GDI y 10.000 objetos de usuario. Para encontrar la configuración relevante para su máquina, puede utilizar las siguientes claves de registro:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows.GDIProcessHandle( GDIProcessHandle): GDIProcessHandleQuota y USERProcessHandleQuota.
¿Adivina qué? No es tan simple y rápidamente te encontrarás con otras limitaciones. Por ejemplo, consulte mi blog sobre montones de escritorio.
Suponiendo que estos valores sean personalizables, se podría pensar que la solución es romper los valores predeterminados y aumentar estas cuotas. Pero no creo que sea una buena idea, y he aquí por qué:
1. Las cuotas existen por una razón: usted no es la única aplicación en su sistema y todos los demás procesos que se ejecutan en su computadora deben coincidir. sus aplicaciones comparten recursos del sistema.
2. Si cambias la cuota para que sea diferente al resto del sistema. Debe asegurarse de que todas las máquinas en las que necesita ejecutar su aplicación lo hayan hecho y, desde la perspectiva de un administrador del sistema, dichas modificaciones son problemáticas.
3. Utilice el valor de cuota predeterminado en la mayoría de los casos. Si descubre que los valores configurados no son suficientes para las necesidades de su aplicación, es posible que deba realizar alguna limpieza.
Cómo detectar fugas y encontrar recursos filtrados
Un artículo en MSDN proporciona una buena descripción del problema real de las fugas:
Incluso si son pequeñas fugas, si repetido, también puede hacer caer el sistema.
Esto es similar a una fuga de agua. Una gota de agua que cae no es gran cosa. Pero las filtraciones repetidas, gota a gota, pueden convertirse en un gran problema.
Como explicaré más adelante, un único objeto sin sentido puede soportar un gráfico completo de objetos pesados en la memoria.
Aún en el mismo artículo, aprenderá:
El proceso general de tres pasos para eliminar fugas:
Encontrar la fuga
.2. Encuentre el recurso filtrado
3. Decida cuándo y dónde publicar el recurso filtrado en el código fuente
La forma más directa de "encontrar" una fuga es sufrir los efectos de la fuga y desencadenar fugas. Los mensajes "La forma más directa de descubrir una fuga es vivir con los problemas que crea
Probablemente nunca antes hayas visto falta de memoria. Los mensajes de falta de memoria" son muy raros. Esto se debe a que cuando el sistema operativo se queda sin memoria real (RAM), utiliza el espacio del disco duro para expandir la memoria (esto se llama memoria virtual).
Las excepciones "fuera de control" pueden ser más comunes en aplicaciones de gráficos. La excepción exacta es System.ComponentModel.Win32Exception o System.OutOfMemoryException, los cuales contienen la siguiente información: "Error al crear el identificador de la tabla". Ambas excepciones ocurren cuando se utilizan dos recursos al mismo tiempo, generalmente porque el objeto que debería liberarse aún no lo ha sido.
Otra situación que nos encontramos a menudo es cuando una aplicación o todo el sistema tarda en realizar cambios. Esto sucede porque los recursos del sistema se están agotando.
Permítanme hacer una suposición difícil: las fugas en la mayoría de las aplicaciones no serán un problema la mayor parte del tiempo, porque solo ocurrirán si su aplicación se usa de manera intensiva durante un tiempo prolongado. por una fuga.
Si sospechas que algunos objetos están atrapados en la memoria después de haberlos liberado, lo primero que debes hacer es averiguar cuáles son esos objetos.
Esto puede parecer obvio, pero no lo es tanto cuando se busca.
Se recomienda utilizar herramientas de memoria para encontrar objetos de alto nivel o contenedores raíz que no se espera que permanezcan en la memoria. En el proyecto x, estos objetos pueden ser instancias de LayoutView (usamos el patrón MVP (Presentación de vista de modelo)). En su proyecto real, esto puede depender de cuál sea su objeto raíz.
El siguiente paso es descubrir por qué siguen ahí cuando deberían haber desaparecido. Aquí es donde los depuradores y las herramientas realmente pueden ayudar. Pueden mostrarle cómo estos objetos están vinculados entre sí. Al observar las referencias del objeto zombie, puede encontrar la causa raíz del problema.
Puedes elegir el método ninja. (Consulte los capítulos sobre SOS.dll y WinDbg en el capítulo Introducción a las herramientas).
Utilicé JetBrains dotTrace en Project X y continuaré usándolo en este artículo. Entraré en más detalles sobre esta herramienta en un capítulo posterior relacionado con la herramienta.
Tu objetivo es encontrar la referencia que finalmente causó el problema. No te detengas en el primer objetivo que encuentres, pero pregúntate por qué este tipo todavía está en tu memoria.
Causas de pérdidas de memoria comunes
Las fugas anteriores son más comunes en .net. La buena noticia es que no existen muchas causas para estas filtraciones. Esto significa que no es necesario buscar entre una serie de posibles causas al intentar reparar una fuga.
Revisemos estos culpables comunes y los diferenciaré:
- Referencias estáticas
- Vinculación de eventos no escritos
p>- Enlace de evento estático no escrito
- Método de descarte no llamado
- Método de descarte no completado correctamente
Además de las razones típicas anteriores, existen otras situaciones que pueden también causan fugas:
- Windows Forms: abuso de fuente vinculante
- CAB: elemento de llamada al trabajo no eliminado
Solo he enumerado algunas causas posibles en su aplicación, pero debe quedar claro que el uso real de otro código .net, bibliotecas, etc. puede provocar fugas.
El código Net, las bibliotecas, etc., o el uso real de otros códigos .net, bibliotecas, etc. de los que depende su aplicación, también pueden causar fugas.
Pongamos un ejemplo. En el proyecto x, se utiliza un conjunto de controles de terceros para crear la interfaz. Uno de los controles utilizados para mostrar todas las barras de herramientas administra una lista de barras de herramientas. No hay nada de malo en este enfoque, pero una cosa es que incluso si la barra de herramientas administrada implementa la interfaz IDisposable, la clase de administración nunca llama a su método Dispose. Afortunadamente, esto sucede en el espacio de trabajo y es fácil de detectar: tenemos que llamar nosotros mismos al método Dispose de todas las barras de herramientas. Desafortunadamente, eso no es suficiente, la clase de barra de herramientas en sí tiene muchos problemas: no puede liberar los controles que aloja (botones, etiquetas, etc.). Entonces, en la solución tenemos que agregar funcionalidad de lanzamiento a cada control en la barra de herramientas, pero esta vez no es tan simple porque cada control secundario en la barra de herramientas es diferente. De todos modos, este es solo un ejemplo especial y lo que quiero ilustrar es que cualquier biblioteca y componente de terceros utilizado en la aplicación puede causar fugas.
Finalmente, existe otro tipo de fuga causada por el marco .net, que es causada por algunos malos hábitos de uso. Aunque el propio .NET Framework puede provocar fugas, esto es muy raro. Es fácil culpar a .net, pero antes de culpar a otros, debemos comenzar con el código que escribimos para ver si hay algún problema en él.
Demostración de fugas comunes
He enumerado las principales fuentes de fugas, pero no quiero limitarme a eso todavía. Creo que este artículo sería más útil si se pudiera proporcionar un ejemplo vívido de cada filtración. Bien, comencemos con Vs y dotTrace y luego veamos un código de muestra. Le mostraré cómo reparar o evitar cada fuga simultáneamente.
Project X utiliza patrones CAB y MVP, lo que significa que la interfaz consta de un espacio de trabajo, una vista y un renderizador. En aras de la simplicidad, decidí utilizar una aplicación Winform que consta de un conjunto de ventanas. Este es el mismo enfoque utilizado por Jossef Goldberg en el artículo "Pérdidas de memoria en aplicaciones WPF". Incluso aplicaría el mismo ejemplo y controladores de eventos directamente a mi aplicación Winform.