measurement-and-instrumentation
Déboguement Java réel : Diagnostic et fixation de fuites de mémoire communes
Table of Contents
Malgré le mécanisme de collecte automatique des ordures de Java, les applications peuvent encore souffrir de fuites de mémoire qui dégradent progressivement les performances, augmentent les temps de réponse et finissent par entraîner des défaillances catastrophiques. Comprendre comment diagnostiquer et corriger ces fuites est essentiel pour maintenir des applications Java robustes et performantes.
Comprendre les fuites de mémoire en Java
En Java, une fuite de mémoire signifie que les objets qui ne sont plus nécessaires sont toujours référencés, de sorte que le collecteur de déchets ne peut pas les récupérer. Contrairement aux langages comme C ou C++ où les développeurs attribuent manuellement et la mémoire libre, Java compte sur la collecte automatique de déchets pour nettoyer les objets inutilisés.
Au fil du temps, ces derniers s'accumulent, comblent le tas, ce qui fait que le GC travaille plus dur, augmente les temps de pause, se terminant éventuellement par un Érreur de mémoire. Le problème fondamental n'est pas que la collecte des ordures échoue, mais plutôt que la logique d'application maintient involontairement des références aux objets qui devraient être admissibles à la collecte.
Comment la mémoire s'écarte d'autres problèmes de mémoire
Parfois, ce qui ressemble à une fuite est juste trop d'attribution d'objets ou trop petit un tas, ou un mauvais réglage GC. Le diagnostic aide à distinguer entre ces. Une véritable fuite de mémoire montre un modèle caractéristique où l'utilisation de la mémoire de base après la collecte des ordures continue à augmenter au fil du temps, plutôt que de revenir à un niveau stable.
Il est crucial de comprendre la différence entre la croissance de la mémoire légitime et les fuites réelles. Les applications consomment naturellement plus de mémoire car elles traitent plus de données ou d'utilisateurs, mais cette croissance devrait se stabiliser ou fluctuer dans les limites prévues.
Causes communes de fuites de mémoire
Les modèles classiques de fuite comprennent des collections statiques qui grandissent indéfiniment, des enregistrements d'auditeurs sans radiations correspondantes, des variables ThreadLocal jamais supprimées, et des caches sans politiques d'expulsion. Chacun de ces modèles représente un scénario où les références persistent plus longtemps que le besoin réel des objets qu'ils pointent.
Collections statiques: Les champs statiques ont un cycle de vie qui correspond à l'application elle-même. Si un champ statique fait référence à une collection, comme une liste ou une carte, et que les objets y sont ajoutés en permanence sans jamais être enlevés, ces objets ne seront jamais admissibles à la collecte des ordures.
Ressources non fermées: Les ressources non fermées comme les connexions de base de données, les flux de fichiers ou les connexions réseau peuvent rapidement entraîner des fuites de mémoire.Ces ressources conservent souvent des références à de grands objets ou tampons qui devraient être nettoyés explicitement lorsque ce n'est plus nécessaire.
Listener et Callback Registrations:[ Les architectures animées par des événements souffrent généralement de fuites de mémoire lorsque les auditeurs ou les callbacks sont enregistrés mais jamais non enregistrés. La source d'événements maintient des références à tous les auditeurs enregistrés, les empêchant d'être ramassés des ordures même après que l'objet d'écoute n'est plus utilisé.
Threadvariables locales: ThreadLes variables locales fournissent un stockage spécifique au thread, mais elles peuvent causer des fuites de mémoire dans les environnements de pool de thread. Lorsque les threads sont réutilisés, les valeurs locales de Thread persistent à moins d'être explicitement effacées, ce qui provoque une accumulation d'objets au fil du temps.
Improper Cache Management:[ Les caches sans limites de taille ou politiques d'expulsion peuvent croître sans limite, consommant tout l'espace disponible. Même des stratégies bien intentionnées de cache peuvent devenir des fuites de mémoire s'ils ne rendent pas compte de l'invalidation du cache et de la pression de mémoire.
Reconnaître les symptômes de fuite de mémoire
La détection précoce des fuites de mémoire peut empêcher les pannes de production et la dégradation des performances. Comprendre les signes d'avertissement permet aux développeurs d'intervenir avant que les problèmes deviennent critiques.
Le modèle de Sawtooth et la base de référence en hausse
Normalement, vous vous attendez à voir un motif de dents de scie — la mémoire monte alors que l'application alloue des objets, puis chute brusquement lorsque le collecteur de déchets fonctionne. Cependant, avec une fuite, chaque goutte se situe un peu plus haut que la dernière, et au fil du temps, la base de référence s'élève vers le haut.
Un plancher en montée constante dans ce modèle est un indicateur fort d'une fuite de mémoire. Les outils de surveillance qui visualisent l'utilisation de mémoire de tas au fil du temps rendent ce modèle immédiatement apparent, permettant aux équipes d'identifier les fuites potentielles avant qu'elles ne causent des défaillances.
Augmentation de l'activité de collecte des ordures
Les GC complets fonctionnent plus souvent, mais chacun récupère moins de mémoire qu'auparavant. Cette activité accrue de GC se manifeste par des temps de pause plus longs et une utilisation plus élevée du CPU dédié à la collecte des déchets plutôt que par la logique d'application.
Lorsque le collecteur d'ordures passe plus de temps à courir mais récupère moins d'espace de tas chaque cycle, les objets divulgués s'accumulent probablement. Les applications peuvent sembler réactives au départ, mais la latence augmente progressivement à mesure que le JVM passe plus de temps à essayer de libérer la mémoire qui ne peut pas être récupérée.
Crashs d'orfèvrerie et d'application
Laissée non contrôlée, la fuite se manifeste de la manière la plus visible possible : un java.lang.OutOfMemoryError. À ce moment, le JVM est incapable de libérer assez d'espace pour continuer à affecter de nouveaux objets, et l'application s'écrase ou devient insensible.
Cette erreur indique que le collecteur de déchets ne peut pas rendre l'espace disponible pour accueillir un nouvel objet, et le tas ne peut pas être élargi davantage. Bien que OutOfMemoryError peut résulter d'exigences de mémoire légitimement élevées, dans les scénarios de fuite, il se produit même lorsque le jeu de travail réel de l'application devrait s'adapter confortablement dans le tas alloué.
Dégradation du rendement au fil du temps
Votre application Java fonctionne bien après un nouveau déploiement, mais sur des heures ou des jours, ses performances se dégradent régulièrement. Les temps de réponse s'accroissent, les pauses de collecte des ordures deviennent plus longues et plus fréquentes, et puis, l'inévitable se produit : l'application s'écrase, l'enregistrement d'un OutOfMemoryError fatal.
Cette dégradation progressive distingue les fuites de mémoire des autres problèmes de performance. Les applications qui connaissent des fuites fonctionnent généralement bien au départ, avec des problèmes émergeant seulement après un écoulement prolongé lorsque les objets divulgués s'accumulent.
Épuisement des ressources
Un autre symptôme commun lié aux fuites de mémoire est lorsque les connexions de base de données s'épuisent. Lorsque les connexions, les poignées de fichiers ou les sockets réseau sont ouverts mais jamais correctement fermés, vous allez éventuellement épuiser votre piscine de connexion. L'application commence à lancer des exceptions sur l'incapacité d'acquérir de nouvelles connexions, même si les opérations précédentes auraient dû libérer les leurs.
Outils et techniques de diagnostic
Le diagnostic efficace des fuites de mémoire nécessite les bons outils et méthodologies. Le développement Java moderne offre de nombreuses options pour surveiller l'utilisation de la mémoire et analyser le contenu de tas.
Permettre la collecte des ordures verbeuses
L'un des moyens les plus rapides d'affirmer que vous avez effectivement une fuite de mémoire est de permettre la collecte des ordures verbeuses. Les problèmes de contrainte mémoire peuvent généralement être identifiés en examinant les modèles dans la sortie de verbosegc. L'argument génère une trace à chaque fois que la collecte des ordures tourne, fournissant des informations sur les modèles de gestion de la mémoire.
Pour comprendre cette trace, vous devriez regarder les stanzas successives de l'allocation de la défaillance et chercher la mémoire libérée (octets et pourcentage) en baisse au fil du temps alors que la mémoire totale (ici, 19725304) est en augmentation.
Verbose GC loging fournit un outil de diagnostic léger et toujours disponible qui peut fonctionner en production avec des frais généraux minimes. Les journaux révèlent des modèles qui indiquent des fuites de mémoire bien avant l'écrasement des applications.
Analyse de la douille du heap
Une décharge de tas est un instantané de tous les objets contenus dans le tas à un moment précis. Les décharges de tas fournissent la vue la plus détaillée de l'utilisation de la mémoire, montrant exactement quels objets existent, combien de mémoire ils consomment, et quelles références les maintiennent en vie.
Une Dump de Java Heap est comme une photographie de la mémoire de votre application. Elle montre tous les objets présents dans la mémoire, combien d'espace ils occupent, qui les référencie, et qui ils référencent. Cet instantané complet permet aux développeurs d'identifier les causes profondes des fuites de mémoire en traçant les chaînes de rétention d'objets.
Les dumps de tas peuvent être générés sur demande en utilisant des outils comme , ou automatiquement lorsque OutOfMemoryError se produit en ajoutant l'option JVM . Par défaut, le dump de tas est créé dans un fichier appelé java pid pid .hprof dans le répertoire de travail de la VM, mais nous pouvons définir un chemin alternatif en utilisant l'option JVM -XX:HeapDumpPath=path.
Analyseur de mémoire d'éclipse (MAT)
L'analyseur de mémoire Eclipse est un analyseur Java rapide et riche en fonctionnalités qui vous aide à trouver des fuites de mémoire et à réduire la consommation de mémoire. MAT est devenu le standard de l'industrie pour l'analyse de la décharge de tas en raison de ses caractéristiques puissantes et sa capacité à gérer de grandes décharges.
L'analyseur de mémoire Eclipse (MAT) excelle dans l'analyse des décharges de tas. Son « rapport de fuites » identifie les objets susceptibles de causer des fuites en analysant les chaînes de rétention, les chemins de références qui maintiennent les objets en vie.
MAT calcule la taille retenue (mémoire un objet tient plus tout ce qu'il renvoie) et la taille peu profonde (mémoire l'objet lui-même occupe). Grandes tailles conservées indiquent des goulets d'étranglement de mémoire. Comprendre la distinction entre la taille peu profonde et la taille conservée est crucial pour identifier quels objets dominent réellement la consommation de mémoire.
En plus de ces rapports complets, Eclipse MAT prend en charge Object Query Language (OQL), qui est un langage similaire à SQL à requêter contre le dump heap. OQL permet aux requêtes sophistiquées de trouver des modèles ou types d'objets spécifiques dans des dumps heap massifs.
VisualVM
VisualVM est un outil visuel gratuit pour la surveillance, le dépannage et le profilage des applications Java. Il prend en charge l'analyse de la décharge avec une interface graphique intuitive. VisualVM fournit un point d'entrée plus accessible pour les développeurs nouveaux à l'analyse de la mémoire, avec des visualisations simples et des capacités de surveillance.
VisualVM est un outil de profilage gratuit pour Java qui a été livré avec JDK jusqu'à la version 8. Il est distribué comme une application autonome après JDK 8. Bien qu'il soit dégroupé de la JDK, VisualVM reste largement utilisé pour sa combinaison de fonctions de surveillance en temps réel et d'analyse de dump.
VisualVM fournit des filtres puissants, des chaînes de référence et des vues d'arborescence dominatrice pour comprendre quels objets consomment le plus de mémoire. Ces fonctionnalités permettent aux développeurs de naviguer sur des graphiques d'objets complexes et d'identifier des chemins de rétention qui empêchent la collecte des ordures.
Profileurs commerciaux
Les profileurs commerciaux comme YourKit et JProfiler offrent une analyse plus sophistiquée avec des frais généraux plus bas. Ils sont particulièrement utiles pour le profilage de production où minimiser l'impact sur les performances de votre code Java est critique. Ces outils fournissent des fonctionnalités avancées comme le suivi de l'allocation, le profilage CPU, et la surveillance en temps réel de la mémoire à côté de l'analyse de la décharge de tas.
Java Flight Recorder capture des données détaillées sur le temps d'exécution avec un minimum de frais généraux, ce qui le rend adapté pour la surveillance de la production toujours en cours. Cette combinaison permet un profilage continu dans les environnements de production sans impact significatif sur les performances.
Outils d'analyse heapHero et moderne
HeapHero est un analyseur de dump qui vous aide à identifier rapidement les problèmes de mémoire dans les applications Java et Android. Les outils modernes d'analyse basés sur le cloud comme HeapHero offrent des avantages par rapport aux applications de bureau traditionnelles, y compris la capacité d'analyser de très grandes dumps de tas sans nécessiter de matériel local puissant.
HeapHero analyse les décharges de tas pour mettre en évidence les fuites de mémoire, détecter les structures de données inefficaces, trouver des objets et des chaînes de caractères en double, et calculer combien de mémoire est gaspillée.
Outils d'analyse statique
Des outils d'analyse statiques comme FindBugs ou SonarQube peuvent également aider à attraper des fuites de mémoire potentielles dans votre code. Bien qu'ils ne capturent pas tout, ils peuvent identifier des modèles communs qui conduisent à des fuites, comme ne pas fermer les ressources, utiliser des champs statiques incorrectement, ou ne pas enregistrer les auditeurs.
L'analyse statique offre une approche proactive pour prévenir les fuites de mémoire en identifiant les profils problématiques pendant le développement, avant que le code n'arrive à la production.
Analyser les égouts de heap : une approche étape par étape
L'analyse réussie des décharges de tas nécessite une approche systématique. Comprendre comment naviguer les données et identifier les modèles problématiques sépare le débogage efficace de l'exploration sans but.
Génération de douilles de heap
Avant de commencer l'analyse, vous devez capturer une décharge de tas. Il existe plusieurs méthodes pour générer des décharges de tas, chacune adaptée à différents scénarios:
Génération automatique sur OutOfMemoryError: Un argument JVM peut être ajouté pour générer un dump de tas chaque fois qu'un OutOfMemoryError se produit. L'option -XX:+HeapDumpOnOutOfMemoryError peut être ajoutée pour générer un dump de tas sur OutOfMemoryError. Cela vous assure de capturer l'état de l'application au moment de l'échec.
Génération manuelle avec jmap: L'utilitaire , inclus avec le JDK, permet une génération de dumps sur demande. Ceci est utile lorsque vous soupçonnez une fuite mais n'avez pas encore connu un OutOfMemoryError. La commande génère une dump d'objets en direct seulement.
Programmatic Generation: Les applications peuvent générer des décharges de tas programmatiquement en utilisant le HotSpotDiagnosticMXBean, permettant une logique personnalisée pour déclencher des décharges en fonction des conditions ou des paramètres spécifiques à l'application.
Ouverture et analyse initiale
Ouvrez le dump de tas dans Eclipse Memory Analyzer en utilisant l'option File --> Open Heap Dump. Premièrement, il vous incitera à créer un rapport de fuite suspect. L'utilisateur peut le créer ou le sauter. Le rapport de fuite suspects fournit une analyse automatisée qui identifie souvent les problèmes les plus évidents immédiatement.
Les parties les plus informatives sont les « Classes par nombre d'instances » et « Classes par taille d'instances ». La première montre les 5 classes les plus importantes avec les plus d'instances créées, tandis que la seconde montre les 5 classes les plus consommatrices de mémoire la plus lourde.
Utilisation de la vue histogramme
L'histogramme affiche toutes les instances d'objet triées par leurs noms de classe. Il vous aide à identifier les classes avec le plus d'instances. Recherchez des classes avec des nombres d'instances inattendues, ce qui pourrait indiquer une fuite de mémoire.
L'histogramme fournit une vue d'oiseau de tous les objets dans le tas, triés par classe. Les développeurs devraient chercher des classes spécifiques à l'application avec des nombres d'instances étonnamment élevés ou consommation de mémoire. Les classes système comme les tableaux à chaîne ou octets dominent souvent par nombre, mais les classes d'application avec des milliers ou des millions d'instances justifient une enquête.
Examen des arbres dominateurs
La vue sur l'arbre dominateur de MAT montre quels objets gardent le plus de mémoire en vie, tandis que sa fonction chemin-à-GC-roots révèle pourquoi des objets spécifiques ne peuvent pas être recueillis. L'arbre dominateur organise les objets par leur taille conservée, montrant quels objets, si les ordures recueillies, libéreraient le plus de mémoire.
Comprendre les dominateurs est la clé d'une analyse efficace du tas. Un objet X domine l'objet Y si chaque chemin d'une racine de collecte d'ordures à Y doit passer par X. Cela signifie que si X ont été recueillis, Y deviendrait également admissible à la collecte. L'arbre dominateur révèle ces relations, mettant en évidence les objets qui contrôlent vraiment la rétention de mémoire.
Tracer les chemins vers les racines du GC
Une fois que vous avez identifié des objets suspects, la prochaine étape est de comprendre pourquoi ils restent en mémoire. Tracer le chemin d'un objet à ses racines de collecte des ordures révèle la chaîne de référence empêchant la collecte.
Les racines GC comprennent les champs statiques, les fils actifs, les références JNI et d'autres objets que le JVM considère comme accessibles par nature. Tout objet accessible à partir d'une racine GC ne peut pas être recueilli. En examinant ces chemins, les développeurs peuvent identifier exactement quelles références doivent être effacées pour permettre la collecte des ordures.
Comparaison de douilles multiples de heap
Comparer Douilles de tas multiples : analyser les décharges de tas prises à différents moments pour identifier les modèles de croissance ou les tendances de rétention d'objets.
Prendre des décharges de tas à intervalles réguliers (par exemple, toutes les heures pendant un test de charge) et les comparer montre quels types d'objets sont en croissance. Les classes dont l'instance compte ou la consommation de mémoire augmentent linéairement avec le temps sont les principaux suspects de fuite.
Les modèles et les solutions de fuite de mémoire commune
Comprendre les profils de fuite communs aide les développeurs à reconnaître et à résoudre les problèmes plus rapidement. Chaque profil a des symptômes caractéristiques et des solutions établies.
Collecte statique fuites
Si les champs statiques contiennent des références à des objets, ces objets ne seront jamais admissibles à la collecte des ordures. Ceci est problématique lorsque les caches statiques, les monotones ou des motifs similaires gardent les objets autour longtemps après qu'ils soient nécessaires.
Les collections statiques sont particulièrement dangereuses parce qu'elles persistent pendant tout le cycle de vie de l'application. Un modèle commun est l'utilisation d'une carte statique pour mettre en cache les données, mais jamais en supprimant les entrées quand elles deviennent inexistantes ou inutiles.
Exemple du problème:
public class UserCache {
private static Map<String, User> cache = new HashMap<>();
public static void cacheUser(User user) {
cache.put(user.getId(), user);
// No removal logic - users accumulate forever
}
}
Solution: Assurez-vous que les champs statiques ne contiennent pas de références inutiles. Si vous utilisez des caches ou des singlets, nettoyez toujours les objets qui ne sont plus nécessaires pour libérer la mémoire.
Mettre en place une gestion du cache appropriée avec des limites de taille, une expiration basée sur le temps, ou utiliser des références faibles.
public class UserCache {
private static Map<String, User> cache = new LinkedHashMap<>(100, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 100; // Limit cache to 100 entries
}
};
}
Fuites de ressources non fermées
Si les ressources ne sont pas fermées correctement, elles se maintiendront sur les références aux objets, empêchant la collecte des ordures. Par exemple, une connexion ouverte à une base de données peut garder une rangée entière de données en mémoire.
Les ressources comme les connexions de base de données, les flux de fichiers, les sockets réseau et les lecteurs/auteurs doivent être explicitement fermés.
Exemple du problème:
public void readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
String line = reader.readLine();
// Process line...
// Reader never closed - resource leak
}
Solution: Utilisez toujours l'instruction essai avec ressources ou assurez-vous un nettoyage approprié dans les blocs finals.
public void readFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String line = reader.readLine();
// Process line...
} // Reader automatically closed
}
L'instruction Try-With-Resources, introduite dans Java 7, ferme automatiquement les ressources qui implémentent AutoFermable, assurant le nettoyage même si des exceptions se produisent. Ce modèle doit être utilisé pour toute la gestion des ressources.
Éclats d'écoute et de rappel
Les auditeurs d'événements et les callbacks sont des sources communes de fuites de mémoire dans les applications GUI, les systèmes d'événements et les implémentations de modèles d'observateurs.
Considérez une application web qui enregistre les auditeurs de session mais ne les désenregistre jamais – chaque session reste en mémoire indéfiniment, même après que l'utilisateur se soit déconnecté.
Exemple du problème:
public class EventSource {
private List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
// No removeListener method - listeners accumulate
}
}
Solution: Supprimez explicitement les auditeurs et les rappels lorsqu'ils ne sont plus nécessaires.
public class EventSource {
private List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
}
public void removeListener(EventListener listener) {
listeners.remove(listener);
}
}
// In the listener's cleanup code:
eventSource.removeListener(this);
Alternativement, utilisez des références faibles pour les auditeurs, leur permettant d'être ramassés même si elles ne sont pas explicitement enlevées.
FilFeuilles de fuite locales
Les variables ThreadLocal fournissent un stockage spécifique au thread, mais elles peuvent causer des fuites de mémoire graves dans les applications utilisant des pools de thread. Lorsque les threads sont réutilisés (comme ils le sont dans la plupart des applications du serveur), les valeurs ThreadLocal persistent dans différentes requêtes ou tâches.
Exemple du problème:
public class RequestContext {
private static ThreadLocal<UserSession> session = new ThreadLocal<>();
public static void setSession(UserSession s) {
session.set(s);
// Never removed - accumulates in thread pool threads
}
}
Solution: Effacer les variables locales dans les blocs finals.
public class RequestContext {
private static ThreadLocal<UserSession> session = new ThreadLocal<>();
public static void setSession(UserSession s) {
session.set(s);
}
public static void clearSession() {
session.remove();
}
}
// In request handling code:
try {
RequestContext.setSession(userSession);
// Process request...
} finally {
RequestContext.clearSession();
}
Toujours effacer les variables ThreadLocal quand elles ne sont plus nécessaires, particulièrement à la fin du traitement des demandes dans les applications Web. De nombreux cadres fournissent des filtres ou des intercepteurs spécifiquement pour le nettoyage ThreadLocal.
Cache sans politiques d'éviction
Les caches améliorent les performances en stockant les données fréquemment accessibles en mémoire, mais sans une gestion adéquate, elles deviennent des fuites de mémoire.
Solution:[ Utilisez des références faibles pour les caches afin que les objets puissent être recueillis lorsque la pression de la mémoire augmente.
Les bibliothèques modernes de cache offrent des stratégies d'expulsion sophistiquées, notamment :
- Évitement basé sur le calibre:[ Limiter le cache à un nombre maximum d'entrées ou la taille totale de la mémoire
- Élimination fondée sur le temps:[ Supprimer les entrées après une durée ou une période d'inactivité déterminée
- Exemption fondée sur les références:[ Utiliser des références faibles ou douces pour permettre la collecte des ordures sous pression mémoire
- LRU (Least Recently Used):[ Evitez les entrées les moins récentes lorsque le cache atteint la capacité
// Using Caffeine cache with size and time-based eviction
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
Structures de fuite spécifiques
Apache Tomcat – Les fuites de mémoire des connexions JDBC ne sont pas correctement fermées dans les applications à long terme. Cadre de printemps – ApplicationContext tenir les haricots plus longtemps que nécessaire en raison des références circulaires.
La compréhension des modèles spécifiques au cadre permet de diagnostiquer les fuites plus rapidement. Par exemple, les applications de printemps peuvent fuiter la mémoire par:
- Fèves à champ de prototype référencées par des fèves à simpleton
- ApplicationContexte non correctement fermé dans les scénarios d'essai
- Portées personnalisées sans nettoyage approprié
- Auditeurs d'événements enregistrés mais jamais non enregistrés
Scénarios avancés de fuite de mémoire
Au-delà des modèles communs, certains scénarios de fuite de mémoire nécessitent une compréhension plus approfondie des internes de JVM et de l'architecture d'application.
Fuites directes de mémoire tampon
Les tampons directs créent un défi particulier de gestion de la mémoire. L'API Java NIO cache un ByteBuffer direct de taille maximale pour chaque fil, qui ressemble à une fuite de mémoire native si vous lisez ou écrivez de gros blocs de nombreux fils. Cette cache par fil peut consommer des gigaoctets de mémoire native invisibles pour la surveillance du tas.
Les symptômes incluent RSS (taille de jeu résidente) dépassant de loin la taille du tas, et mystérieux OutOfMemoryError: mémoire tampon direct malgré la disponibilité de l'espace de tas.
Solution: Le paramètre -XX:MaxDirectMemorySize limite l'allocation directe des tampons. Sans cela, les tampons directs peuvent consommer toute la mémoire native disponible. Définissez ce paramètre en fonction des modèles d'E/S de votre application – si vous utilisez de nombreux grands tampons directs, augmentez la limite; si vous les utilisez rarement, limitez-les pour empêcher la consommation de mémoire native fugueuse.
Fuites liées à la finalisation
Une autre source potentielle de cette erreur se produit avec des applications qui font une utilisation excessive des finalistes. Si une classe a une méthode de finalisation, alors les objets de ce type ne font pas récupérer leur espace au moment de la collecte des ordures. Au lieu de cela, après la collecte des ordures, les objets sont en file d'attente pour la finalisation, qui se produit à un moment ultérieur.
Un scénario qui peut causer cette situation est qu'une application crée des threads hautement prioritaires qui font augmenter la file d'attente à un rythme plus rapide que celui auquel le thread finalisateur assure le service de cette file d'attente.
Solution: Évitez d'utiliser des finalistes. Java moderne offre de meilleures alternatives comme essayer avec les ressources et l'API plus propre introduit dans Java 9. Si la finalisation est inévitable, surveillez la file d'attente de finalisation et assurez-vous qu'elle ne se développe pas sans limites.
Fuites de chargeuses de classe
Les fuites de chargeurs de classe sont particulièrement problématiques dans les serveurs d'applications qui supportent le déploiement à chaud. Lorsqu'une application est redéployée, l'ancien chargeur de classe doit être ramassé avec toutes les classes qu'il charge. Toutefois, si une référence à une classe ou à un objet de l'ancien déploiement reste, l'ensemble du chargeur de classe et toutes ses classes sont conservés.
Les causes communes de fuites de chargeuses de classe sont les suivantes:
- ThreadDivers locaux contenant des références aux classes d'application
- Les fils ont commencé par l'application mais ne sont pas arrêtés pendant le non-déploiement
- Références statiques dans les bibliothèques aux classes d'applications
- Conducteurs JDBC enregistrés mais non radiés
- Cadres de logage contenant des références aux classes d'application
Les fuites de chargeurs de classe peuvent être particulièrement graves parce qu'ils conservent non seulement des objets individuels, mais aussi des définitions de classe entière et tous les champs statiques, consommant potentiellement des centaines de mégaoctets par déploiement.
Stratégies de prévention et pratiques exemplaires
La prévention des fuites de mémoire est beaucoup plus efficace que le diagnostic et la fixation dans la production. Adopter des pratiques de codage défensives et des modèles architecturaux réduit significativement le risque de fuite.
Code de discipline
Les stratégies de prévention comprennent la discipline du codage. L'établissement et le suivi de modèles uniformes de gestion des ressources, d'enregistrement des auditeurs et de gestion du cache empêchent les scénarios de fuite les plus courants.
Ne jamais stocker les collections dans des champs statiques sans limites de taille. Cette règle simple empêche l'un des modèles de fuite les plus courants. Toute collecte statique devrait avoir des limites de taille explicites, des politiques d'expulsion, ou utiliser des références faibles.
Utilisation de références faibles et douces
Java fournit plusieurs types de référence au-delà de références fortes qui permettent une gestion de mémoire plus sophistiquée:
- Faisceaux Références:[ Les objets référencés sont collectés à la prochaine collecte des ordures, peu importe la disponibilité de la mémoire.
- Soft Références: Les objets référencés sont collectés doucement seulement lorsque la mémoire est nécessaire. Le JVM garde des références douces aussi longtemps que possible, ce qui les rend idéales pour les caches sensibles à la mémoire.
- Fantôme Références: Utilisé pour les actions de nettoyage, les références fantômes permettent l'exécution de code après qu'un objet devient inaccessible mais avant que sa mémoire ne soit récupérée.
WeakHashMap fournit une implémentation de carte où les clés sont maintenues faiblement, en supprimant automatiquement les entrées lorsque les clés ne sont plus référencées ailleurs. Ceci est utile pour associer des métadonnées avec des objets sans empêcher leur collection.
Test automatisé pour les fuites de mémoire
Test avec charge de travail de longue durée : les tests unitaires ne captent pas les fuites ; il faut des tests d'intégration ou des simulations de longue durée.
Les stratégies efficaces d'essai des fuites comprennent :
- Test de fuite:[ Exécuter l'application sous une charge réaliste pendant de longues périodes (heures ou jours) pendant la surveillance de l'utilisation de la mémoire
- Comparaison des dépôts de hépaches: Prendre des dépôts de hépaches à intervalles réguliers pendant les essais et les comparer pour identifier les populations d'objets en croissance
- Profilage de mémoire dans CI/CD: Intégrer le profilage de mémoire dans des pipelines d'intégration continue pour attraper les fuites avant la production
- Analyse automatisée du tas:[ Utiliser des outils qui peuvent analyser automatiquement les décharges de tas et les constructions de échecs si des motifs suspects sont détectés
Surveillance et alerte
Si vous voyez le jeu en direct -- la quantité de mémoire encore en usage après une collecte complète -- qui augmente régulièrement au fil du temps, c'est un signal clair. Les applications saines maintiennent un jeu en direct relativement stable, tandis que les applications en fuite montrent une hausse de la base de données qui ne revient jamais aux niveaux précédents.
Mettre en œuvre un suivi et un alerte pour :
- Tendances de l'utilisation du heap au fil du temps
- Niveaux de mémoire post-GC (le "ensemble en direct")
- Fréquence et durée de collecte des ordures
- Fréquence de GC complète
- Utilisation de la mémoire native (pour les fuites de tampons directs)
Les outils modernes de surveillance de la performance des applications (APM) permettent de détecter les fuites de mémoire sophistiquées, d'identifier automatiquement les niveaux de référence en hausse et les équipes d'alerte avant que des erros de sortie de mémoire ne se produisent.
Domaines d'intérêt de la révision du code
Les examens de codes devraient viser spécifiquement les profils de fuites communs :
- Collectes statiques sans limites de taille ni politiques d'expulsion
- Acquisition de ressources sans essai-avec-ressources correspondantes ou en fin de compte blocs
- Inscription de l'auditeur sans radiation correspondante
- ThreadUtilisation locale sans nettoyage
- Mise en œuvre de caches sans stratégie d ' expulsion
- Objets à longue durée de vie contenant des références à des objets à courte durée de vie
Études de cas sur le monde réel
L'examen des scénarios de fuites de mémoire dans le monde réel fournit des informations précieuses sur la façon dont les fuites se manifestent et comment elles peuvent être résolues.
Étude de cas : Fuite de la séance d'applications Web
Une application web de production a connu une croissance progressive de la mémoire sur plusieurs jours, nécessitant éventuellement des redémarrages quotidiens. L'analyse de la décharge de Heap a révélé des milliers d'objets HttpSession restant en mémoire longtemps après que les utilisateurs se soient déconnectés.
Root Cause:[ L'application a enregistré des auditeurs de session pour suivre les utilisateurs actifs mais ne les a jamais retirés d'une collection statique lorsque les sessions ont expiré. Chaque objet de session conservait des références aux données utilisateur, fichiers téléchargés et autres attributs de session.
Solution:[ Implémenté le nettoyage de l'auditeur de session dans la méthode sessionDestroyé, en supprimant les entrées de la collection de suivi lorsque les sessions ont expiré. L'utilisation de la mémoire s'est stabilisée et l'application a fonctionné pendant des semaines sans nécessiter de redémarrage.
Étude de cas: Épuisement du bassin de connexion à la base de données
Un microservice a commencé à lancer des exceptions « Impossible d'obtenir la connexion » après avoir couru pendant plusieurs heures, malgré avoir une piscine de connexion configurée avec 50 connexions.
Cause de la panne : Le code de traitement des exceptions dans les méthodes d'accès aux données n'a pas fermé les connexions lorsque des erreurs se sont produites. Les blocs de capture d'essais ont pris des exceptions, mais n'ont pas inclus les blocs pour assurer la fermeture de la connexion.
Solution:[ Refactorisé tous les codes d'accès aux données pour utiliser des ressources d'essai, assurant que les connexions sont toujours retournées au pool, que les opérations aient réussi ou échoué.
Étude de cas: Accumulation locale de fils dans un bassin de fils
Un service API à haut débit a montré une utilisation de mémoire en augmentation constante malgré la gestion d'un taux de requête constant.
Cause de la requête : L'application utilisait des variables ThreadLocal pour stocker les informations de contexte de la demande, les rendant disponibles tout au long de la chaîne de traitement des demandes. Cependant, le ThreadLocal n'a jamais été effacé après l'achèvement de la demande.
Solution: Implémenté un filtre de servlet qui a effacé toutes les variables ThreadLocal dans un bloc final après le traitement des demandes terminée. L'utilisation de la mémoire s'est immédiatement stabilisée aux niveaux attendus.
Impact de la performance des fuites de mémoire
Les fuites de mémoire ne causent pas seulement des irritations de l'outOfMemory, mais elles dégradent les performances bien avant que les applications ne s'écrasent.
Augmentation de la collecte des déchets
Le service peut encore sembler réactif, mais la latence commence à s'infiltrer à mesure que les pauses du GC s'allongent. Les équipes d'exploitation remarquent souvent que les temps de réponse sont laborieux pendant la charge maximale ou les pics soudains dans l'utilisation du processeur liés à l'activité du GC.
À mesure que les objets fuient s'accumulent, le collecteur de déchets doit scanner des graphiques d'objets de plus en plus grands pour identifier les objets collectables, ce qui augmente la fréquence et la durée de la collecte des déchets, ce qui a un impact direct sur la réactivité de l'application.
Limite de dépassement de la tête de la GC
Le message de détail que la limite de frais généraux de GC a dépassé indique que le collecteur d'ordures (GC) fonctionne la plupart du temps, et l'application Java progresse très lentement. Cette erreur survient lorsque le JVM passe plus de 98 % de son temps à la collecte des ordures et récupère moins de 2 % de l'espace de tas.
Cet état représente une spirale de mort où l'application devient essentiellement non fonctionnelle, passant presque tout le temps CPU essayant de libérer la mémoire plutôt que de traiter les requêtes. Il précède souvent OutOfMemoryError par des minutes ou des heures.
Impact sur le débit de demande
Les fuites de mémoire réduisent le débit d'application de plusieurs façons :
- Arrêts du monde : La plupart des algorithmes de collecte des ordures nécessitent l'arrêt des fils d'application pendant la collecte, réduisant directement le débit
- Constatation du CPU: La collecte des ordures consomme des cycles du CPU qui pourraient autrement traiter les demandes
- Pollution par les caches:[ Les objets laissés en fuite occupent un espace de tas qui pourrait être utilisé pour la mise en cache utile, réduisant les taux de frappe du cache
- Taux d'attribution accru:[ À mesure que le tas se remplit, le JVM peut déclencher des collections plus fréquentes de jeunes générations
Outils Guide de comparaison et de sélection
Le choix de l'outil approprié pour le diagnostic des fuites de mémoire dépend de vos besoins, de votre environnement et de vos contraintes spécifiques.
Quand utiliser chaque outil
Android Studio Profiler est idéal pour la surveillance en temps réel d'une application androïde. VisualVM est un outil simple et léger qui est idéal pour un rapide regard sur un programme en cours d'exécution. JDK Mission Control est utile pour des informations plus approfondies sur un JVM en cours d'exécution. Eclipse MAT est un bon choix pour la plupart des tâches d'analyse de dump, mais manque certaines des fonctionnalités utiles de HeapHero.
Pour le diagnostic rapide: VisualVM fournit le chemin le plus rapide vers les idées de mémoire de base. Sa nature légère et son interface intuitive le rendent idéal pour les enquêtes initiales ou lorsque vous avez besoin de réponses rapides.
Pour l'analyse approfondie: Eclipse MAT reste la norme d'or pour l'analyse complète des fuites de tas.
Pour la surveillance de la production: Java Flight Recorder with Mission Control fournit un profilage continu et peu encombrant adapté aux environnements de production. Sa capacité à capturer des données détaillées sur les temps d'exécution sans impact significatif sur les performances rend ce profilage inestimable pour le dépannage de la production.
Pour la collaboration d'équipe: HeapHero est un bon choix pour l'analyse approfondie, les suggestions d'apprentissage automatique, le partage de rapports interactifs au sein de l'équipe, et l'intégration de l'analyse du dump de tas dans les workflows automatisés via les API REST.
Limites et considérations des outils
Sa capacité à analyser les grandes décharges dépend de la RAM disponible sur la machine où elle est installée. Eclipse MAT nécessite une mémoire importante pour analyser les grandes décharges de tas – exigeant souvent que les décharges de tas soient analysées sur des machines avec plus de RAM que l'application elle-même utilise.
Analyser sur une machine avec une mémoire suffisante : les dumps de tas peuvent être grands ; utiliser une machine avec suffisamment de RAM pour manipuler les outils d'analyse en douceur. Planifier une infrastructure d'analyse qui peut gérer vos plus grands dumps de tas attendus, potentiellement nécessitant des serveurs d'analyse dédiés.
Tendances et orientations futures
La détection et la prévention des fuites de mémoire continuent d'évoluer grâce à de nouveaux outils, techniques et améliorations du JVM.
Analyse de l'apprentissage automatique
Recommandations de Machine Learning Powered : HeapHero utilise ML pour identifier automatiquement les suspects, tels que des graphiques d'objets étonnamment grands ou des duplicatas excessifs.
Les modèles d'apprentissage automatique formés sur des milliers de décharges de tas peuvent reconnaître des modèles qui indiquent des types de fuites spécifiques, fournissant des recommandations plus précises et plus exploitables que l'heuristique traditionnelle.
Profil de mémoire continue
L'analyse traditionnelle des décharges de tas est réactive : les problèmes doivent se poser avant que les décharges ne soient capturées et analysées.
Des outils comme Java Flight Recorder permettent de profiler toujours en production, captant en permanence les schémas d'allocation et les cycles de vie des objets. Ces données permettent aux équipes d'identifier les tendances de la mémoire avant qu'elles ne deviennent des problèmes critiques.
Collecteurs améliorés de déchets
Les collecteurs modernes comme ZGC et Shenandoah offrent des temps de pause extrêmement faibles, réduisant l'impact de performance des fuites de mémoire. Bien qu'ils n'empêchent pas les fuites, ils rendent les applications plus résistantes à la croissance progressive de la mémoire en maintenant la réactivité même au fur et à mesure que l'utilisation augmente.
Ces collecteurs fournissent également de meilleures informations diagnostiques, ce qui facilite l'identification des cas où la mémoire est conservée inutilement.
Flux de travail pratique pour l'enquête sur les fuites de mémoire
L'établissement d'un flux de travail systématique pour l'étude des fuites de mémoire améliore l'efficacité et assure une analyse approfondie.
Étape 1: Confirmer la fuite
Avant d'investir des efforts importants dans l'analyse des décharges de tas, confirmez qu'il existe une véritable fuite de mémoire :
- Surveiller l'utilisation du tas au fil du temps, à la recherche de la caractéristique de la hausse de la base
- Activer la logation de CG verbose et examiner les modèles
- Vérifier que la croissance de la mémoire n'est pas simplement due à une charge ou un volume de données accrus
- Vérifier que la taille du tas est configurée de manière appropriée pour les besoins de l'application
Étape 2: Capturer les données diagnostiques
Recueillir des informations diagnostiques complètes:
- Prendre plusieurs décharges de tas à différents points dans le temps
- Capturer des journaux GC couvrant la période de croissance de la mémoire
- Mesure des applications enregistrées (taux de demande, volumes de données, nombre d'utilisateurs)
- Documenter tout changement de code ou événement de déploiement récent
Étape 3 : Analyser les douilles de heap
Analyser systématiquement les décharges de tas capturées :
- Commencez par les rapports automatisés de suspects de fuite
- Examiner l'histogramme pour les populations d'objets de grande taille inattendue
- Utilisez l'arborescence dominatrice pour identifier les objets contrôlant le plus de mémoire
- Tracez les chemins vers les racines de GC pour les objets suspects
- Comparer plusieurs décharges pour identifier les types d'objets croissants
Étape 4: Identifier la cause racine
Traduire les résultats du dump en causes profondes au niveau du code :
- Identifier le code qui crée les objets divulgués
- Comprendre pourquoi les références à ces objets persistent
- Déterminer la référence dans le chemin racine du GC à supprimer
- Vérifier la cause racine par l'examen du code
Étape 5 : Mettre en œuvre et vérifier la correction
Élaborer, tester et vérifier la correction:
- Mettre en œuvre la correction suivante :
- Ajouter des tests qui auraient pu attraper la fuite
- Effectuer des essais de stabilisation pour vérifier que la fuite est résolue
- Surveiller la production après déploiement pour confirmer la correction
Documentation et partage des connaissances
Constatations du document : Conservez des notes détaillées des constatations au cours de l'analyse pour faciliter le dépannage et le partage des connaissances.
Maintenir une base de connaissances sur :
- Les profils de fuites et leurs solutions précédemment rencontrés
- Scénarios de fuite spécifiques au cadre
- Techniques d'analyse du dépotage des hépaches qui se sont révélées efficaces
- Configurations d'outils et meilleures pratiques
Cette documentation accélère les recherches futures et aide les membres de l'équipe à tirer des leçons des expériences passées.
Intégration avec le flux de travail de développement
La prévention et la détection des fuites de mémoire devraient être intégrées tout au long du cycle de développement, et non pas considérées comme une activité de lutte contre l'incendie de production.
Phase de développement
- Utiliser des plugins IDE qui détectent les profils de fuite courants
- Lancer des outils d'analyse statique dans le cadre du processus de construction
- Suivre les normes de codage qui empêchent les scénarios de fuites communs
- Réexamen des codes en mettant l'accent sur la gestion des ressources
Phase d'essai
- Inclure des essais à long terme dans la suite d'essais
- Surveiller l'utilisation de la mémoire pendant les essais d'intégration
- Effectuer des tests de charge avec profilage de mémoire activé
- Comparer les décharges de tas avant et après les essais
Phase de production
- Mettre en œuvre un suivi et une alerte complets de la mémoire
- Activer la génération automatique de dump sur OutOfMemoryError
- Utiliser des outils de profilage continus avec des frais généraux bas
- Établir des runbooks pour répondre aux alertes mémoire
Conclusion
Les fuites de mémoire Java sont une menace sérieuse pour la stabilité et la performance de l'application. Bien que le collecteur d'ordures gère une grande partie de la complexité de la gestion de la mémoire, ce n'est pas une balle argent.
Les fuites de mémoire ne sont pas seulement des nuisances, elles peuvent dégrader silencieusement les performances et causer des échecs de production. En reconnaissant les modèles, en utilisant une gestion appropriée du cycle de vie, et en utilisant des outils de détection, vous pouvez prévenir la plupart des fuites.
La gestion réussie des fuites de mémoire nécessite une approche multiforme combinant des pratiques de codage préventif, des tests complets, une surveillance efficace et des techniques de diagnostic systématiques. Comprendre les profils de fuites communs, maîtriser les outils d'analyse des décharges de tas et établir des flux de travail d'investigation clairs permet aux équipes de développement de maintenir des applications Java robustes et performantes.
L'investissement dans la prévention et la détection des fuites de mémoire est bénéfique en améliorant la stabilité des applications, en améliorant les performances, en réduisant les incidents de production et en réduisant les coûts d'infrastructure.
Pour plus de détails sur l'optimisation des performances et la gestion de la mémoire Java, explorez la documentation officielle de réglage d'Oracle JVM[, le projet Éclipse Memory Analyzer et Baeldung's complet Java tutorials. De plus, la référence Java Virtual Machine options fournit des informations détaillées sur la configuration de JVM pour la gestion de la mémoire, tandis que Netdata's monitoring platform offre des informations en temps réel sur les performances d'application et les modèles d'utilisation de la mémoire.