Comprensione della garbage collection in .NET


170

Considera il codice seguente:

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

Ora, anche se la variabile c1 nel metodo principale non rientra nell'ambito di applicazione e non viene ulteriormente referenziata da nessun altro oggetto quando GC.Collect()viene chiamata, perché non viene finalizzata lì?


8
Il GC non libera immediatamente le istanze quando sono fuori dal campo di applicazione. Lo fa quando pensa che sia necessario. Potete leggere tutto ciò che riguarda il GC qui: msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
user1908061

@ user1908061 (Pssst. Il tuo collegamento è interrotto.)
Dragomok

Risposte:


352

Vieni inciampato qui e stai traendo conclusioni molto sbagliate perché stai usando un debugger. Dovrai eseguire il codice nel modo in cui viene eseguito sul computer dell'utente. Passa prima alla versione di rilascio con Build + Configuration manager, modifica la combinazione "Configurazione soluzione attiva" nell'angolo in alto a sinistra su "Rilascio". Successivamente, vai su Strumenti + Opzioni, Debug, Generale e deseleziona l'opzione "Sopprima ottimizzazione JIT".

Ora esegui di nuovo il programma e armeggia con il codice sorgente. Nota come le parentesi graffe extra non hanno alcun effetto. E nota come l'impostazione della variabile su null non fa alcuna differenza. Stampa sempre "1". Ora funziona come speri e ti aspetti che funzioni.

Che lascia con il compito di spiegare perché funziona in modo così diverso quando si esegue la build di debug. Ciò richiede una spiegazione di come il Garbage Collector individua le variabili locali e come ciò sia influenzato dalla presenza di un debugger.

Prima di tutto, il jitter esegue due compiti importanti quando compila l'IL per un metodo in codice macchina. Il primo è molto visibile nel debugger, puoi vedere il codice macchina con la finestra Debug + Windows + Disassembly. Il secondo dovere è tuttavia completamente invisibile. Genera anche una tabella che descrive come vengono utilizzate le variabili locali all'interno del corpo del metodo. Quella tabella ha una voce per ogni argomento del metodo e variabile locale con due indirizzi. L'indirizzo in cui la variabile memorizzerà prima un riferimento all'oggetto. E l'indirizzo dell'istruzione del codice macchina in cui quella variabile non viene più utilizzata. Anche se quella variabile è memorizzata nel frame dello stack o in un registro cpu.

Questa tabella è essenziale per il Garbage Collector, deve sapere dove cercare i riferimenti agli oggetti quando esegue una raccolta. Abbastanza facile da fare quando il riferimento fa parte di un oggetto sull'heap GC. Sicuramente non facile da fare quando il riferimento all'oggetto è memorizzato in un registro CPU. La tabella dice dove cercare.

L'indirizzo "non più utilizzato" nella tabella è molto importante. Rende il garbage collector molto efficiente . Può raccogliere un riferimento a un oggetto, anche se viene utilizzato all'interno di un metodo e tale metodo non ha ancora terminato l'esecuzione. Il che è molto comune, ad esempio il tuo metodo Main () smetterà mai di essere eseguito solo prima che il tuo programma finisca. Chiaramente non vorrai che nessun riferimento a oggetto utilizzato all'interno di quel metodo Main () vivesse per la durata del programma, il che equivarrebbe a una perdita. Il jitter può usare la tabella per scoprire che una tale variabile locale non è più utile, a seconda di quanto il programma ha progredito all'interno di quel metodo Main () prima di effettuare una chiamata.

Un metodo quasi magico correlato a quella tabella è GC.KeepAlive (). È un metodo molto speciale, non genera alcun codice. Il suo unico dovere è modificare quella tabella. Si estendela durata della variabile locale, impedendo che il riferimento che archivia venga raccolto. L'unica volta che è necessario utilizzarlo è per impedire al GC di essere troppo desideroso di raccogliere un riferimento, che può accadere in scenari di interoperabilità in cui un riferimento viene passato a codice non gestito. Il garbage collector non può vedere tali riferimenti utilizzati da tale codice poiché non è stato compilato dal jitter, quindi non ha la tabella che dice dove cercare il riferimento. Il passaggio di un oggetto delegato a una funzione non gestita come EnumWindows () è l'esempio di boilerplate di quando è necessario utilizzare GC.KeepAlive ().

Quindi, come puoi vedere dal tuo frammento di esempio dopo averlo eseguito nella build di rilascio, le variabili locali possono essere raccolte in anticipo, prima che il metodo abbia terminato l'esecuzione. Ancora più efficacemente, un oggetto può essere raccolto mentre uno dei suoi metodi viene eseguito se tale metodo non fa più riferimento a questo . C'è un problema con questo, è molto imbarazzante eseguire il debug di un tale metodo. Dal momento che è possibile inserire la variabile nella finestra di controllo o ispezionarla. E scomparirebbe durante il debug se si verifica un GC. Sarebbe molto spiacevole, quindi il jitter è consapevole che ci sia un debugger collegato. Quindi modificala tabella e modifica l'indirizzo "ultimo usato". E lo cambia dal suo valore normale all'indirizzo dell'ultima istruzione nel metodo. Il che mantiene viva la variabile fintanto che il metodo non è tornato. Ciò consente di continuare a guardarlo fino a quando il metodo non ritorna.

Questo ora spiega anche cosa hai visto prima e perché hai posto la domanda. Stampa "0" perché la chiamata GC.Collect non è in grado di raccogliere il riferimento. La tabella indica che la variabile è in uso oltre la chiamata GC.Collect (), fino alla fine del metodo. Costretto a dirlo avendo il debugger collegato ed eseguendo il debug build.

L'impostazione della variabile su null ora ha effetto perché il GC controllerà la variabile e non vedrà più un riferimento. Ma assicurati di non cadere nella trappola in cui sono caduti molti programmatori C #, in realtà scrivere quel codice era inutile. Non fa alcuna differenza se quell'istruzione sia presente o meno quando si esegue il codice nella build di rilascio. In effetti, l'ottimizzatore del jitter rimuoverà tale affermazione poiché non ha alcun effetto. Quindi assicurati di non scrivere codice del genere, anche se sembra avere un effetto.


Un'ultima nota su questo argomento, questo è ciò che mette i programmatori nei guai che scrivono piccoli programmi per fare qualcosa con un'app di Office. Il debugger di solito li porta sul percorso sbagliato, vogliono che il programma di Office esca su richiesta. Il modo appropriato per farlo è chiamando GC.Collect (). Scopriranno però che non funziona quando eseguono il debug della loro app, conducendoli in una terra che non ha mai chiamato chiamando Marshal.ReleaseComObject (). Gestione manuale della memoria, raramente funziona correttamente perché trascura facilmente un riferimento all'interfaccia invisibile. GC.Collect () funziona davvero, non solo quando si esegue il debug dell'app.


1
Vedi anche la mia domanda a cui Hans ha risposto bene per me. stackoverflow.com/questions/15561025/…
Dave Nay,

1
@HansPassant Ho appena trovato questa fantastica spiegazione, che risponde anche a una parte della mia domanda qui: stackoverflow.com/questions/30529379/… sulla sincronizzazione di GC e thread. Una domanda che ho ancora: mi chiedo se il GC compatta e aggiorna effettivamente gli indirizzi utilizzati in un registro (archiviato in memoria mentre è sospeso), o semplicemente li salta? Un processo che aggiorna i registri dopo aver sospeso il thread (prima del curriculum) mi sembra un serio thread di sicurezza bloccato dal sistema operativo.
atlaste

Indirettamente sì. Il thread è sospeso, il GC aggiorna l'archivio di backup per i registri della CPU. Quando il thread riprende a funzionare, ora utilizza i valori di registro aggiornati.
Hans Passant,

1
@HansPassant, apprezzerei se aggiungi riferimenti per alcuni dei dettagli non ovvi del Garbage Collector CLR che hai descritto qui?
denfromufa,

Sembra che per quanto riguarda la configurazione, un punto importante è che "Ottimizza codice" ( <Optimize>true</Optimize>in .csproj) è abilitato. Questo è il valore predefinito nella configurazione "Rilascio". Ma nel caso in cui si utilizzino configurazioni personalizzate, è importante sapere che questa impostazione è importante.
Zero3,

34

[Volevo solo aggiungere ulteriori informazioni sul processo di internazionalizzazione della finalizzazione]

Quindi, si crea un oggetto e quando l'oggetto viene raccolto, è Finalizenecessario chiamare il metodo dell'oggetto . Ma c'è molto altro da finalizzare rispetto a questa ipotesi molto semplice.

CONCETTI CORTI ::

  1. Gli oggetti NON implementano Finalizemetodi, lì la memoria viene recuperata immediatamente, a meno che, ovviamente, non siano più raggiungibili dal
    codice dell'applicazione

  2. Oggetti attuazione FinalizeMetodo Il concetto / Applicazione Application Roots, Finalization Queue, Freacheable Queueviene prima che possano essere recuperati.

  3. Qualsiasi oggetto è considerato spazzatura se NON è raggiungibile dal codice dell'applicazione

Assumi :: Classi / Oggetti A, B, D, G, H NON implementano il FinalizeMetodo e C, E, F, I, J implementano il FinalizeMetodo.

Quando un'applicazione crea un nuovo oggetto, il nuovo operatore alloca la memoria dall'heap. Se il tipo di oggetto contiene un Finalizemetodo, un puntatore all'oggetto viene posizionato sulla coda di finalizzazione .

pertanto i puntatori agli oggetti C, E, F, I, J vengono aggiunti alla coda di finalizzazione.

La coda di finalizzazione è una struttura di dati interna controllata dal Garbage Collector. Ogni voce nella coda punta a un oggetto che dovrebbe avere il suo Finalizemetodo chiamato prima che la memoria dell'oggetto possa essere recuperata. La figura seguente mostra un heap contenente diversi oggetti. Alcuni di questi oggetti sono raggiungibili dalle origini dell'applicazionee alcuni non lo sono. Quando sono stati creati gli oggetti C, E, F, I e J, il framework .Net rileva che questi oggetti hanno Finalizemetodi e puntatori a questi oggetti vengono aggiunti alla coda di finalizzazione .

inserisci qui la descrizione dell'immagine

Quando si verifica un GC (1a raccolta), gli oggetti B, E, G, H, I e J vengono determinati come immondizia. Perché A, C, D, F sono ancora raggiungibili dal codice dell'applicazione rappresentato attraverso le frecce dalla casella gialla sopra.

Il garbage collector analizza la coda di finalizzazione alla ricerca di puntatori a questi oggetti. Quando viene trovato un puntatore, il puntatore viene rimosso dalla coda di finalizzazione e aggiunto alla coda freachable ("F-raggiungibile").

La coda freachable è un'altra struttura di dati interna controllata dal Garbage Collector. Ogni puntatore nella coda freachable identifica un oggetto che è pronto per essere Finalizechiamato il suo metodo.

Dopo la raccolta (1a raccolta), l'heap gestito ha un aspetto simile alla figura seguente. Spiegazione fornita di seguito ::
1.) La memoria occupata dagli oggetti B, G e H è stata recuperata immediatamente perché questi oggetti non avevano un metodo di finalizzazione che doveva essere chiamato .

2.) Tuttavia, la memoria occupata dagli oggetti E, I e J non può essere recuperata perché il loro Finalizemetodo non è stato ancora chiamato. La chiamata al metodo Finalize viene eseguita dalla coda freacheable.

3.) A, C, D, F sono ancora raggiungibili tramite il codice dell'applicazione rappresentato attraverso le frecce dalla casella gialla sopra, quindi NON verranno raccolti in nessun caso

inserisci qui la descrizione dell'immagine

Esiste uno speciale thread di runtime dedicato alla chiamata dei metodi Finalize. Quando la coda freachable è vuota (che di solito è il caso), questo thread dorme. Ma quando appaiono le voci, questo thread si riattiva, rimuove ogni voce dalla coda e chiama il metodo Finalize di ciascun oggetto. Il garbage collector compatta la memoria recuperabile e lo speciale thread di runtime svuota la coda freachable , eseguendo il Finalizemetodo di ciascun oggetto . Quindi ecco finalmente quando viene eseguito il metodo Finalize

La volta successiva che il garbage collector viene richiamato (2a raccolta), vede che gli oggetti finalizzati sono veramente spazzatura, poiché le radici dell'applicazione non puntano su di essa e la coda freachable non punta più su di essa (è anche VUOTA), quindi il la memoria per gli oggetti (E, I, J) viene semplicemente recuperata da Heap. Vedi la figura sotto e confrontala con la figura appena sopra

inserisci qui la descrizione dell'immagine

La cosa importante da capire qui è che sono richiesti due GC per recuperare la memoria utilizzata dagli oggetti che richiedono la finalizzazione . In realtà, sono necessarie anche più di due collezioni in cabina poiché questi oggetti possono essere promossi a una generazione precedente

NOTA :: La coda freachable è considerata una radice proprio come le variabili globali e statiche sono le radici. Pertanto, se un oggetto si trova nella coda freachable, allora l'oggetto è raggiungibile e non è spazzatura.

Come ultima nota, ricorda che l'applicazione di debug è una cosa, Garbage Collection è un'altra cosa e funziona in modo diverso. Finora non puoi SENTIRE la garbage collection semplicemente eseguendo il debug delle applicazioni, inoltre se desideri investigare Memory inizia qui.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.