web-dev-qa-db-fra.com

Pourquoi les objets Java ne sont-ils pas supprimés immédiatement après qu'ils ne sont plus référencés?

En Java, dès qu'un objet n'a plus de références, il devient éligible pour la suppression, mais la JVM décide quand l'objet est réellement supprimé. Pour utiliser la terminologie Objective-C, toutes les références Java sont intrinsèquement "fortes". Cependant, dans Objective-C, si un objet n'a plus de références fortes, l'objet est supprimé immédiatement. Pourquoi n'est-il pas c'est pas le cas en Java?

79
moonman239

Tout d'abord, Java a des références faibles et une autre catégorie de meilleur effort appelée références logicielles. Les références faibles contre les références fortes sont un problème complètement différent du comptage des références par rapport au ramasse-miettes).

Deuxièmement, il existe des modèles d'utilisation de la mémoire qui peuvent rendre la collecte des ordures plus efficace dans le temps en sacrifiant de l'espace. Par exemple, les objets plus récents sont beaucoup plus susceptibles d'être supprimés que les objets plus anciens. Donc, si vous attendez un peu entre les balayages, vous pouvez supprimer la plupart de la nouvelle génération de mémoire, tout en déplaçant les quelques survivants vers un stockage à plus long terme. Ce stockage à plus long terme peut être analysé beaucoup moins fréquemment. La suppression immédiate via la gestion manuelle de la mémoire ou le comptage des références est beaucoup plus sujette à la fragmentation.

C'est un peu comme la différence entre faire l'épicerie une fois par chèque de paie et aller tous les jours pour obtenir juste assez de nourriture pour une journée. Votre grand voyage prendra beaucoup plus de temps qu'un petit voyage individuel, mais dans l'ensemble, vous finissez par gagner du temps et probablement de l'argent.

79
Karl Bielefeldt

Parce que savoir correctement quelque chose n'est plus référencé n'est pas facile. Pas même proche de facile.

Et si vous avez deux objets se référençant? Restent-ils pour toujours? En étendant cette ligne de pensée à la résolution de toute structure de données arbitraire, et vous verrez bientôt pourquoi la JVM ou d'autres récupérateurs de place sont obligés d'employer des méthodes beaucoup plus sophistiquées pour déterminer ce qui est encore nécessaire et ce qui peut aller.

86
whatsisname

AFAIK, le spécification JVM (écrit en anglais) ne mentionne pas quand exactement un objet (ou une valeur) doit être supprimé, et laisse cela à l'implémentation (de même pour R5RS ). Il nécessite ou suggère en quelque sorte un garbage collector mais laisse les détails à l'implémentation. Et de même pour la spécification Java.

N'oubliez pas que langages de programmation sont spécifications (de syntaxe , sémantique , etc ...), pas de logiciel implémentations. Un langage comme Java (ou sa JVM) a de nombreuses implémentations. Sa spécification est publiée , téléchargeable (pour que vous puissiez l'étudier) et écrite en anglais. §2.5.3 Heap de la spécification JVM mentionne un garbage collector:

Le stockage en tas pour les objets est récupéré par un système de gestion de stockage automatique (appelé garbage collector); les objets ne sont jamais explicitement désalloués. La machine virtuelle Java suppose aucun type particulier de système de gestion automatique du stockage

(l'accent est sur moi; la finalisation BTW est mentionnée dans §12.6 de Java spec, et un modèle de mémoire est dans §17.4 de Java spec)

Donc (en Java) vous ne devriez pas vous soucier quand un objet est supprimé , et vous pouvez coder as-if cela ne se produit pas (en raisonnant dans un abstraction où vous l'ignorez). Bien sûr, vous devez vous soucier de la consommation de mémoire et des objets vivants, ce qui est une question différente. Dans plusieurs cas simples (pensez à un programme "bonjour le monde"), vous êtes en mesure de prouver - ou de vous convaincre - que la mémoire allouée est plutôt petite (par exemple moins d'un gigaoctet), et puis vous ne vous souciez pas du tout de suppression des objets individuels. Dans plus de cas, vous pouvez vous convaincre que les objets vivants (ou accessibles, ce qui est un sur-ensemble - plus facile à raisonner) des vivants) ne dépassent jamais une limite raisonnable (et alors vous comptez sur GC, mais vous ne vous souciez pas comment et quand la collecte des ordures se produit). Lisez à propos de complexité de l'espace .

Je suppose que sur plusieurs JVM implémentations exécutant un programme Java de courte durée comme un bonjour le monde, le garbage collector n'est pas déclenché à tous et aucune suppression ne se produit. AFAIU, un tel comportement est conforme aux nombreuses spécifications Java.

La plupart des implémentations JVM utilisent des techniques de copie générationnelles (au moins pour la plupart des objets Java, celles qui n'utilisent pas finalisation ou références faibles ; et la finalisation n'est pas garantie de se produire dans un court laps de temps et pourrait être reportée, est donc juste une fonctionnalité utile dont votre code ne devrait pas dépendre beaucoup) dans laquelle la notion de suppression d'un objet individuel n'a aucun sens (car un grand bloc de mémoire -contenant des zones de mémoire pour de nombreux objets-, peut-être plusieurs mégaoctets à la fois, sont libérés à la fois).

Si la spécification JVM exigeait que chaque objet soit supprimé le plus rapidement possible (ou simplement imposait plus de contraintes à la suppression d'objet), des techniques de génération GC efficaces seraient interdites, et les concepteurs de Java et de la JVM ont été sage en évitant cela.

BTW, il pourrait être possible qu'une JVM naïve qui ne supprime jamais d'objets et ne libère pas de mémoire soit conforme aux spécifications (la lettre, pas l'esprit) et soit certainement capable d'exécuter un truc bonjour dans la pratique (notez que la plupart les programmes Java minuscules et de courte durée de vie n'allouent probablement pas plus de quelques gigaoctets de mémoire). Bien sûr, une telle machine virtuelle Java ne mérite pas d'être mentionnée et n'est qu'un jouet (comme l'est this implémentation de malloc pour C). Voir Epsilon NoOp GC pour en savoir plus. Les JVM réelles sont des logiciels très complexes et mélangent plusieurs techniques de collecte des déchets.

En outre, Java n'est pas la même chose que la JVM, et vous avez des implémentations Java exécutées sans la JVM (par exemple à l'avance Java compilateurs, runtime Android ). Dans certains cas (principalement académiques), vous pourriez imaginer (ce que l'on appelle des techniques de "collecte des ordures au moment de la compilation") qu'un programme Java n'alloue ni ne supprime à runtime (par exemple parce que le optimisation du compilateur a été assez intelligent pour utiliser uniquement pile d'appels et variables automatiques ).

Pourquoi les objets Java ne sont-ils pas supprimés immédiatement après qu'ils ne sont plus référencés?

Parce que les spécifications Java et JVM ne l'exigent pas.


Lisez le GC manuel pour plus (et le spécification JVM ). Notez qu'être vivant (ou utile pour de futurs calculs) pour un objet est une propriété de programme entier (non modulaire).

Objective-C favorise une approche comptage de référencesgestion de la mémoire . Et cela a aussi des pièges (par exemple, l'Objective-C programmeur doit se soucier références circulaires en expliquant les références faibles, mais une machine virtuelle Java gère bien les références circulaires dans la pratique sans nécessiter attention du programmeur Java).

Il y a No Silver Bullet dans la programmation et la conception du langage de programmation (soyez conscient du Halting Problem ; être un objet vivant utile est - indécidable en général).

Vous pouvez également lire SICP , Programming Language Pragmatics , the Dragon Book , LISP en petits morceaux et Systèmes d'exploitation: trois morceaux faciles . Ils ne concernent pas Java, mais ils vous ouvriront l'esprit et devraient vous aider à comprendre ce qu'une JVM devrait faire et comment cela pourrait fonctionner (avec d'autres éléments) sur votre ordinateur. Vous pouvez également passer plusieurs mois (ou plusieurs années) à étudier le code source complexe des implémentations JVM open source (comme OpenJDK , qui compte plusieurs millions de lignes de code source) .

45

Pour utiliser la terminologie Objective-C, toutes les références Java sont intrinsèquement "fortes").

Ce n'est pas correct - Java a des références faibles et souples, bien que celles-ci soient implémentées au niveau de l'objet plutôt que comme mots-clés de langage.

Dans Objective-C, si un objet n'a plus de références fortes, l'objet est supprimé immédiatement.

Ce n'est pas non plus nécessairement correct - certaines versions d'Objective C utilisaient en effet un garbage collector générationnel. Les autres versions n'avaient aucune collecte de déchets du tout.

Il est vrai que les versions plus récentes d'Objective C utilisent le comptage de référence automatique (ARC) plutôt qu'un GC basé sur des traces, et cela entraîne (souvent) la suppression de l'objet lorsque ce nombre de références atteint zéro. Cependant, notez qu'une implémentation JVM pourrait également être conforme et fonctionner exactement de cette façon (diable, elle pourrait être conforme et ne pas avoir de GC du tout.)

Alors pourquoi la plupart des implémentations JVM ne font-elles pas cela et utilisent-elles plutôt des algorithmes GC basés sur des traces?

Autrement dit, ARC n'est pas aussi utopique qu'il n'y paraît à première vue:

  • Vous devez incrémenter ou décrémenter un compteur chaque fois qu'une référence est copiée, modifiée ou hors de portée, ce qui entraîne une surcharge de performances évidente.
  • ARC ne peut pas facilement effacer les références cycliques, car elles ont toutes une référence les unes aux autres, donc leur nombre de références n'atteint jamais zéro.

L'ARC a bien sûr des avantages - sa simplicité de mise en œuvre et sa collecte sont déterministes. Mais les inconvénients ci-dessus, entre autres, sont la raison pour laquelle la majorité des implémentations JVM utiliseront un GC générationnel basé sur des traces.

23
berry120

Java ne spécifie pas précisément quand l'objet est collecté car cela donne aux implémentations la liberté de choisir comment gérer le garbage collection.

Il existe de nombreux mécanismes différents de récupération de place, mais ceux qui garantissent que vous pouvez collecter un objet immédiatement sont presque entièrement basés sur le comptage de références (je ne connais aucun algorithme qui brise cette tendance). Le comptage de références est un outil puissant, mais il a un coût de maintien du comptage de références. Dans le code à un seul fil, ce n'est rien de plus qu'un incrément et un décrément, donc l'attribution d'un pointeur peut coûter de l'ordre de 3 fois autant dans le code compté de référence que dans le code compté non-référence (si le compilateur peut tout faire cuire sur machine) code).

En code multithread, le coût est plus élevé. Il nécessite soit des augmentations/diminutions atomiques, soit des verrous, les deux pouvant être coûteux. Sur un processeur moderne, une opération atomique peut être de l'ordre de 20 fois plus chère qu'une simple opération de registre (varie évidemment d'un processeur à l'autre). Cela peut augmenter le coût.

Ainsi, avec cela, nous pouvons considérer les compromis effectués par plusieurs modèles.

  • Objective-C se concentre sur ARC - comptage de références automatisé. Leur approche consiste à utiliser le comptage de références pour tout. Il n'y a pas de détection de cycle (que je sache), donc les programmeurs sont censés empêcher les cycles de se produire, ce qui coûte du temps de développement. Leur théorie est que les pointeurs ne sont pas assignés très souvent, et leur compilateur peut identifier les situations où l'incrémentation/décrémentation des décomptes de référence ne peut pas provoquer la mort d'un objet, et éliminer complètement ces incréments/décrémentations. Ils minimisent ainsi le coût du comptage des références.

  • CPython utilise un mécanisme hybride. Ils utilisent des décomptes de référence, mais ils ont également un ramasse-miettes qui identifie les cycles et les libère. Cela offre les avantages des deux mondes, au détriment des deux approches. CPython doit à la fois maintenir le nombre de références et faire la comptabilité pour détecter les cycles. CPython s'en sort de deux manières. Le premier est que CPython n'est vraiment pas entièrement multithread. Il a un verrou connu sous le nom de GIL qui limite le multithreading. Cela signifie que CPython peut utiliser des incréments/décréments normaux plutôt que des atomiques, ce qui est beaucoup plus rapide. CPython est également interprété, ce qui signifie que les opérations telles que l'attribution à une variable prennent déjà une poignée d'instructions plutôt que juste 1. Le coût supplémentaire de faire les incréments/décréments, qui se fait rapidement en code C, est moins problématique parce que nous ' ai déjà payé ce coût.

  • Java descend l'approche de ne pas garantir du tout un système compté par référence. En effet, la spécification ne dit pas quoi que ce soit sur la façon dont les objets sont gérés, sinon qu'il y aura un système de gestion de stockage automatique. Cependant, la spécification fait également fortement allusion à l'hypothèse qu'il s'agira de déchets récupérés d'une manière qui gère les cycles. En ne spécifiant pas quand les objets expirent, Java gagne la liberté d'utiliser des collecteurs qui ne perdent pas de temps à incrémenter/décrémenter. En effet, des algorithmes intelligents tels que les collecteurs de déchets générationnels peuvent même gérer de nombreux cas simples sans même regarder sur les données qui sont récupérées (ils n'ont qu'à regarder les données qui sont encore référencées).

Nous pouvons donc voir que chacun de ces trois a dû faire des compromis. Le meilleur compromis dépend grandement de la nature de la façon dont la langue est destinée à être utilisée.

5
Cort Ammon

Bien que finalize soit compatible avec le GC de Java, la collecte des ordures ne s'intéresse pas aux objets morts, mais aux objets vivants. Sur certains systèmes GC (incluant éventuellement certaines implémentations de Java), la seule chose qui distingue un groupe de bits qui représente un objet d'un groupe de stockage qui n'est utilisé pour rien peut être l'existence de références au premier. Alors que les objets avec des finaliseurs sont ajoutés à une liste spéciale, d'autres objets peuvent ne rien avoir dans l'univers qui indique que leur stockage est associé à un objet, à l'exception des références contenues dans le code utilisateur. Lorsque la dernière référence de ce type est écrasée, le motif binaire en mémoire immédiatement cesse d'être reconnaissable en tant qu'objet, que quelque chose dans l'univers en soit conscient ou non.

Le but de la récupération de place n'est pas de détruire des objets auxquels aucune référence n'existe, mais plutôt d'accomplir trois choses:

  1. Invalidez les références faibles qui identifient les objets auxquels aucune référence fortement accessible n'est associée.

  2. Effectuez une recherche dans la liste d'objets du système avec les finaliseurs pour voir si aucun de ceux-ci n'a de références fortement accessibles qui leur sont associées.

  3. Identifiez et consolidez les régions de stockage qui ne sont utilisées par aucun objet.

Notez que l'objectif principal du GC est le n ° 3, et plus on attend avant de le faire, plus il y aura de chances de consolidation. Il est logique de faire # 3 dans les cas où l'on aurait une utilisation immédiate pour le stockage, mais sinon, il est plus logique de le reporter.

4
supercat

Permettez-moi de suggérer une reformulation et une généralisation de votre question:

Pourquoi Java ne fait-il pas de garanties solides sur son processus GC?

Dans cet esprit, parcourez rapidement les réponses ici. Il y en a sept jusqu'à présent (sans compter celui-ci), avec pas mal de fils de commentaires.

C'est votre réponse.

GC est difficile. Il y a beaucoup de considérations, beaucoup de compromis différents et, finalement, beaucoup d'approches très différentes. Certaines de ces approches permettent de GC un objet dès qu'il n'est pas nécessaire; d'autres non. En gardant le contrat lâche, Java donne plus d'options à ses implémenteurs.

Il y a un compromis même dans cette décision, bien sûr: en gardant le contrat lâche, Java surtout * enlève la possibilité pour les programmeurs de s'appuyer sur des destructeurs. C'est quelque chose que les programmeurs C++ en particulier souvent miss ([la citation nécessaire];)), donc ce n'est pas un compromis insignifiant. Je n'ai pas vu de discussion sur cette méta-décision particulière, mais sans doute les Java personnes ont décidé que les avantages d'avoir plus d'options GC l'emportaient sur les avantages de pouvoir dire aux programmeurs exactement quand un objet va être détruit.


* Il existe la méthode finalize, mais pour diverses raisons qui sont hors de portée pour cette réponse, il est difficile et pas une bonne idée de s'y fier.

4
yshavit

Il existe deux stratégies différentes pour gérer la mémoire sans code explicite écrit par le développeur: la collecte des ordures et le comptage des références.

La récupération de place a l'avantage de "fonctionner" à moins que le développeur ne fasse quelque chose de stupide. Avec le comptage de références, vous pouvez avoir des cycles de référence, ce qui signifie que cela "fonctionne" mais le développeur doit parfois être intelligent. C'est donc un plus pour la collecte des ordures.

Avec le comptage de références, l'objet disparaît immédiatement lorsque le comptage de références descend à zéro. C'est un avantage pour le comptage de références.

En termes de vitesse, la collecte des ordures est plus rapide si vous croyez aux fans de la collecte des ordures, et le comptage des références est plus rapide si vous croyez aux fans du comptage des références.

Il n'y a que deux méthodes différentes pour atteindre le même objectif, Java a choisi une méthode, Objective-C en a choisi une autre (et a ajouté beaucoup de prise en charge du compilateur pour la changer de douloureuse en quelque chose qui est peu de travail pour les développeurs).

Changer Java de la collecte des ordures au comptage des références serait une entreprise majeure, car de nombreuses modifications de code seraient nécessaires.

En théorie, Java aurait pu implémenter un mélange de collecte des ordures et de comptage des références: si le nombre de références est 0, alors l'objet est inaccessible, mais pas nécessairement l'inverse. Donc vous - pourrait conserver les décomptes de référence et supprimer des objets lorsque leur décompte de référence est nul (puis exécuter de temps en temps le ramasse-miettes pour capturer des objets dans des cycles de référence inaccessibles). Je pense que le monde est divisé 50/50 en personnes qui pensent que l'ajout du comptage des références à la collecte des ordures est une mauvaise idée, et les gens qui pensent que l'ajout de la collecte des ordures au comptage des références est une mauvaise idée. Donc, cela ne va pas se produire.

Donc Java pourrait supprimer les objets immédiatement si leur nombre de références devient nul, et supprimer les objets dans les cycles inaccessibles plus tard. Mais c'est une décision de conception, et Java a décidé contre.

3
gnasher729

Tous les autres arguments de performance et discussions sur la difficulté de comprendre quand il n'y a plus de références à un objet sont corrects, bien qu'une autre idée qui, à mon avis, mérite d'être mentionnée est qu'il existe au moins une machine virtuelle Java (azul) qui considère quelque chose comme ça en ce qu'il implémente gc parallèle qui a essentiellement un thread vm vérifiant constamment les références pour tenter de les supprimer, ce qui n'agira pas de manière totalement différente de ce dont vous parlez. Fondamentalement, il regardera constamment le tas et tentera de récupérer toute la mémoire qui n'est pas référencée. Cela entraîne un très faible coût de performance, mais cela conduit à des temps de GC essentiellement nuls ou très courts. (C'est à moins que la taille du tas en constante expansion dépasse le système RAM puis Azul se confond et puis il y a des dragons)

TLDR Quelque chose comme ça existe pour la JVM c'est juste un jvm spécial et il a des inconvénients comme tout autre compromis d'ingénierie.

Avertissement: je n'ai aucun lien avec Azul, nous l'avons utilisé lors d'un précédent travail.

1
ford prefect

La maximisation du débit soutenu ou la minimisation de la latence gc sont en tension dynamique, ce qui est probablement la raison la plus courante pour laquelle la GC ne se produit pas immédiatement. Dans certains systèmes, comme les applications d'urgence 911, le non-respect d'un seuil de latence spécifique peut déclencher des processus de basculement de site. Dans d'autres, comme un site bancaire et/ou d'arbitrage, il est beaucoup plus important de maximiser le débit.

1
barmid

La vitesse

Pourquoi tout cela se passe en fin de compte à cause de la vitesse. Si les processeurs étaient infiniment rapides, ou (pour être pratique) proches, par exemple 1 000 000 000 000 000 000 000 000 000 000 000 d'opérations par seconde, alors vous pouvez avoir des choses incroyablement longues et compliquées entre chaque opérateur, comme s'assurer que les objets dé-référencés sont supprimés. Comme ce nombre d'opérations par seconde n'est pas vrai actuellement et, comme la plupart des autres réponses l'expliquent, il est en fait compliqué et gourmand en ressources pour le comprendre, la récupération de place existe afin que les programmes puissent se concentrer sur ce qu'ils essaient réellement de réaliser dans un manière rapide.

0
Michael Durrant