web-dev-qa-db-fra.com

Comment Java La récupération de place fonctionne-t-elle avec des références circulaires?

D'après ma compréhension, la récupération de place dans Java nettoie certains objets si rien d'autre ne "pointe" vers cet objet.

Ma question est, que se passe-t-il si nous avons quelque chose comme ceci:

class Node {
    public object value;
    public Node next;
    public Node(object o, Node n) { value = 0; next = n;}
}

//...some code
{
    Node a = new Node("a", null), 
         b = new Node("b", a), 
         c = new Node("c", b);
    a.next = c;
} //end of scope
//...other code

a, b et c devraient être récupérés, mais ils sont tous référencés par d'autres objets.

Comment la Java garbage collection traite-t-elle cela? (Ou s'agit-il simplement d'une perte de mémoire?)

145
AlexeyMK

Le GC de Java considère les objets comme des "déchets" s'ils ne sont pas accessibles via une chaîne commençant à la racine du récupérateur de déchets, de sorte que ces objets seront collectés. Même si les objets peuvent se rejoindre pour former un cycle, ils restent des ordures s'ils sont coupés de la racine.

Reportez-vous à la section sur les objets inaccessibles de l'Annexe A: La vérité sur le garbage collection dans Performances de la plate-forme Java: stratégies et tactiques pour les détails sanglants.

147
Bill the Lizard

yes Java Garbage Collector gère les références circulaires!)

How?

Il existe des objets spéciaux appelés racines de récupération de place (racines GC). Ceux-ci sont toujours accessibles, de même que tout objet qui les a à sa propre racine.

Une simple Java a les racines GC suivantes:

  1. Variables locales dans la méthode principale
  2. Le fil conducteur
  3. Variables statiques de la classe principale

enter image description here

Pour déterminer quels objets ne sont plus utilisés, la machine virtuelle Java exécute par intermittence ce que l’on appelle très justement un algorithme de balisage . Cela fonctionne comme suit

  1. L'algorithme parcourt toutes les références d'objet, en commençant par les racines du GC, et marque chaque objet trouvé comme vivant.
  2. Toute la mémoire de segment non occupée par les objets marqués est récupérée. Il est simplement marqué comme libre, essentiellement débarrassé des objets inutilisés.

Ainsi, si un objet n'est pas accessible depuis les racines du GC (même s'il est auto-référencé ou référencé de manière cyclique), il sera soumis à un nettoyage de la mémoire.

Bien sûr, cela peut parfois entraîner une fuite de mémoire si le programmeur oublie de déréférencer un objet.

enter image description here

Source: Gestion de la mémoire Java

121
Aniket Thakur

Un ramasse-miettes commence à partir d'un ensemble d'emplacements "racine" toujours considérés comme "accessibles", tels que les registres de la CPU, la pile et les variables globales. Cela fonctionne en trouvant des indications dans ces domaines et en recherchant récursivement tout ce à quoi ils pointent. Une fois que tout est trouvé, tout sinon c'est de la foutaise.

Il existe bien sûr de nombreuses variantes, principalement pour des raisons de rapidité. Par exemple, la plupart des éboueurs modernes sont "générationnels", ce qui signifie qu'ils divisent les objets en générations et que, lorsqu'un objet vieillit, il devient de plus en plus long à chaque fois qu'il essaie de déterminer si cet objet est toujours valide ou non. - on commence à croire que, s'il a vécu longtemps, il y a de bonnes chances qu'il continue à vivre encore plus longtemps.

Néanmoins, l'idée de base reste la même: tout est basé sur le fait de partir d'un ensemble fondamental d'éléments pris pour acquis, puis de poursuivre tous les indicateurs pour trouver ce qui pourrait être utilisé.

Intéressant à part: peut-être que les gens sont souvent surpris par le degré de similitude entre cette partie d'un ramasse-miettes et le code de marshaling pour des objets tels que les appels de procédure à distance. Dans chaque cas, vous partez d'un ensemble d'objets racine et cherchez des pointeurs pour trouver tous les autres objets auxquels il fait référence ...

11
Jerry Coffin

Vous avez raison. La forme spécifique de garbage collection que vous décrivez s'appelle " comptage de références ". La façon dont cela fonctionne (du moins conceptuellement, la plupart des implémentations modernes du comptage de références sont en réalité mises en œuvre de manière très différente) dans le cas le plus simple, ressemble à ceci:

  • chaque fois qu'une référence à un objet est ajoutée (par exemple, elle est affectée à une variable ou à un champ, transmise à la méthode, etc.), son nombre de références est augmenté de 1
  • chaque fois qu'une référence à un objet est supprimée (la méthode retourne, la variable sort de la portée, le champ est réaffecté à un autre objet ou l'objet contenant le champ se ramasse lui-même), le nombre de références est réduit de 1
  • dès que le nombre de références atteint 0, il n'y a plus de référence à l'objet, ce qui signifie que personne ne peut plus l'utiliser, il est donc inutile et peut être collecté

Et cette stratégie simple a exactement le problème que vous décrivez: si A fait référence à B et B à A, leurs deux comptes de référence peuvent ne jamais être inférieurs à 1 , ce qui signifie qu'ils ne seront jamais collectés.

Il y a quatre façons de traiter ce problème:

  1. Ignore le. Si vous avez assez de mémoire, que vos cycles sont petits et peu fréquents et que votre temps d'exécution est court, vous pouvez peut-être vous en sortir en ne collectant tout simplement pas de cycles. Pensez à un interpréteur de script shell: les scripts shell ne s'exécutent que pendant quelques secondes et n'allouent pas beaucoup de mémoire.
  2. Combinez votre ramasse-miettes de comptage de références avec un autre ramasse-miettes qui n'a pas de problèmes de cycles. CPython fait ceci, par exemple: le garbage collector principal de CPython est un collecteur de comptage de références, mais de temps en temps un garbage collector de traçage est exécuté pour collecter les cycles.
  3. Détecter les cycles. Malheureusement, la détection des cycles dans un graphique est une opération plutôt coûteuse. En particulier, cela nécessite à peu près les mêmes frais généraux qu'un collecteur de traçage, de sorte que vous pouvez tout aussi bien en utiliser un.
  4. N'implémentez pas l'algorithme comme vous le feriez avec naïveté: depuis les années 1970, de nombreux algorithmes tout à fait intéressants ont été développés, qui combinent la détection de cycle et le comptage de références en une seule opération, ce qui est nettement moins coûteux que de le faire. les deux séparément ou en faisant un collecteur de traçage.

À propos, l’autre autre moyen d’implémenter un ramasse-miettes (et j’ai déjà fait allusion à cela quelques fois ci-dessus), est traçage . Un collecteur de traçage est basé sur le concept d'accessibilité . Vous commencez avec un ensemble de racines que vous savez être toujours accessible ( les constantes globales, par exemple, ou la classe Object, la portée lexicale actuelle, le cadre de pile actuel) et à partir de là, vous tracez tous les objets accessibles à partir du jeu de racines, puis tous les objets accessibles à partir des objets accessibles à partir du jeu de racines, et ainsi de suite, jusqu'à la fermeture transitive. Tout ce qui est pas dans cette fermeture est de la foutaise.

Puisqu'un cycle est seulement accessible en lui-même, mais pas accessible à partir du jeu de racines, il sera collecté.

10
Jörg W Mittag

Les GC Java ne se comportent pas vraiment comme vous le décrivez. Il est plus précis de dire qu’ils partent d’un ensemble d’objets de base, souvent appelés "racines GC", et collecteront tout objet pouvant ne pas être atteint à partir d'une racine.
Les racines du GC comprennent des éléments tels que:

  • variables statiques
  • variables locales (y compris toutes les références 'this' applicables) actuellement dans la pile d'un thread en cours d'exécution

Ainsi, dans votre cas, une fois que les variables locales a, b et c sont hors de portée à la fin de votre méthode, il n’y a plus de racines GC qui contiennent, directement ou indirectement, une référence à l’un de vos trois nœuds et ils seront éligibles pour la collecte des ordures.

Le lien de TofuBeer contient plus de détails si vous le souhaitez.

6
Sbodd

Cet article (non disponible) va en profondeur sur le ramasse-miettes (conceptuellement ... il y a plusieurs implémentations). La partie pertinente de votre message est "A.3.4 Injoignable":

A.3.4 Injoignable Un objet entre dans un état inaccessible lorsqu'il n'existe plus aucune référence forte à cet objet. Lorsqu'un objet est inaccessible, il est candidat à la collecte. Notez le libellé: Le fait qu'un objet soit candidat à la collecte ne signifie pas qu'il sera immédiatement collecté. La machine virtuelle Java est libre de retarder la collecte jusqu'à ce qu'il y ait un besoin immédiat de mémoire consommée par l'objet.

4
TofuBeer

La récupération de place ne signifie généralement pas "nettoyer un objet si et rien d'autre ne" pointe "vers cet objet" (c'est le comptage de références). Le ramassage des ordures signifie en gros rechercher des objets auxquels le programme ne permet pas d'accéder.

Ainsi, dans votre exemple, après que a, b et c aient quitté la portée, ils peuvent être collectés par le GC, car vous ne pouvez plus accéder à ces objets.

0
Amnon

Bill a répondu à votre question directement. Comme Amnon l'a dit, votre définition du ramassage des ordures n'est qu'un comptage de références. Je voulais juste ajouter que même des algorithmes très simples tels que mark, sweep and copy collection gèrent facilement les références circulaires. Donc, rien de magique à ce sujet!

0
Claudiu