Comprensione della raccolta dei rifiuti in .NET

 C Programming >> Programmazione C >  >> Tags >> .NET
Comprensione della raccolta dei rifiuti in .NET

Sei inciampato qui e trai 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 build di rilascio con Build + Configuration manager, cambia la combinazione "Configurazione soluzione attiva" nell'angolo in alto a sinistra su "Release". Quindi, vai in Strumenti + Opzioni, Debug, Generale e deseleziona l'opzione "Sopprimi ottimizzazione JIT".

Ora esegui di nuovo il tuo programma e armeggi con il codice sorgente. Nota come le parentesi graffe extra non hanno alcun effetto. E nota come impostare la variabile su null non fa alcuna differenza. Stamperà sempre "1". Ora funziona nel modo in cui speri e ti aspettavi che avrebbe funzionato.

Il che lascia il compito di spiegare perché funziona in modo così diverso quando si esegue la build Debug. Ciò richiede la spiegazione di come il Garbage Collector scopre le variabili locali e in che modo è 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 è invece del tutto 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à per la prima volta un riferimento a un oggetto. E l'indirizzo dell'istruzione del codice macchina in cui quella variabile non è più utilizzata. Anche se quella variabile è memorizzata nello stack frame o in un registro della 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 nell'heap GC. Sicuramente non facile da fare quando il riferimento all'oggetto è memorizzato in un registro della CPU. La tabella dice dove guardare.

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à di essere eseguito appena prima che il tuo programma termini. Chiaramente non vorresti che alcun riferimento a oggetto utilizzato all'interno di quel metodo Main() viva per la durata del programma, ciò equivarrebbe a una perdita. Il jitter può utilizzare la tabella per scoprire che una tale variabile locale non è più utile, a seconda di quanto è avanzato il programma all'interno del metodo Main() prima di effettuare una chiamata.

Un metodo quasi magico correlato a quella tabella è GC.KeepAlive(). È un molto metodo speciale, non genera alcun codice. Il suo unico dovere è modificare quella tabella. Si si estende la durata della variabile locale, impedendo al riferimento memorizzato di ricevere la raccolta dati obsoleti. L'unica volta che è necessario utilizzarlo è impedire al GC di essere troppo ansioso di raccogliere un riferimento, cosa che può verificarsi 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 standard di quando è necessario utilizzare GC.KeepAlive().

Quindi, come puoi vedere dal tuo snippet di esempio dopo averlo eseguito nella build Release, le variabili locali possono vengono raccolti in anticipo, prima che il metodo termini l'esecuzione. Ancora più potentemente, un oggetto può essere raccolto mentre uno dei suoi metodi è in esecuzione se quel metodo non fa più riferimento a questo . C'è un problema con questo, è molto imbarazzante eseguire il debug di un tale metodo. Dal momento che potresti inserire la variabile nella finestra Watch o ispezionarla. E sarebbe scomparire durante il debug se si verifica un GC. Sarebbe molto spiacevole, quindi il nervosismo è consapevole della presenza di un debugger collegato. Quindi modifica la tabella e modifica l'indirizzo "ultimo utilizzato". E lo cambia dal suo valore normale all'indirizzo dell'ultima istruzione nel metodo. Che mantiene viva la variabile finché il metodo non viene restituito. Ciò ti consente di continuare a guardarlo fino al ritorno del metodo.

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

L'impostazione della variabile su null ora ha effetto perché il GC ispezionerà 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 tale istruzione è presente o meno quando si esegue il codice nella build Release. In effetti, l'ottimizzatore del jitter rimuove tale affermazione poiché non ha alcun effetto. Quindi assicurati di non scrivere codice del genere, anche se sembra avere effetto.

Un'ultima nota su questo argomento, questo è ciò che mette nei guai i programmatori 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 è chiamare GC.Collect(). Ma scopriranno che non funziona quando eseguiranno il debug della loro app, portandoli in una terra che non c'è mai chiamando Marshal.ReleaseComObject(). Gestione manuale della memoria, raramente funziona correttamente perché trascureranno facilmente un riferimento di interfaccia invisibile. GC.Collect() funziona effettivamente, ma non quando esegui il debug dell'app.


[Volevo solo aggiungere ulteriori dettagli sul processo di finalizzazione interna]

Quindi, crei un oggetto e quando l'oggetto viene raccolto, il Finalize dell'oggetto dovrebbe essere chiamato il metodo. Ma c'è di più da finalizzare oltre a questo semplicissimo presupposto.

BREVI CONCETTI::

  1. Oggetti che NON implementano Finalize metodi, lì la memoria viene recuperata immediatamente, a meno che, ovviamente, non siano raggiungibili da
    codice dell'applicazione più

  2. Oggetti che implementano Finalize Metodo, Il concetto/Implementazione di Application Roots , Finalization Queue , Freacheable Queue prima che possano essere recuperati.

  3. Qualsiasi oggetto è considerato spazzatura se NON è raggiungibile da ApplicationCode

Assume::Classi/Oggetti A, B, D, G, H NON implementano Finalize Metodo e C, E, F, I, J implementano Finalize Metodo.

Quando un'applicazione crea un nuovo oggetto, l'operatore new alloca la memoria dall'heap. Se il tipo dell'oggetto contiene un Finalize metodo, quindi un puntatore all'oggetto viene posizionato nella coda di finalizzazione .

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

La coda di finalizzazione è una struttura dati interna controllata dal Garbage Collector. Ogni voce nella coda punta a un oggetto che dovrebbe avere il suo Finalize metodo 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 radici dell'applicazione , e alcuni no. Quando sono stati creati gli oggetti C, E, F, I e J, il framework .Net rileva che questi oggetti hanno Finalize metodi e puntatori a questi oggetti vengono aggiunti alla coda di finalizzazione .

Quando si verifica un GC (1a raccolta), gli oggetti B, E, G, H, I e J vengono determinati come spazzatura. Perché A,C,D,F sono ancora raggiungibili tramite il codice dell'applicazione rappresentato dalle frecce del riquadro giallo in alto.

Il Garbage Collector esegue la scansione della 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 scaricabile ("F-raggiungibile").

La coda raggiungibile è un'altra struttura dati interna controllata dal Garbage Collector. Ogni puntatore nella coda raggiungibile identifica un oggetto che è pronto per avere il suo Finalize metodo chiamato.

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

2.) Tuttavia, la memoria occupata dagli oggetti E, I e J non poteva essere rivendicata perché il loro Finalize il metodo non è stato ancora chiamato. La chiamata al metodo Finalize viene eseguita da coda accessibile.

3.) A,C,D,F sono ancora raggiungibili dal Codice dell'Applicazione rappresentato tramite le frecce dal riquadro giallo in alto, quindi NON verranno comunque ritirati

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

La prossima volta che il Garbage Collector viene invocato (2a raccolta), vede che gli oggetti finalizzati sono veramente spazzatura, poiché le radici dell'applicazione non puntano ad esso e la coda accessibile non punta più ad esso (è anche VUOTO), quindi la memoria per gli oggetti (E, I, J) viene semplicemente recuperata da Heap. Vedere la figura sotto e confrontarla con la figura appena sopra

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

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

Come ultima nota, ricorda che il debug dell'applicazione è una cosa, Garbage Collection è un'altra cosa e funziona in modo diverso. Finora non puoi SENTIRE la raccolta dei rifiuti semplicemente eseguendo il debug delle applicazioni, inoltre se desideri indagare sulla memoria, inizia qui.