En Java, dès qu’un objet n’a plus de références, il est éligible à la suppression, mais la JVM décide à quel moment 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 ne possède plus aucune référence forte, il est immédiatement supprimé. Pourquoi n'est-ce pas le cas en Java?
java
garbage-collection
moonman239
la source
la source
Réponses:
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 par rapport aux références fortes sont un problème totalement distinct du comptage des références par rapport au nettoyage des ordures.
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 à long terme peut être analysé beaucoup moins fréquemment. La suppression immédiate via la gestion manuelle de la mémoire ou le comptage de références est beaucoup plus susceptible de se fragmenter.
C'est un peu la différence entre faire les courses une fois par salaire et aller tous les jours chercher juste assez de nourriture pour une journée. Votre grand voyage prendra beaucoup plus de temps qu'un petit voyage individuel, mais globalement, vous gagnerez du temps et probablement de l'argent.
la source
Parce que bien savoir que quelque chose n'est plus référencé n'est pas facile. Même pas près de facile.
Et si vous avez deux objets qui se référent? Restent-ils pour toujours? Si vous élargissez cette ligne de pensée à la résolution de toute structure de données arbitraire, vous comprendrez vite pourquoi la JVM ou d'autres éboueurs sont obligés d'employer des méthodes beaucoup plus sophistiquées pour déterminer ce qui reste nécessaire et ce qui peut rester.
la source
Autant que je sache, la spécification de la machine virtuelle Java (écrite en anglais) ne mentionne pas le moment précis où un objet (ou une valeur) doit être supprimé, et laisse cela à la mise en œuvre (de même pour R5RS ). Cela nécessite ou suggère un ramasse-miettes, mais laisse les détails à la mise en œuvre. Et de même pour la spécification Java.
N'oubliez pas que les langages de programmation sont des spécifications (de syntaxe , de sémantique , etc.) et non des implémentations logicielles. Un langage comme Java (ou sa machine virtuelle Java) a de nombreuses implémentations. Sa spécification est publiée , téléchargeable (afin que vous puissiez l'étudier) et écrite en anglais. §2.5.3 Le tas de la spécification de la JVM mentionne un ramasse-miettes:
(L’accent est à moi; la finalisation de BTW est mentionnée au § 12.6 de la spécification Java, et un modèle de mémoire est au § 17.4 de la spécification Java)
Donc (en Java) vous ne devriez pas prendre soin quand un objet est supprimé , et vous pouvez coder comme, si cela ne se produit pas (par le raisonnement dans une abstraction où vous ignorez que). Bien sûr, vous devez vous préoccuper de la consommation de mémoire et des objets vivants, ce qui est une question différente . Dans plusieurs cas simples (pensez à un programme "hello world"), 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 vous ne vous souciez plus du tout. suppression d' objets individuels . Dans plus de cas, vous pouvez vous convaincre que les objets vivants(ou ceux qui sont joignables, ce qui est plus facile à raisonner que les vivants) ne dépasse jamais une limite raisonnable (et vous vous fiez alors à GC, mais vous vous moquez de savoir quand et comment la collecte des ordures a lieu). Lisez à propos de la complexité de l'espace .
J'imagine que sur plusieurs implémentations de machine virtuelle Java exécutant un programme Java de courte durée, comme celui de hello world, le ramasse-miettes n'est pas déclenché du tout 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 (du moins pour la plupart des objets Java, ceux qui n'utilisent pas la finalisation ou des références faibles ; la finalisation n'est pas garantie et peut être différée. Il s'agit donc d'une fonctionnalité utile que votre code ne devrait pas utiliser. dépend beaucoup de) dans lequel la notion de suppression d’un objet individuel n’a aucun sens (puisqu'un large bloc de mémoire contenant des zones de mémoire pour de nombreux objets, peut-être plusieurs mégaoctets à la fois, est libéré à la fois).
Si la spécification JVM exigeait que chaque objet soit supprimé le plus tôt possible (ou imposait simplement plus de contraintes à la suppression d'objet), les techniques de GC générationnelles efficaces seraient interdites et les concepteurs de Java et de la JVM ont été avisés de l'éviter.
En passant, il est possible qu’une JVM naïve qui ne supprime jamais d’objets et ne libère pas de mémoire se conforme aux spécifications (la lettre, pas l’esprit) et puisse certainement exécuter un problème de bonjour dans la pratique (notez que la plupart les programmes Java minuscules et de courte durée n'allouent probablement pas plus de quelques giga-octets de mémoire). Bien sûr, une telle machine virtuelle ne mérite pas d’être mentionnée et n’est qu’un jouet (comme cette implémentation de
malloc
C). Voir le GC Epsilon NoOp pour plus d'informations. Les machines virtuelles Java réelles sont des logiciels très complexes et combinent plusieurs techniques de récupération de place.En outre, Java n'est pas identique à la machine virtuelle Java et vous avez des implémentations Java qui s'exécutent sans machine virtuelle (par exemple , des compilateurs Java en avance sur le temps , le moteur d'exécution Android ). Dans certains cas (principalement académiques), vous pouvez imaginer (techniques dites de "récupération de place au moment de la compilation") qu’un programme Java n’alloue ni ne supprime au moment de l’exécution (par exemple parce que le compilateur optimiseur a été assez intelligent pour utiliser uniquement pile d'appels et variables automatiques ).
Parce que les spécifications Java et JVM ne l'exigent pas.
Lisez le manuel du GC pour plus d’informations (et les spécifications de la JVM ). Notez que le fait d'être en vie (ou utile pour les calculs futurs) d'un objet est une propriété de programme entier (non modulaire).
Objective-C privilégie une approche de comptage de références pour la gestion de la mémoire . Et cela a aussi des pièges (par exemple , l'Objective-C programmeur doit se soucier de références circulaires par explicitant références faibles, mais une machine virtuelle Java traite les références circulaires bien dans la pratique sans nécessiter l' attention du programmeur Java).
Il n'y a pas de solution miracle dans la programmation et la conception de langages de programmation (soyez conscient du problème de l' arrêt, car être un objet vivant utile est indécidable en général).
Vous pouvez également lire SICP , Pragmatique sur les langages de programmation , Le Livre du Dragon , Lisp In Small Pieces et les systèmes d’exploitation: Trois pièces faciles . Ils ne concernent pas Java, mais ils vous ouvriront l'esprit et devraient vous aider à comprendre ce que doit faire une machine virtuelle et comment elle pourrait fonctionner (avec d'autres éléments) sur votre ordinateur. Vous pouvez également passer plusieurs mois (ou plusieurs années) dans l' étude du code source complexe existant open source implémentations de la JVM (comme OpenJDK , qui a plusieurs millions de lignes de code source).
la source
finalize
aucune gestion de ressources (de descripteurs de fichiers, de connexions à une base de données, de ressources gpu, etc.).Ce n'est pas correct - Java contient à la fois 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 du langage.
Cela n’est pas non plus nécessairement correct. Certaines versions de l’objectif C utilisaient en effet un garbage collector générationnel. Les autres versions ne comportaient aucune récupération de place.
Il est vrai que les versions les plus récentes d'Objective C utilisent un comptage de référence automatique (ARC) plutôt qu'un GC basé sur une trace, ce qui entraîne (souvent) la "suppression" de l'objet lorsque ce nombre de références atteint zéro. Cependant, notez qu'une implémentation de machine virtuelle Java peut également être conforme et fonctionner exactement de cette façon (enfin, elle pourrait être conforme et ne pas avoir de GC.)
Alors, pourquoi la plupart des implémentations JVM ne font-elles pas cela et utilisent-elles des algorithmes de GC basés sur la trace?
En termes simples, ARC n’est pas aussi utopique qu’il semble au premier abord:
Bien entendu, ARC présente des avantages: sa mise en œuvre est simple et sa collecte est déterministe. Mais les inconvénients ci-dessus, entre autres, expliquent la raison pour laquelle la plupart des implémentations de machine virtuelle Java utilisent un GC générationnel basé sur les traces.
la source
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 de récupération de place, mais ceux qui garantissent la collecte immédiate d'un objet sont presque entièrement basés sur le comptage de références (je ne connais aucun algorithme qui rompt 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 simple, cela n’est rien d’autre qu’un incrément et un décrément. Par conséquent, l’affectation d’un pointeur peut coûter trois fois plus cher en code compté de référence qu’elle ne le fait en code compté non référencé (si le compilateur peut tout casser jusqu’à la machine). code).
Dans le code multithread, le coût est plus élevé. Il appelle soit des incréments / décréments atomiques, soit des verrous, ce qui peut coûter cher. Sur un processeur moderne, une opération atomique peut être environ 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.
Donc, avec cela, nous pouvons considérer les compromis faits par plusieurs modèles.
Objective-C se concentre sur ARC - comptage automatique des références. Leur approche consiste à utiliser le décompte de références pour tout. Il n'y a pas de détection de cycle (à ma connaissance), 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 souvent assignés, et leur compilateur peut identifier des situations dans lesquelles l'incrémentation / décrémentation des comptages de références ne peut pas causer la mort d'un objet et éliminer complètement ces incréments / décréments. Ainsi, ils minimisent le coût du comptage des références.
CPython utilise un mécanisme hybride. Ils utilisent des comptages de références, mais ils ont également un ramasse-miettes qui identifie les cycles et les libère. Cela procure les avantages des deux mondes, au détriment des deux approches. CPython doit à la fois gérer le nombre de références etfaire la comptabilité pour détecter les cycles. CPython s'en tire de deux manières. Le poing est que CPython n’est vraiment pas entièrement multithread. Il possède un verrou appelé GIL qui limite le multithreading. Cela signifie que CPython peut utiliser des incréments / décréments normaux plutôt que des incréments atomiques, ce qui est beaucoup plus rapide. CPython est également interprété, ce qui signifie que des opérations telles que l’affectation à une variable prennent déjà une poignée d’instructions plutôt que seulement 1. Le coût supplémentaire associé aux incréments / décréments, effectué rapidement dans le code C, pose moins de problèmes, car nous ' Nous avons déjà payé ce coût.
Java adopte l'approche consistant à ne pas garantir du tout un système de référence compté. En effet, la spécification ne dit rien sur la façon dont les objets sont gérés, à part le fait qu'il y aura un système de gestion de stockage automatique. Cependant, la spécification suggère également fortement l'hypothèse qu'il s'agira d'un ramassage des ordures de manière à gérer les cycles. En ne spécifiant pas la date d'expiration des objets, Java gagne la liberté d'utiliser des collecteurs qui ne perdent pas de temps en incrémentation / décrémentation. En effet, des algorithmes intelligents tels que les éboueurs générationnels peuvent même traiter de nombreux cas simples sans même regarder les données en cours de récupération (ils doivent seulement regarder les données qui sont toujours référencées).
Nous pouvons donc constater que chacun de ces trois pays a dû faire des compromis. Le meilleur compromis dépend largement de la nature de l'utilisation de la langue.
la source
Bien que
finalize
reliée au GC de Java, la récupération de place ne s'intéresse pas aux objets morts, mais aux objets vivants. Sur certains systèmes du GC (y compris éventuellement des implémentations de Java), la seule chose qui distingue un groupe de bits représentant un objet d'un groupe de stockage inutilisé peut être l'existence de références à l'ancien. Tandis que les objets avec les finaliseurs sont ajoutés à une liste spéciale, d'autres objets peuvent ne comporter aucun élément dans l'univers indiquant que leur stockage est associé à un objet, à l'exception des références conservées dans le code utilisateur. Lorsque la dernière référence de ce type est écrasée, le modèle de bits en mémoire cesse immédiatement d'être reconnaissable en tant qu'objet, que quelque chose dans l'univers le sache ou non.Le ramassage des ordures n'a pas pour objet de détruire des objets pour lesquels il n'existe aucune référence, mais plutôt d'accomplir trois choses:
Invalidez les références faibles qui identifient les objets auxquels aucune référence très accessible n'est associée.
Effectuez une recherche dans la liste d'objets du système avec les finaliseurs pour voir si aucun de ceux-ci ne comporte de référence trop accessible.
Identifiez et consolidez les zones de stockage qui ne sont utilisées par aucun objet.
Notez que l'objectif principal du GC est le n ° 3 et que plus on attend longtemps avant de le faire, plus il y a de chances que la consolidation présente des opportunités. Il est logique de faire le n ° 3 dans les cas où on aurait une utilisation immédiate pour le stockage, mais sinon, il est plus logique de le différer.
la source
Permettez-moi de suggérer une reformulation et une généralisation de votre question:
Dans cet esprit, parcourez rapidement les réponses. Il y en a sept jusqu'à présent (sans compter celui-ci), avec pas mal de fils de commentaires.
C'est ta 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 gérer un objet dès qu'il n'est pas nécessaire. d'autres non. En gardant le contrat vacant, Java offre plus d'options à ses développeurs.
Bien sûr, il y a un compromis même dans cette décision: en gardant le contrat en suspens, Java * supprime la possibilité pour les programmeurs de pouvoir compter sur des destructeurs. C'est quelque chose que les programmeurs C ++ en particulier manquent souvent ([citation nécessaire];)), donc ce n'est pas un compromis insignifiant. Je n'ai pas assisté à une discussion de cette méta-décision particulière, mais les gens de Java ont sans doute décidé que les avantages de disposer de davantage d'options de GC dépassaient les avantages de pouvoir dire aux programmeurs exactement quand un objet serait détruit.
* Il y a la
finalize
méthode, mais pour diverses raisons qui sont hors de portée pour cette réponse, il est difficile et peu recommandable de s'y fier.la source
Il existe deux stratégies différentes de gestion de la mémoire sans code explicite écrit par le développeur: la récupération de place et le comptage de références.
La récupération de place a l'avantage de "fonctionner" sauf si le développeur fait 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 que le développeur doit parfois être intelligent. C'est donc un avantage pour la collecte des ordures.
Avec le comptage de références, l'objet s'éloigne immédiatement lorsque le comptage de références tombe à zéro. C'est un avantage pour le comptage de références.
Speedwise, le ramassage des ordures est plus rapide si vous croyez les fans de ce dernier et le décompte des références est plus rapide si vous croyez les fans du décompte des références.
Il existe juste deux méthodes différentes pour atteindre le même objectif, Java en a choisi une, Objective-C en a choisi une autre (et beaucoup de support du compilateur lui a été ajouté pour le transformer en une tâche fastidieuse pour les développeurs).
Passer de la récupération de place au dépouillement des références en Java serait une entreprise majeure, car de nombreux changements de code seraient nécessaires.
En théorie, Java aurait pu implémenter un mélange de récupération de place et de comptage de références: si le nombre de références est 0, l'objet est inaccessible, mais pas nécessairement l'inverse. Vous pouvez donc conserver les comptes de référence et supprimer des objets lorsque leur nombre de référence est égal à zéro (puis exécuter une récupération de place de temps en temps pour intercepter des objets dans des cycles de référence inaccessibles). Je pense que le monde est divisé en 50/50 pour ceux qui pensent que l'ajout du décompte de références au ramassage des ordures est une mauvaise idée, et ceux qui pensent que l'ajout du ramassage des ordures au décompte des références est une mauvaise idée. Donc, cela ne va pas arriver.
Ainsi, Java peut supprimer des objets immédiatement si leur nombre de références est égal à zéro et supprimer des objets dans des cycles inaccessibles ultérieurement. Mais c'est une décision de conception et Java a décidé de ne pas la prendre.
la source
Tous les autres arguments de performance et discussions sur la difficulté de comprendre lorsqu'il n'y a plus de références à un objet sont corrects, bien qu'une autre idée qui mérite d'être mentionnée mérite d'être mentionnée, c'est qu'il existe au moins une machine virtuelle (azul) prenant en compte quelque chose comme ceci. en ce sens qu'il implémente parallel gc qui a essentiellement un thread vm vérifiant en permanence 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 va constamment regarder le tas et essayer de récupérer toute mémoire qui n'est pas référencée. Cela engendre un coût de performance très léger, mais conduit à des temps de CPG essentiellement nuls ou très courts. (C'est-à-dire que si la taille du segment de mémoire en expansion constante dépasse la mémoire vive du système, Azul devient confus, puis il y a des dragons)
TLDR Quelque chose comme ça existe un peu pour la JVM, c'est juste une JVM spéciale et elle a des inconvénients comme n'importe quel autre compromis d'ingénierie.
Disclaimer: Je n'ai aucun lien avec Azul, nous l'avons utilisé lors d'un précédent emploi.
la source
L'optimisation du débit soutenu ou la minimisation de la latence de la génératrice sont en tension dynamique, ce qui est probablement la raison la plus courante pour laquelle la GC n'est pas immédiate. Dans certains systèmes, tels que les applications d'urgence 911, le non-respect d'un seuil de latence spécifique peut déclencher des processus de basculement du site. Dans d’autres, comme les sites bancaires et / ou d’arbitrage, il est bien plus important de maximiser le débit.
la source
La vitesse
Pourquoi tout cela se passe-t-il finalement à 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 000 000 opérations par seconde, des opérations extrêmement longues et compliquées peuvent se produire entre chaque opérateur, telles que la suppression d'objets supprimés. Comme ce nombre d'opérations par seconde n'est pas exact et que, comme l'expliquent la plupart des autres réponses, il est en réalité compliqué et nécessite beaucoup de ressources pour le résoudre, il existe un garbage collection qui permet aux programmes de se concentrer sur ce qu'ils essaient réellement d'accomplir. manière rapide.
la source