web-dev-qa-db-fra.com

Comment la collecte des ordures se compare-t-elle au comptage des références?

Je commence à travailler à travers un cours en ligne sur le développement iOS dans la nouvelle langue d'Apple, Swift. L'instructeur a fait une remarque qui a soulevé cette question dans mon esprit. Il a dit quelque chose à l'effet de:

Il n'y a pas besoin de s'inquiéter de la gestion de la mémoire car tout est pris en charge pour vous par le comptage des références, et cela signifie qu'il n'y a pas besoin de s'inquiéter de comprendre comment fonctionne le ramassage des ordures.

Quand j'ai entendu cela, je me suis dit: "Pourquoi utiliser la collecte des ordures alors que vous pouvez simplement utiliser le comptage de références?"

Alors, comment les deux approches se comparent-elles?

46
Aaron Anodide

Pour comprendre comment les deux approches se comparent, nous devons d'abord examiner leur fonctionnement et les faiblesses de chacune.

Le comptage automatique de références ou ARC, est une forme de récupération de place dans laquelle les objets sont désalloués une fois qu'il n'y a plus de références à eux, c'est-à-dire qu'aucune autre variable ne fait référence à l'objet en particulier. Chaque objet, sous ARC, contient un compteur de référence, stocké en tant que champ supplémentaire en mémoire, qui est incrémenté chaque fois que vous définissez une variable sur cet objet (c'est-à-dire qu'une nouvelle référence à l'objet est créée), et est décrémentée chaque fois que vous définissez une référence à l'objet à nil/null, ou une référence sort du domaine (c'est-à-dire qu'elle est supprimée lorsque la pile se déroule), une fois que le compteur de référence descend à zéro, l'objet se charge de se supprimer, d'appeler le destructeur et de libérer la mémoire allouée. Cette approche présente une faiblesse importante, comme nous le verrons ci-dessous.

"Il n'y a pas besoin de s'inquiéter de la gestion de la mémoire car tout est pris en charge pour vous par le comptage des références", c'est en fait une idée fausse que vous devez toujours prendre soin d'éviter certaines conditions, à savoir les références circulaires, pour que ARC fonctionne correctement. Une référence circulaire est lorsqu'un objet A contient une référence forte à un objet B, qui lui-même détient une référence forte au même objet A, dans cette situation, aucun objet ne sera désalloué car pour que A soit désalloué son compteur de référence doit être décrémenté à zéro, mais au moins une de ces références est l'objet B, pour que l'objet B soit désalloué, son compteur de référence doit également être décrémenté à 0, mais au moins une de ces références est l'objet A, pouvez-vous voir le problème ? ARC résout ce problème en permettant au programmeur de donner des conseils au compilateur sur la façon dont les différentes références d'objet doivent être traitées. Il existe deux types de références: les références fortes et les références faibles. Les références fortes sont, comme je l'ai mentionné ci-dessus, un type de référence qui prolonge la vie de l'objet référencé (incrémente son compteur de référence), les références faibles sont un type de référence qui ne prolonge pas la vie d'un objet (c'est-à-dire qu'il fait ne pas incrémenter le compteur de référence de l'objet), mais cela signifierait que l'objet référencé pourrait être désalloué et il vous resterait une référence non valide pointant vers la mémoire indésirable. Pour éviter cette situation, la référence faible est définie sur une valeur sûre (par exemple, zéro dans Objective-C) une fois l'objet désalloué, donc l'objet a une responsabilité supplémentaire de garder une trace de toutes les références faibles et de les définir sur une valeur sûre une fois qu'il s'est supprimé. Les références faibles sont généralement utilisées dans une relation d'objet enfant-parent, le parent détient une référence forte à tous ses objets enfants, tandis que les objets enfant contiennent une référence faible au parent, la justification étant que dans la plupart des cas, si vous ne vous souciez plus de l'objet parent, vous ne vous souciez probablement plus non plus des objets enfants.

Le traçage du garbage collection (c'est-à-dire ce qui est le plus souvent appelé simplement garbage collection) implique de conserver une liste de tous les objets racine (c'est-à-dire ceux stockés dans des variables globales, les variables locales de la procédure principale, etc.) et de tracer quels objets sont accessibles (marquage chaque objet rencontré) à partir de ces objets racine. Une fois que le garbage collector a parcouru tous les objets référencés par les objets racine, le GC parcourt maintenant chaque objet alloué, s'il est marqué comme accessible, il reste en mémoire, s'il n'est pas marqué comme accessible, il est désalloué, c'est connu comme algorithme de marquage et de balayage. Cela présente l'avantage de ne pas souffrir du problème de référence circulaire car: si ni l'objet A ni l'objet B mutuellement référencés ne sont référencés par un autre objet accessible à partir des objets racine, ni l'objet A ni l'objet B ne sont marqués comme accessibles et sont tous deux désalloués . Le traçage des récupérateurs s'exécute à certains intervalles, interrompant tous les threads, ce qui peut entraîner des performances incohérentes (pauses sporadiques). L'algorithme décrit ici est une description très basique, les GC modernes sont généralement beaucoup plus avancés en utilisant un système de génération d'objets, des ensembles tricolores, etc., et effectuent également d'autres tâches telles que la défragmentation de l'espace mémoire du programme en déplaçant les objets vers un stockage contigu l'espace, c'est la raison pour laquelle les langages GC tels que C # et Java n'autorisent pas les pointeurs. Une faiblesse importante du traçage des ramasse-miettes est que les destructeurs de classe ne sont plus déterministes, c'est le programmeur ne peut pas dire quand un objet va être récupéré. Les langages GC en fait ne permettent même pas au programmeur de spécifier un destructeur de classe, donc les classes ne peuvent plus être utilisées pour encapsuler la gestion des ressources telles que les descripteurs de fichiers, les connexions à la base de données , etc. La responsabilité est laissée au programmeur de fermer les fichiers ouverts, les connexions à la base de données manuellement, d'où la raison pour laquelle des langages tels que Java ont un mot-clé finally (dans le bloc try, catch) pour s'assurer que le nettoyer up code est toujours exécuté avant que la pile ne se déroule, alors qu'en C++ (pas de GC) ces ressources sont gérées par un objet wrapper (alloué sur la pile) qui acquiert la ressource dans le constructeur et la libère dans le destructeur, qui est toujours appelé comme l'objet est retiré de la pile.

Quant à la performance, les deux ont des pénalités de performance. Le comptage automatique des références offre des performances plus cohérentes, sans pause, mais ralentit votre application dans son ensemble car chaque affectation d'un objet à une variable, chaque désallocation d'un objet, etc., aura besoin d'une incrémentation/décrémentation associée du compteur de référence, et prendre soin de réaffecter les références faibles et d'appeler chaque destructeur de chaque objet en cours de désallocation. GC n'a pas la pénalité de performance d'ARC lorsqu'il s'agit de références d'objet; cependant, il encourt des pauses pendant qu'il collecte des ordures (rendant inutilisable pour les systèmes de traitement en temps réel) et nécessite un grand espace mémoire pour qu'il fonctionne efficacement de sorte qu'il ne soit pas forcé de s'exécuter, interrompant ainsi l'exécution trop souvent.

Comme vous pouvez le voir, les deux ont leurs propres avantages et inconvénients, il n'y a pas de coupure nette ARC est meilleur ou GC est meilleur, les deux sont des compromis.

PS: ARC devient également problématique lorsque les objets sont partagés sur plusieurs threads nécessitant une incrémentation/décrémentation atomique du compteur de référence, qui lui-même présente un tout nouveau tableau de complexités et de problèmes. Cela devrait répondre à votre question sur "pourquoi quelqu'un utiliserait-il la collecte des ordures".

71
ALXGTV

L'instructeur a tort. Vous savez mieux comment fonctionne la collecte des ordures et le comptage des références.

Avec la récupération de place, le problème est que vous avez peut-être encore laissé une référence à un objet quelque part. Il y a eu un cas où un premier véhicule autonome s'est écrasé parce que les références aux informations sur les emplacements précédents étaient stockées dans un tableau et ne sont jamais devenues des ordures, donc après 45 minutes, il a manqué de mémoire et s'est écrasé. Je pense que pas littéralement, il a cessé de rouler, mais il aurait pu aussi s'écraser.

Avec le comptage de références, le problème est que vous pouvez avoir des références cycliques A-> B-> A ou A-> B-> C -> ...-> Z-> A, et aucun comptage de référence ne va jamais à zéro. C'est pourquoi vous avez des références faibles et vous devez savoir quand les utiliser.

Dans les deux cas, vous devez comprendre comment les choses fonctionnent, sinon vous aurez des ennuis. En ce qui concerne les performances, si vous demandez Java disent que le nettoyage de la mémoire est plus rapide; si vous demandez aux développeurs Objective-C, ils disent que le comptage des références est plus rapide. Les études prouvent ce qu'ils veulent prouver. Si cela fait une différence, vous devez réduire le nombre d'allocations, pas changer de langue.

Vous devez également connaître références faibles , essentiellement une référence à un objet qui ne maintient pas l'objet en vie. Et vous devez savoir ce qui se passe exactement une fois qu'il a été décidé qu'un objet devrait être jeté; dans Java Je pense qu'il y a des façons dont un objet pourrait redevenir vivant, dans Objective-C/Swift une fois que le nombre de références est nul, cet objet va pour s'en aller, peu importe ce que vous essayez de conserver. Eh bien, sauf si vous ajoutez une ligne pour (;;); dans la méthode dealloc/deinit :-(

9
gnasher729

La gestion manuelle de la mémoire, le comptage des références et la collecte des ordures ont tous leurs avantages et leurs inconvénients:

  • Gestion manuelle de la mémoire: imbattable rapidement, mais sujette à des bugs en raison d'erreurs de libération de la mémoire. De plus, vous devrez souvent implémenter vous-même au moins le comptage des références en plus de la gestion manuelle de la mémoire lorsque vous obtenez plusieurs objets qui nécessitent tous qu'un seul objet reste en vie.

  • Comptage de références: petit surcoût (incrémenter/décrémenter un compteur et un contrôle zéro n'est pas si cher), permet une gestion facile de structures de données assez complexes, où chaque objet peut être référencé par plusieurs autres. L'inconvénient est que le comptage des références nécessite que les références ne soient pas circulaires. Une fois que vous obtenez des cercles de référence, vous perdez de la mémoire.

    Des références faibles peuvent être utilisées pour briser certains cycles de référence, cependant, elles entraînent pas mal de coûts supplémentaires:

    1. Les références faibles nécessitent un deuxième décompte de références pour gérer la référence faible elle-même. Probablement, la référence faible est un autre objet qui doit être alloué indépendamment, entraînant une surcharge importante de consommation de mémoire.

    2. La destruction d'un objet en présence de références faibles nécessite la réinitialisation atomique de la référence faible qui appartient à l'objet et la décrémentation du décompte de références. Sinon, vous obtenez un comportement erratique des références faibles. Je ne suis pas dans les détails, mais je pense que cela peut être difficile à réaliser sans verrouillage.

    Tout cela peut être fait, mais ce n'est pas aussi simple que le comptage de références sans références faibles.

  • Collecte des ordures: peut gérer tous les graphiques de dépendance possibles, mais a un impact assez grave sur les performances. Après tout, le garbage collector doit prouver en quelque sorte qu'un objet n'est plus accessible avant de pouvoir le récupérer. Les éboueurs modernes sont assez bons pour éviter les longs retards pendant qu'ils font leur travail, mais le travail doit être fait d'une manière ou d'une autre. Cela est particulièrement mauvais pour les applications en temps réel qui doivent garantir une réponse dans un délai donné.

Comme vous le voyez, les trois méthodes ont toutes des situations où elles sont les meilleures: si vous pouvez facilement faire une gestion manuelle et que votre programme a des contraintes de performances strictes, la gestion manuelle de la mémoire peut être la voie à suivre. Et tant que vous pouvez utiliser le comptage de références sur la collecte des ordures, c'est beaucoup plus rapide et ne génère pas de retards parasites. Néanmoins, vous devez parfois utiliser la récupération de place car vous ne pouvez pas garantir des références sans cycle.

Les langages qui utilisent le garbage collection pour tout viennent de décider que la facilité d'utilisation est plus importante pour eux que de permettre des applications sensibles aux performances.