Tech Days 2010 : Détection de fuites mémoire en .NET

Executive summary : La CLR .NET se charge de la gestion de mémoire. Il ne devrait donc jamais y avoir de fuites… Vous avez certainement déjà entendu cette erreur. Eh bien si, car seule la mémoire managée est gérée. Tout ce qui est lié à des ressources ou handles Win32, ou bien en interop mémoire, peut poser problème. La session montre aussi que c’est souvent des problèmes liés aux évènements desquels on oublie de se désabonner, en particulier lorsque ce sont des évènements statiques, car la durée de fixation des dépendances est alors la durée de vie de l’application.

La session est présentée par Yann Schwartz et Fabrice Marguerie, tous deux architectes .NET, respectivement chez Polom et MetaSapiens.

Principes

D’abord, une petite introduction est faite sur la gestion de la mémoire .NET. La gestion de la mémoire en .NET est réalisée par la CLR, et le Garbage Collector est responsable du recyclage de la mémoire. Par contre, il y a bien sûr des cas où on ne peut pas libérer la mémoire :

  • Références statiques
  • GCHandle (ce qui fait le lien mémoire dans le cas d’interopérabilité)
  • Références des piles (une par thread)
  • Finalization queue

Et encore on ne parle pas du code non managé.

Détection et correction

On commence avec perfmon.exe, le moniteur de performances de Windows, en ajoutant un compteur Processus / Octets privés pour l’application en question.

Attention, ce n’est pas parce que la mémoire augmente que la mémoire n’est pas relâchée. Donc, on met un bouton dans l’application pour forcer le garbage collection (ça peut être intéressant de mettre un bouton caché dans une application pour ça). Et si effectivement le compteur ne redescend pas lors du GC, c’est vraiment une fuite.

Approches complémentaires :

  • dotTrace pour un audit de haut niveau
  • .NET Memory Profiler pour plus de détails
  • WinDbg pour aller vraiment au fond des choses

JetBrains DotTrace

On commence par une démonstration de dotTrace. On regarde les root paths sur l’instance de l’objet qui garde de la mémoire. Microsoft.Win32.SystemEvents est une liste d’évènements statiques qui restent en vie toute la durée de l’application. Du coup, si une instance reste abonnée, elle ne peut pas être purgée.

Si on fait deux snapshots dotTrace, on a ensuite des icones pour faire du différentiel, voir les nouveaux objets, etc. On voit par exemple que l’application test s’abonne pour une fenêtre à l’événement SystemEvents.InstalledFontsChanged et ne s’en désabonne pas sur l’événement Disposed(). Note : on ne peut pas le faire sur la surcharge du Dispose() car elle est déjà réalisé dans le designer de la WinForms.

L’abonnement à un évènement fait qu’on vit autant de temps que l’objet exposant l’évènement. En effet, en interne, on passe un point d’entrée de fonction à un délégué sur une autre classe pour qu’elle nous appelle sur cette fonction. Du coup, ceci vaut pour un évènement statique, de manière drastique car c’est lié à la vie complète de l’application, mais aussi pour n’importe quel évènement exposé par un objet qui serait lui-même non supprimé par le garbage collector.

SciTech .NET Memory Profiler

Le produit est aussi commercial. Il permet contrairement à dotTrace de se brancher sur une application en cours. Il permet également l’analyse de dump mémoire post-mortem.

Cet outil donne des conseils, en relevant par exemple les instances qui ont été disposées, mais sont encore en vie. Typiquement, la fenêtre est fermée, et le Dipose() est donc appelé immédiatement en WinForms, mais il y a encore des instances qui pointent sur elle.

Attention, on a le même problème sur les closures dans des implémentations d’évènements avec des méthodes lambda.

WinDbg

Débogueur natif et managé, avec un mode graphique et une installation minimale pour pouvoir être placé sur un serveur en production si nécessaire (30 Mo).

Extensions managées : sos.dll pour la gestion mémoire. cmdtree permet d’afficher une fenêtre avec des raccourcis pour charger sos, l’utiliser, etc.

!DumpHeap, !DumpObject, et !GCRoot sont les trois commandes les plus utilisées. Elles permettent de voir le tas, de faire un espion sur un objet particulier, et de voir toutes les racines utilisées.

La liste de commandes réalisées est la suivante :

  1. !DumpHeap –stat –type MonApplication pour voir nos objets.
  2. !DumpHeap –mt Identifiant du type concerné.
  3. !do (abréviation du DumpObject) Identifiant de l’objet.
  4. !GCRoot Identifiant de l’objet.

Dans le cas d’exemple, on se rend compte que l’abonnement à un évènement statique est fait par le ContextMenu de WinForms. Du coup, il est essentiel de bien faire un Dispose(), si on ne passe pas par le designer. Quand on passe par le designer, il crée un ContextMenuStrip en lui passant this.Components, c’est-à-dire en se plaçant dans le container. Du coup, le Dispose() du container parcours tous les contrôles contenus et fait le Dispose() en automatique.

Dernier cas de problème

Plus le temps de faire le reste de la démo complète, mais ProcessExplorer de SysInternals permet de voir des choses en plus, en particulier les processus .NET sont en jaune. On voit dans l’exemple que csc.exe est sans arrêt lancé. Comme on génère des librairies en mémoire pour les XmlSerializer (bug sur le constructeur avec plus d’un paramètre, qui ne met pas en cache), et que l’AppDomain ne peut pas les décharger, on se retrouve à garder la mémoire occupée.

Prévention

Il est important de :

  • Bien appeler les Dispose().
  • Enrichir les Dispose() avec les désabonnements.
  • Code compilé à la volée (XSL/T compilé, sérialisation Xml, anciennement RegExp) : mettre dans des AppDomain séparés et les supprimer si nécessaire.
  • Faire attention aux problèmes spécifiques à des technologies.
  • Contrôler la possession des objets : le créateur n’est pas nécessairement celui qui tient l’objet, et donc en charge de leur libération.
  • Bien faire un –= pour chaque +=.
  • Utiliser les using et Dispose.
  • Utiliser si nécessaire des WeakEvents (garde le lien mais pas si il est le seul).
  • EventBroker pour des cas complexes.
  • Au pire, recycler les applications sur les serveurs.

Une bonne idée est d’intégrer un écran de monitoring dans une application, qui permet de suivre les objets en vie dans un logiciel.

VMMap de Sysinternals à voir, ainsi que les autres références que je n’ai pas eu le temps de prendre en note.

[Edit]

Suite migration du blog depuis l’ancienne plateforme, je rajoute les commentaires à la main…

Fabrice Marguerie :

Merci pour le compte-rendu !
Les transparents et le code source de l’application exemple sont disponibles : weblogs.asp.net/…/…slides-and-source-code.aspx

About JP Gouigoux

Jean-Philippe Gouigoux est Architecte Logiciel, MVP Connected Systems Developer. Il intervient régulièrement à l'Université de Bretagne Sud ainsi qu'à l'Agile Tour. Plus de détails sur la page "Curriculum Vitae" de ce blog.
This entry was posted in Retours. Bookmark the permalink.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha Captcha Reload