La gestion de la mémoire de Java, avec son ramasse-miettes intégré, est l’une des plus belles réalisations du langage. Elle permet aux développeurs de créer de nouveaux objets sans se soucier explicitement de l’allocation et de la désallocation de la mémoire, car le ramasseur de déchets récupère automatiquement la mémoire pour la réutiliser. Cela permet un développement plus rapide avec moins de code passe-partout, tout en éliminant les fuites de mémoire et autres problèmes liés à la mémoire. Du moins en théorie.
Ironiquement, le garbage collector de Java semble fonctionner trop bien, créant et supprimant trop d’objets. La plupart des problèmes de gestion de la mémoire sont résolus, mais souvent au prix de la création de graves problèmes de performance. Le fait de rendre le garbage collection adaptable à toutes sortes de situations a conduit à un système complexe et difficile à optimiser. Pour faire le tour de la collecte des ordures, il faut d’abord comprendre comment fonctionne la gestion de la mémoire dans une machine virtuelle Java (JVM).
Comment fonctionne réellement la collecte des ordures Java
Beaucoup de gens pensent que la collecte des ordures collecte et élimine les objets morts. En réalité, le ramassage des ordures de Java fait le contraire ! Les objets vivants sont suivis et tout le reste a désigné les ordures. Comme vous le verrez, cette incompréhension fondamentale peut entraîner de nombreux problèmes de performance.
Débutons par le tas, qui est la zone de mémoire utilisée pour l’allocation dynamique. Dans la plupart des configurations, le système d’exploitation alloue le heap à l’avance pour qu’il soit géré par la JVM pendant l’exécution du programme. Cela a quelques ramifications importantes :
- La création d’objets est plus rapide car la synchronisation globale avec le système d’exploitation n’est pas nécessaire pour chaque objet. Une allocation revendique simplement une certaine partie d’un tableau de mémoire et déplace le pointeur de décalage vers l’avant (voir la figure 2.1). L’allocation suivante commence à ce décalage et revendique la portion suivante du tableau.
- Lorsqu’un objet n’est plus utilisé, le ramasseur de déchets récupère la mémoire sous-jacente et la réutilise pour les futures allocations d’objets. Il n’y a donc pas de suppression explicite et aucune mémoire n’est rendue au système d’exploitation.
Figure 2.1 : Les nouveaux objets sont simplement alloués à la fin du heap utilisé.
Tous les objets sont alloués sur la zone de heap gérée par la JVM. Chaque élément que le développeur utilise est traité de cette manière, y compris les objets de classe, les variables statiques et même le code lui-même. Tant qu’un objet est référencé, la JVM le considère comme vivant. Dès qu’un objet n’est plus référencé et n’est donc plus accessible par le code de l’application, le ramasseur de déchets le supprime et récupère la mémoire inutilisée. Aussi simple que cela puisse paraître, cela soulève une question : quelle est la première référence dans l’arbre ?
Les racines du ramasse-miettes – la source de tous les arbres d’objets
Tout arbre d’objets doit avoir un ou plusieurs objets racines. Tant que l’application peut atteindre ces racines, l’ensemble de l’arbre est atteignable. Mais quand ces objets racines sont-ils considérés comme atteignables ? Des objets spéciaux appelés racines de collecte de déchets (racines GC ; voir la figure 2.2) sont toujours atteignables et il en est de même pour tout objet qui possède une racine de collecte de déchets à sa propre racine.
Il existe quatre types de racines GC en Java :
- Les variables locales sont maintenues en vie par la pile d’un thread. Il ne s’agit pas d’une référence virtuelle d’objet réel et n’est donc pas visible. À toutes fins utiles, les variables locales sont des racines GC.
- Les threads Java actifs sont toujours considérés comme des objets vivants et sont donc des racines GC. Ceci est particulièrement important pour les variables locales de threads.
- Les variables statiques sont référencées par leurs classes. Ce fait en fait des racines GC de facto. Les classes elles-mêmes peuvent être garbage-collectées, ce qui supprimerait toutes les variables statiques référencées. Ceci est particulièrement important lorsque nous utilisons des serveurs d’applications, des conteneurs OSGi ou des chargeurs de classes en général. Nous aborderons les problèmes connexes dans la section Patrons de problèmes.
- Les références JNI sont des objets Java que le code natif a créés dans le cadre d’un appel JNI. Les objets ainsi créés sont traités de manière particulière car la JVM ne sait pas si elle est référencée par le code natif ou non. De tels objets représentent une forme très spéciale de racine GC, que nous examinerons plus en détail dans la section Patrons de problèmes ci-dessous.
Figure 2.2 : Les racines GC sont des objets qui sont eux-mêmes référencés par la JVM et empêchent ainsi tout autre objet d’être ramassé à la poubelle.
Par conséquent, une application Java simple possède les racines GC suivantes :
- Variables locales dans la méthode main
- Le thread principal
- Variables statiques de la classe main
Marquage et balayage des ordures
Pour déterminer quels objets ne sont plus utilisés, la JVM exécute par intermittence ce que l’on appelle très justement un algorithme de marquage et de balayage. Comme vous pouvez l’imaginer, il s’agit d’un processus simple en deux étapes :
- L’algorithme parcourt toutes les références d’objets, en commençant par les racines GC, et marque chaque objet trouvé comme vivant.
- Toute la mémoire du tas qui n’est pas occupée par des objets marqués est récupérée. Elle est simplement marquée comme libre, essentiellement balayée sans objets inutilisés.
La collecte d’ordures est destinée à supprimer la cause des fuites de mémoire classiques : les objets inaccessibles mais non supprimés en mémoire. Cependant, cela ne fonctionne que pour les fuites de mémoire au sens premier du terme. Il est possible d’avoir des objets inutilisés qui sont toujours accessibles par une application parce que le développeur a simplement oublié de les déréférencer. De tels objets ne peuvent pas être collectés. Pire encore, une telle fuite de mémoire logique ne peut être détectée par aucun logiciel (voir la figure 2.3). Même le meilleur logiciel d’analyse ne peut que mettre en évidence les objets suspects. Nous examinerons l’analyse des fuites de mémoire dans la section Analyser l’impact de l’utilisation de la mémoire et du Garbage Collection sur les performances, ci-dessous.
.