Est correct d'utiliser GC.Collect(); GC.WaitForPendingFinalizers ();?

J'ai commencé à revoir du code dans un projet et j'ai trouvé quelque chose comme ceci:

GC.Collect();
GC.WaitForPendingFinalizers();

Ces lignes apparaissent généralement sur des méthodes conçues pour détruire l'objet sous la justification de l'augmentation de l'efficacité. J'ai fait cette remarque:

  1. appeler explicitement garbage collection sur la destruction de chaque objet diminue les performances car le faire n'a pas en compte si c'est absolument nécessaire pour les performances CLR.
  2. appeler ces instructions dans cet ordre provoque la destruction de chaque objet uniquement si d'autres objets sont en cours de finalisation. Par conséquent, un objet qui pourrait être détruit indépendamment doit attendre la destruction d'un autre objet sans nécessité réelle.
  3. , Il peut générer un blocage (voir: cette question)

Est-ce que 1, 2 et 3 sont vrais? Pouvez-vous donner des références à l'appui de vos réponses?

Bien que je sois presque sûr de mes remarques, je dois être clair dans mes arguments afin d'expliquer à mon l'équipe pourquoi est-ce un problème . C'est la raison pour laquelle je demande confirmation et référence.

27
demandé sur Community 2012-09-04 18:26:18

6 réponses

La réponse courte est: sortir. Ce code va presque jamais améliorer les performances, ou l'utilisation de la mémoire à long terme.

Tous vos points sont vrais. (Il peut générer un blocage; cela ne signifie pas toujours sera.) Calling {[2] } collectera la mémoire de toutes les générations de GC. Cela ne signifie deux choses.

  • Il recueille à travers toutes les générations chaque fois - au lieu de ce que le GC fera par défaut, qui est de ne collecter qu'un génération quand il est plein. L'utilisation typique verra Gen0 collecter (environ) dix fois plus souvent que Gen1, qui à son tour recueille (environ) dix fois plus souvent que Gen2. Ce code collectera toutes les générations à chaque fois. La collection Gen0 est généralement inférieure à 100 ms; Gen2 peut être beaucoup plus longue.
  • Il favorise les objets non collectables à la prochaine génération. C'est-à-dire que chaque fois que vous forcez une collection et que vous avez toujours une référence à un objet, cet objet sera promu au la génération suivante. Typiquement, cela se produira relativement rarement, mais le code tel que ci-dessous forcera beaucoup plus souvent:

    void SomeMethod()
    { 
     object o1 = new Object();
     object o2 = new Object();
    
     o1.ToString();
     GC.Collect(); // this forces o2 into Gen1, because it's still referenced
     o2.ToString();
    }
    

Sans GC.Collect(), ces deux éléments seront collectés à la prochaine occasion. avec la collection en écriture, {[4] } finira dans Gen1 - ce qui signifie qu'une collection gen0 automatisée ne libérera pas cette mémoire.

Il est également intéressant de noter une horreur encore plus grande: en mode débogage, le GC fonctionne différemment et ne récupérera Aucune variable qui est encore dans la portée (même si elle n'est pas utilisée plus tard dans la méthode actuelle). Donc, en mode débogage, le code ci-dessus ne collecterait même pas o1 lors de l'appel de GC.Collect, et donc o1 et o2 seront promus. Cela pourrait conduire à une utilisation de la mémoire très erratique et inattendue lors du débogage du code. (Des Articles tels que this mettent en évidence ce comportement.)

EDIT: après avoir juste testé ce comportement, une véritable ironie: si vous avez une méthode quelque chose comme ce:

void CleanUp(Thing someObject)
{
    someObject.TidyUp();
    someObject = null;
    GC.Collect();
    GC.WaitForPendingFinalizers(); 
}

... ensuite, il ne libérera explicitement pas la mémoire de someObject, même en mode RELEASE: il le promouvra dans la prochaine génération de GC.

24
répondu Dan Puzey 2012-09-04 14:55:36

Il y a un point que l'on peut faire qui est très facile à comprendre: avoir GC run nettoie automatiquement de nombreux objets par exécution (disons, 10000). L'appeler après chaque destruction nettoie environ un objet par exécution.

Parce que GC a une surcharge élevée (doit arrêter et démarrer les threads, doit analyser tous les objets vivants), les appels par lots sont hautement préférables.

Aussi, bon pourrait sortir de nettoyage après chaque objet? Comment cela pourrait-il être plus efficace que le dosage?

8
répondu usr 2012-09-04 14:41:24

Votre point numéro 3 est techniquement correct, mais ne peut se produire que si quelqu'un se verrouille pendant un finaliseur.

Même sans ce genre d'appel, le verrouillage à l'intérieur d'un finaliseur est encore pire que ce que vous avez ici.

Il y a une poignée de fois où l'appel GC.Collect() aide vraiment les performances.

Jusqu'à présent, je l'ai fait 2, peut-être 3 fois dans ma carrière. (Ou peut-être environ 5 ou 6 fois si vous incluez ceux où je l'ai fait, mesuré les résultats, puis l'a sorti à nouveau - et c'est quelque chose que vous devriez toujours mesurer après avoir fait).

Dans les cas où vous parcourez des centaines ou des milliers de mégas de mémoire en peu de temps, puis passez à une utilisation beaucoup moins intensive de la mémoire pendant une longue période, Cela peut être une amélioration massive ou même vitale à collecter explicitement. C'est que ce qui se passe ici?

Partout ailleurs, ils vont au mieux le rendre plus lent et utiliser plus de mémoire.

6
répondu Jon Hanna 2012-09-05 00:10:31

Voir mon autre réponse ici:

Au GC.Collecter ou pas?

Deux choses peuvent arriver lorsque vous appelez GC.Collect () vous-même: vous finissez par passer plus de temps à faire des collections (car les collections d'arrière-plan normales se produiront toujours en plus de votre GC manuel.Collect()) et vous vous accrocherez à la mémoire plus longue (parce que vous avez forcé certaines choses à une génération d'ordre supérieur qui n'avait pas besoin d'y aller). En d'autres termes, en utilisant des GC.Collect() vous-même est presque toujours une mauvaise idée.

À propos de la seule fois où vous voulez appeler GC.Collect () vous-même est lorsque vous avez des informations spécifiques sur votre programme qui sont difficiles à connaître pour le garbage Collector. L'exemple canonique est un programme de longue durée avec des cycles de charge occupés et légers distincts. Vous pouvez forcer une collection vers la fin d'une période de charge légère, avant un cycle occupé, pour vous assurer que les ressources sont aussi libres que possible pour le cycle occupé. Mais même ici, vous pourriez trouver que vous faites mieux en repensant comment votre application est construite (c'est-à-dire, une tâche planifiée fonctionnerait-elle mieux?).

4
répondu Joel Coehoorn 2017-05-23 12:24:56

Je l'ai utilisé une seule fois: pour nettoyer le cache côté serveur des documents Crystal Report. Voir ma réponse dans Exception Crystal Reports: la limite maximale des tâches de traitement des rapports configurée par votre administrateur système a été atteinte

Le WaitForPendingFinalizers m'a été particulièrement utile, car parfois les objets n'étaient pas nettoyés correctement. Compte tenu de la performance relativement lente du rapport dans une page web-tout retard mineur du GC était négligeable, et le l'amélioration de la gestion de la mémoire a donné un serveur globalement plus heureux pour moi.

1
répondu gojimmypi 2017-05-23 12:09:14

Nous avons rencontré des problèmes similaires à @Grzenio mais nous travaillons avec des tableaux 2 dimensions beaucoup plus grands, de l'ordre de 1000x1000 à 3000x3000, c'est dans un webservice.

Ajouter plus de mémoire n'est pas toujours la bonne réponse, vous devez comprendre votre code et le cas d'utilisation. Sans la collecte GC, nous avons besoin de 16 à 32 Go de mémoire (selon la taille du client). Sans cela, nous aurions besoin de 32-64gb de mémoire et même alors, il n'y a aucune garantie que le système ne souffrira pas. filet garbage collector n'est pas parfait.

Notre webservice a un cache en mémoire de l'ordre de 5-50 millions de chaînes (~80-140 caractères par paire clé / valeur en fonction de la configuration), en plus avec chaque demande du client, nous construirions 2 matrices une de double, une de boolean qui ont ensuite été transmises à un autre service pour faire le travail. Pour une "matrice" 1000x1000 (tableau à 2 dimensions), c'est ~25mb, par requête. Le booléen dirait quels éléments nous avons besoin (en fonction de notre cache). Chacun entrée de cache représente une "cellule" dans la "matrice".

Les performances du cache se dégradent considérablement lorsque le serveur utilise plus de 80% de la mémoire en raison de la pagination.

Ce que nous avons trouvé, c'est que si nous N'avions pas explicitement GC, le garbage collector.Net ne "nettoyerait" jamais les variables transitoires jusqu'à ce que nous soyons dans la plage de 90-95% à quel point les performances du cache s'étaient considérablement dégradées.

Étant donné que le processus descendant prenait souvent une longue durée (3-900 secondes), les performances atteignaient D'une collection GC était neglible (3-10 secondes par collection). Nous avons lancé cette collecte après que nous avions déjà retourné la réponse au client.

En fin de compte, nous avons rendu les paramètres GC configurables, également avec. net 4.6 il y a d'autres options. Voici le code. net 4.5 que nous avons utilisé.

if (sinceLastGC.Minutes > Service.g_GCMinutes)
{
     Service.g_LastGCTime = DateTime.Now;
     var sw = Stopwatch.StartNew();
     long memBefore = System.GC.GetTotalMemory(false);
     context.Response.Flush();
     context.ApplicationInstance.CompleteRequest();
     System.GC.Collect( Service.g_GCGeneration, Service.g_GCForced ? System.GCCollectionMode.Forced : System.GCCollectionMode.Optimized);
     System.GC.WaitForPendingFinalizers();
     long memAfter = System.GC.GetTotalMemory(true);
     var elapsed = sw.ElapsedMilliseconds;
     Log.Info(string.Format("GC starts with {0} bytes, ends with {1} bytes, GC time {2} (ms)", memBefore, memAfter, elapsed));
}

Après la réécriture pour une utilisation avec. net 4.6, nous divisons le Colleciton de déchets en 2 étapes - une simple collecte et une collecte de compactage.

    public static RunGC(GCParameters param = null)
    {
        lock (GCLock)
        {
            var theParams = param ?? GCParams;
            var sw = Stopwatch.StartNew();
            var timestamp = DateTime.Now;
            long memBefore = GC.GetTotalMemory(false);
            GC.Collect(theParams.Generation, theParams.Mode, theParams.Blocking, theParams.Compacting);
            GC.WaitForPendingFinalizers();
            //GC.Collect(); // may need to collect dead objects created by the finalizers
            var elapsed = sw.ElapsedMilliseconds;
            long memAfter = GC.GetTotalMemory(true);
            Log.Info($"GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");

        }
    }

    // https://msdn.microsoft.com/en-us/library/system.runtime.gcsettings.largeobjectheapcompactionmode.aspx
    public static RunCompactingGC()
    {
        lock (CompactingGCLock)
        {
            var sw = Stopwatch.StartNew();
            var timestamp = DateTime.Now;
            long memBefore = GC.GetTotalMemory(false);

            GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
            GC.Collect();
            var elapsed = sw.ElapsedMilliseconds;
            long memAfter = GC.GetTotalMemory(true);
            Log.Info($"Compacting GC starts with {memBefore} bytes, ends with {memAfter} bytes, GC time {elapsed} (ms)");
        }
    }

J'espère que cela aide quelqu'un d'autre comme nous passé beaucoup de temps à la recherche de ce.

1
répondu Justin 2017-11-06 19:28:47