Non puoi davvero fare affermazioni generali sul modo appropriato di utilizzare tutte le implementazioni di GC. Variano selvaggiamente. Quindi parlerò con quello a cui ti riferivi originariamente.
È necessario conoscere il comportamento del GC piuttosto intimamente per farlo con qualsiasi logica o ragione.
L'unico consiglio sulla raccolta che posso dare è: non farlo mai.
Se conosci veramente i dettagli intricati del GC, non avrai bisogno dei miei consigli, quindi non importa. Se non lo sai già con la certezza del 100%, sarà di aiuto, e dovrai cercare online e trovare una risposta come questa: non dovresti chiamare GC.Collect , o in alternativa: dovresti andare a conoscere i dettagli di come funziona il GC dentro e fuori, e solo allora conoscerai la risposta .
C'è un posto sicuro in cui ha senso usare GC.Collect :
GC.Collect è un'API disponibile che puoi utilizzare per la profilazione dei tempi delle cose. Potresti profilare un algoritmo, raccogliere e profilare un altro algoritmo subito dopo sapendo che GC del primo algo non si stava verificando durante il secondo alterando i risultati.
Questo tipo di profilazione è la sola volta che suggerirei di raccogliere manualmente a chiunque.
Esempio contrastato comunque
Un possibile caso d'uso è se carichi cose molto grandi, finiranno nell'Heap degli oggetti grandi che andrà direttamente alla Gen 2, anche se ancora Gen 2 è per oggetti di lunga durata perché raccoglie meno frequentemente. Se sai che stai caricando oggetti di breve durata in Gen 2 per qualsiasi motivo, potresti eliminarli più rapidamente per mantenere la tua Gen 2 più piccola e le sue raccolte più veloci.
Questo è il miglior esempio che potrei trovare, e non è buono - la pressione LOH che stai costruendo qui causerebbe raccolte più frequenti, e le raccolte sono così frequenti come è - è probabile che eliminerebbe il LOH proprio come veloce mentre lo facevi esplodere con oggetti temporanei. Ho semplicemente non mi fido di me presumere una frequenza di raccolta migliore rispetto al GC in sé - messo a punto da persone lontane gran lunga più intelligenti di me
Quindi parliamo di alcune delle semantiche e dei meccanismi nel GC GC ... o ...
Tutto quello che penso di sapere sul GC GC
Per favore, chiunque trovi errori qui - correggimi. Gran parte del GC è ben noto per essere magia nera e mentre cercavo di tralasciare dettagli di cui non ero sicuro, probabilmente ho ancora sbagliato qualcosa.
Di seguito mancano di proposito numerosi dettagli di cui non sono sicuro, oltre a un corpus di informazioni molto più ampio di cui sono semplicemente ignaro. Utilizzare queste informazioni a proprio rischio.
Concetti GC
.NET GC si verifica in momenti incoerenti, motivo per cui si chiama "non deterministico", ciò significa che non si può fare affidamento sul fatto che si verifichi in momenti specifici. È anche un garbage collector generazionale, il che significa che suddivide i tuoi oggetti in quanti passaggi GC hanno attraversato.
Gli oggetti nell'heap di Gen 0 hanno attraversato 0 raccolte, queste sono state appena realizzate, quindi di recente non si è verificata alcuna raccolta dalla loro istanza. Gli oggetti nel tuo heap di Gen 1 hanno vissuto un passaggio di raccolta e allo stesso modo gli oggetti nel tuo heap di Gen 2 hanno vissuto 2 passaggi di raccolta.
Ora vale la pena notare il motivo per cui qualifica queste specifiche generazioni e partizioni di conseguenza. .NET GC riconosce solo queste tre generazioni, perché i passaggi di raccolta che superano questi tre cumuli sono leggermente diversi. Alcuni oggetti potrebbero sopravvivere a passaggi di raccolta migliaia di volte. Il GC lascia semplicemente questi dall'altro lato della partizione heap di Gen 2, non ha senso partizionarli ulteriormente perché sono in realtà Gen 44; il passaggio di raccolta su di essi è uguale a tutto nell'heap di seconda generazione.
Ci sono scopi semantici per queste generazioni specifiche, così come meccanismi implementati che le onorano, e arriverò a quelli in un momento.
Cosa c'è in una collezione
Il concetto di base di un passaggio di raccolta GC è che controlla ogni oggetto in uno spazio heap per vedere se ci sono ancora riferimenti attivi (radici GC) a questi oggetti. Se viene trovata una radice GC per un oggetto, significa che attualmente l'esecuzione del codice può ancora raggiungere e utilizzare quell'oggetto, quindi non può essere eliminato. Tuttavia, se una radice GC non viene trovata per un oggetto, significa che il processo in esecuzione non ha più bisogno dell'oggetto, quindi può rimuoverlo per liberare memoria per i nuovi oggetti.
Ora dopo aver finito di ripulire un gruppo di oggetti e di lasciarne alcuni da soli, ci sarà uno sfortunato effetto collaterale: spazi vuoti tra gli oggetti vivi dove sono stati rimossi quelli morti. Questa frammentazione della memoria, se lasciata sola, sprecherebbe semplicemente memoria, quindi le raccolte in genere fanno ciò che viene chiamato "compattazione" dove prendono tutti gli oggetti vivi rimasti e li stringono insieme nell'heap in modo che la memoria libera sia contigua su un lato dell'heap per Gen 0.
Ora, data l'idea di 3 cumuli di memoria, tutti partizionati dal numero di passaggi di raccolta che hanno vissuto, parliamo del perché esistono queste partizioni.
Collezione Gen 0
Essendo la gen 0 gli oggetti più recenti in assoluto, tende ad essere molto piccola, quindi puoi tranquillamente raccoglierla molto frequentemente . La frequenza assicura che l'heap rimanga piccolo e le raccolte siano molto veloci perché si stanno accumulando su un heap così piccolo. Questo si basa più o meno su un'euristica che afferma: una grande maggioranza di oggetti temporanei che crei sono molto temporanei, così temporanei che non saranno più utilizzati o referenziati quasi immediatamente dopo l'uso, e quindi possono essere raccolti.
Collezione Gen 1
Essendo la 1a generazione oggetti che non rientravano in questa categoria di oggetti molto temporanea, potrebbero avere una vita piuttosto breve, poiché ancora - una vasta porzione degli oggetti creati non viene utilizzata a lungo. Pertanto anche la Gen 1 raccoglie piuttosto frequentemente, mantenendo di nuovo l'heap piccolo e le raccolte sono veloci. Tuttavia, il presupposto è minore dei suoi oggetti sono temporanei rispetto alla Gen 0, quindi raccoglie meno frequentemente della Gen 0
Dirò che sinceramente non conosco i meccanismi tecnici che differiscono tra i passi di raccolta della Gen 0 e quelli della Gen 1, se ce ne sono altri diversi dalla frequenza che raccolgono.
Collezione Gen 2
Gen 2 ora deve essere la madre di tutti i cumuli giusto? Bene, sì, è più o meno giusto. È dove vivono tutti i tuoi oggetti permanenti - l'oggetto in cui Main()
vivi, per esempio, e tutto ciò che fa Main()
riferimento perché saranno radicati fino al tuo Main()
ritorno alla fine del tuo processo.
Dato che Gen 2 è un secchio praticamente per tutto ciò che le altre generazioni non sono riuscite a collezionare, i suoi oggetti sono in gran parte permanenti o almeno longevi. Quindi riconoscere molto poco di ciò che è nella Gen 2 sarà effettivamente qualcosa che può essere raccolto, non ha bisogno di essere raccolto frequentemente. Ciò consente alla raccolta di essere anche più lenta, poiché viene eseguita molto meno frequentemente. Quindi questo è fondamentalmente il punto in cui hanno affrontato tutti i comportamenti extra per scenari strani, perché hanno il tempo di eseguirli.
Mucchio di oggetti di grandi dimensioni
Un esempio dei comportamenti extra di Gen 2 è che esegue anche la raccolta sull'heap di oggetti di grandi dimensioni. Fino ad ora ho parlato interamente di Small Object Heap, ma il runtime .NET alloca cose di determinate dimensioni a un heap separato a causa di ciò che ho definito compattazione sopra. La compattazione richiede lo spostamento di oggetti quando le raccolte finiscono nell'heap di piccoli oggetti. Se c'è un oggetto 10mb vivente nella prima generazione, ci vorrà molto più tempo per completare la compattazione dopo la raccolta, rallentando così la raccolta della prima generazione. In modo che l'oggetto 10mb sia allocato all'heap di oggetti di grandi dimensioni e raccolto durante la Gen 2 che viene eseguito di rado.
finalizzazione
Un altro esempio sono gli oggetti con finalizzatori. È stato inserito un finalizzatore su un oggetto che fa riferimento a risorse oltre l'ambito di .NETs GC (risorse non gestite). Il finalizzatore è l'unico modo in cui il GC può richiedere la raccolta di una risorsa non gestita: implementate il finalizzatore per eseguire la raccolta / rimozione / rilascio manuale della risorsa non gestita per garantire che non fuoriesca dal processo. Quando il GC esegue l'esecuzione del finalizzatore degli oggetti, l'implementazione eliminerà la risorsa non gestita, rendendo il GC in grado di rimuovere l'oggetto senza rischiare una perdita di risorse.
Il meccanismo con cui i finalizzatori eseguono questa operazione è il riferimento diretto in una coda di finalizzazione. Quando il runtime alloca un oggetto con un finalizzatore, aggiunge un puntatore a tale oggetto nella coda di finalizzazione e blocca l'oggetto in posizione (chiamato pinning) in modo che la compattazione non lo sposti, interrompendo il riferimento alla coda di finalizzazione. Man mano che si verificano i passaggi di raccolta, alla fine il tuo oggetto non avrà più una radice GC, ma la finalizzazione deve essere eseguita prima di poter essere raccolta. Quindi, quando l'oggetto è morto, la raccolta sposta il suo riferimento dalla coda di finalizzazione e inserisce un riferimento su quella che è nota come coda "FReachable". Quindi la raccolta continua. In un altro momento "non deterministico" nel futuro, un thread separato noto come thread Finalizer passerà attraverso la coda FReachable, eseguendo i finalizzatori per ciascuno degli oggetti a cui viene fatto riferimento. Al termine, la coda FReachable è vuota e si è capovolta un po 'nell'intestazione di ogni oggetto che dice che non necessita di finalizzazione (Questo bit può anche essere capovolto manualmente conGC.SuppressFinalize
che è comune nei Dispose()
metodi), sospetto anche che abbia sbloccato gli oggetti, ma non citarmi su questo. La prossima collezione che si presenta su qualsiasi heap in cui si trova questo oggetto, finalmente lo raccoglierà. Le raccolte Gen 0 non prestano nemmeno attenzione agli oggetti con quel bit necessario per la finalizzazione, li promuove automaticamente, senza nemmeno controllare la loro radice. Un oggetto non rootato che necessita di finalizzazione in Gen 1, verrà lanciato in FReachable
coda, ma la collezione non fa altro, quindi vive in Gen 2. In questo modo, tutti gli oggetti che hanno un finalizzatore e non GC.SuppressFinalize
sarà raccolto in Gen 2.