.NET Framework
Collecte des ordures
Recherche…
Introduction
Dans .Net, les objets créés avec new () sont alloués sur le tas géré. Ces objets ne sont jamais explicitement finalisés par le programme qui les utilise; à la place, ce processus est contrôlé par le ramasse-miettes .Net.
Certains des exemples ci-dessous sont des "cas pratiques" pour montrer le Garbage Collector au travail et des détails importants sur son comportement, tandis que d'autres se concentrent sur la manière de préparer les classes pour une manipulation correcte par le Garbage Collector.
Remarques
Le Garbage Collector vise à réduire le coût du programme en termes de mémoire allouée, mais cela a un coût en termes de temps de traitement. Afin de parvenir à un bon compromis global, un certain nombre d’optimisations doivent être prises en compte lors de la programmation en gardant à l’esprit le Garbage Collector:
- Si la méthode Collect () doit être explicitement appelée (ce qui ne devrait pas être le cas de toute façon), envisagez d'utiliser le mode "optimisé" qui finalise l'objet mort uniquement lorsque la mémoire est réellement nécessaire
- Au lieu d'appeler la méthode Collect (), envisagez d'utiliser les méthodes AddMemoryPressure () et RemoveMemoryPressure (), qui déclenchent une collecte de mémoire uniquement si nécessaire
- Une collection de mémoire n'est pas garantie pour finaliser tous les objets morts; au lieu de cela, le garbage collector gère 3 "générations", un objet parfois "survivant" d'une génération à l'autre
- Plusieurs modèles de threads peuvent s'appliquer, en fonction de divers facteurs, y compris le réglage fin de la configuration, entraînant différents degrés d'interférence entre le thread du garbage collector et les autres threads d'application.
Un exemple basique de collection (garbage)
Compte tenu de la classe suivante:
public class FinalizableObject
{
public FinalizableObject()
{
Console.WriteLine("Instance initialized");
}
~FinalizableObject()
{
Console.WriteLine("Instance finalized");
}
}
Un programme qui crée une instance, même sans l'utiliser:
new FinalizableObject(); // Object instantiated, ready to be used
Produit la sortie suivante:
<namespace>.FinalizableObject initialized
Si rien d'autre ne se produit, l'objet n'est pas finalisé jusqu'à ce que le programme se termine (ce qui libère tous les objets sur le tas géré, finalisant ceux-ci dans le processus).
Il est possible de forcer le récupérateur de place à s'exécuter à un point donné, comme suit:
new FinalizableObject(); // Object instantiated, ready to be used
GC.Collect();
Qui produit le résultat suivant:
<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized
Cette fois, dès que le récupérateur de place a été appelé, l'objet inutilisé (aka "dead") a été finalisé et libéré du tas géré.
Objets vivants et objets morts - les bases
Règle de base: lorsque la récupération de place se produit, les «objets actifs» sont ceux qui sont encore utilisés, tandis que les «objets morts» sont ceux qui ne sont plus utilisés (toute variable ou champ les référençant) .
Dans l'exemple suivant (par commodité, FinalizableObject1 et FinalizableObject2 sont des sous-classes de FinalizableObject de l'exemple ci-dessus et héritent donc du comportement du message d'initialisation / finalisation):
var obj1 = new FinalizableObject1(); // Finalizable1 instance allocated here
var obj2 = new FinalizableObject2(); // Finalizable2 instance allocated here
obj1 = null; // No more references to the Finalizable1 instance
GC.Collect();
Le résultat sera:
<namespace>.FinalizableObject1 initialized
<namespace>.FinalizableObject2 initialized
<namespace>.FinalizableObject1 finalized
Au moment où le récupérateur de place est appelé, FinalizableObject1 est un objet mort et est finalisé, tandis que FinalizableObject2 est un objet actif et qu'il est conservé sur le segment de mémoire géré.
Plusieurs objets morts
Que se passe-t-il si deux (ou plusieurs) objets morts se réfèrent l'un à l'autre? Ceci est illustré dans l'exemple ci-dessous, en supposant que OtherObject est une propriété publique de FinalizableObject:
var obj1 = new FinalizableObject1();
var obj2 = new FinalizableObject2();
obj1.OtherObject = obj2;
obj2.OtherObject = obj1;
obj1 = null; // Program no longer references Finalizable1 instance
obj2 = null; // Program no longer references Finalizable2 instance
// But the two objects still reference each other
GC.Collect();
Cela produit la sortie suivante:
<namespace>.FinalizedObject1 initialized
<namespace>.FinalizedObject2 initialized
<namespace>.FinalizedObject1 finalized
<namespace>.FinalizedObject2 finalized
Les deux objets sont finalisés et libérés du tas géré même s'ils se référencent l'un l'autre (car aucune autre référence n'existe à aucun d'entre eux à partir d'un objet réel).
Références faibles
Les références faibles sont ... des références, à d'autres objets (alias "cibles"), mais "faibles" car elles n'empêchent pas ces objets d'être collectés. En d'autres termes, les références faibles ne comptent pas lorsque le récupérateur de place évalue les objets en tant que "live" ou "dead".
Le code suivant:
var weak = new WeakReference<FinalizableObject>(new FinalizableObject());
GC.Collect();
Produit la sortie:
<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized
L'objet est libéré du tas géré bien qu'il soit référencé par la variable WeakReference (toujours dans l'étendue lorsque le récupérateur de place a été appelé).
Conséquence n ° 1: à tout moment, il est dangereux de supposer qu'une cible WeakReference est toujours allouée ou non sur le segment de mémoire géré.
Conséquence n ° 2: chaque fois qu'un programme doit accéder à la cible d'une référence faible, le code doit être fourni pour les deux cas, la cible étant toujours allouée ou non. La méthode pour accéder à la cible est TryGetTarget:
var target = new object(); // Any object will do as target
var weak = new WeakReference<object>(target); // Create weak reference
target = null; // Drop strong reference to the target
// ... Many things may happen in-between
// Check whether the target is still available
if(weak.TryGetTarget(out target))
{
// Use re-initialized target variable
// To do whatever the target is needed for
}
else
{
// Do something when there is no more target object
// The target variable value should not be used here
}
La version générique de WeakReference est disponible depuis .Net 4.5. Toutes les versions du framework fournissent une version non générique, non typée, construite de la même manière et vérifiée comme suit:
var target = new object(); // Any object will do as target
var weak = new WeakReference(target); // Create weak reference
target = null; // Drop strong reference to the target
// ... Many things may happen in-between
// Check whether the target is still available
if (weak.IsAlive)
{
target = weak.Target;
// Use re-initialized target variable
// To do whatever the target is needed for
}
else
{
// Do something when there is no more target object
// The target variable value should not be used here
}
Dispose () vs. finalizers
Implémentez la méthode Dispose () (et déclarez la classe contenant comme IDisposable) pour vous assurer que toutes les ressources dont la mémoire est lourde sont libérées dès que l'objet n'est plus utilisé. Le "catch" est qu’il n’ya pas de garantie forte que la méthode Dispose () soit invoquée (contrairement aux finaliseurs qui sont toujours appelés à la fin de la vie de l’objet).
Un scénario est un programme appelant Dispose () sur les objets qu'il crée explicitement:
private void SomeFunction()
{
// Initialize an object that uses heavy external resources
var disposableObject = new ClassThatImplementsIDisposable();
// ... Use that object
// Dispose as soon as no longer used
disposableObject.Dispose();
// ... Do other stuff
// The disposableObject variable gets out of scope here
// The object will be finalized later on (no guarantee when)
// But it no longer holds to the heavy external resource after it was disposed
}
Un autre scénario consiste à déclarer une classe instanciée par le framework. Dans ce cas, la nouvelle classe hérite généralement d'une classe de base. Par exemple, dans MVC, une classe de contrôleur est créée en tant que sous-classe de System.Web.Mvc.ControllerBase. Lorsque la classe de base implémente l'interface IDisposable, il s'agit d'une bonne indication que Dispose () serait correctement invoqué par le framework, mais là encore, il n'y a pas de garantie forte.
Ainsi, Dispose () ne remplace pas un finaliseur; au lieu de cela, les deux devraient être utilisés à des fins différentes:
- Un finaliseur libère éventuellement des ressources pour éviter les fuites de mémoire qui se produiraient autrement
- Dispose () libère les ressources (éventuellement les mêmes) dès que celles-ci ne sont plus nécessaires, pour alléger la pression sur l'allocation globale de la mémoire.
Élimination correcte et finalisation des objets
Comme Dispose () et les finaliseurs visent des objectifs différents, une classe gérant des ressources externes lourdes de mémoire doit les implémenter. La conséquence est d’écrire la classe pour qu’elle gère bien deux scénarios possibles:
- Lorsque seul le finaliseur est appelé
- Lorsque Dispose () est invoqué en premier et plus tard, le finaliseur est également appelé
Une solution consiste à écrire le code de nettoyage de telle sorte que son exécution une ou deux fois produirait le même résultat qu’une exécution unique. La faisabilité dépend de la nature du nettoyage, par exemple:
- Fermer une connexion à une base de données déjà fermée n'aurait probablement aucun effet, donc cela fonctionne
- La mise à jour de certains «comptes d'utilisation» est dangereuse et produirait un résultat erroné lorsqu'elle est appelée deux fois au lieu d'une fois.
Une solution plus sûre consiste à s'assurer, par conception, que le code de nettoyage est appelé une seule fois, quel que soit le contexte externe. Cela peut être réalisé de manière classique en utilisant un drapeau dédié:
public class DisposableFinalizable1: IDisposable
{
private bool disposed = false;
~DisposableFinalizable1() { Cleanup(); }
public void Dispose() { Cleanup(); }
private void Cleanup()
{
if(!disposed)
{
// Actual code to release resources gets here, then
disposed = true;
}
}
}
Alternativement, Garbage Collector fournit une méthode spécifique SuppressFinalize () qui permet d'ignorer le finaliseur après l'appel de Dispose:
public class DisposableFinalizable2 : IDisposable
{
~DisposableFinalizable2() { Cleanup(); }
public void Dispose()
{
Cleanup();
GC.SuppressFinalize(this);
}
private void Cleanup()
{
// Actual code to release resources gets here
}
}