Garbage Collection

di il
11 risposte

Garbage Collection

Salve a tutti, sto studiando c# da un libro ma temo di non aver compreso bene determiante cose del garbage collection.
Partendo dal principio:
Mi viene spiegato che il CLR differenzia i tipi in due categorie: Tipi di valore e Tipi di riferimento, sostanzialmente i primi salvano il valore nello stack e i secondi in una memoria chiamata menaged heap fornendoci il riferimento.
Poi mi viene presentato il GC dicendomi che è una delle funzionalità offerte dal CLR e che gestisce solo il managed heap andando a rimuovere le RISORSE gestite in modo automatico, mi viene spiegato che questo non si occupa delle RISORSE NON gestite e che quindi il programmatore deve andare a gestire ed eliminare tramite l'interfaccia IDisposable utilizzando il metodo dispose e il finalizzatore.
Okay ora vi espongo i miei dubbi:
1) Visto che il GC si occupa esclusivamente del menaged heap, in automatico tutti i tipi di riferimento vengono gestiti dal GC?
2) Quando si parla di risorse gestite ci si riferisce ai tipi di riferimento?(Il libro prima della spiegazione del GC mi spiega semplicemente che con il termine managed code indica quelle applicazioni gestite dal CLR, quindi che è possibile sfruttare tutte le funzionalità che questo mette a dispisizione, mentre con codice non gestito indica che appunto non è gestito dal CLR e che quindi non è possibile sfruttare le sue funzionalità) come riconosco le risorse gestite e le risore non gestite? Tenendo conto di quello che mi è stato spiegato in automatico capisco che parlando del GC i tipi di valore sono le risorse non gestite mentre quelli di riferimento sono le gestite.
Grazie per l'attenzione.

11 Risposte

  • Re: Garbage Collection

    Sinteticamente, il GC si occupa di gestire la rimozione dalla memoria degli oggetti che non sono più utilizzati, ossia quegli oggetti per i quali non è più presente un riferimento attivo (es. una variabile) o che in ogni caso non sono più raggiungibili.

    Questi oggetti, assieme a quelli eventualmente creati come dipendenze, se istanziati dal CLR sono "risorse gestite" che quindi vengono distrutte dal GC, come predetto.

    Quando si parla di risorse "non gestite" invece, ci si riferisce a tutte quelle risorse che non fanno parte del CLR o che sono diverse da una banale istanza di un oggetto che deve essere semplicemente deallocato al termine del suo utilizzo: pensa a tutti quei casi in cui apri una connessione al DB, oppure un file, o un socket, in generale a tutti quei casi in cui stai istanziando un oggetto (del CLR) che ti permette di interagire con qualcosa di "esterno", fornito dal sistema operativo come servizio o da qualcos'altro.

    Per queste ultime risorse viene posto l'accento sulla necessità di rilasciarle il prima possibile. Cosa vuol dire? Che sebbene il GC si occupi di distruggere autonomamente l'oggetto creato nell'ecosistema .NET, deve essere tua cura fare in modo che - se quell'oggetto gestisce una risorsa esterna - questo la chiuda, la rilasci o la liberi il prima possibile, senza attendere l'intervento del GC (che comunque procederà all'operazione al posto tuo, se non hai eseguito questa finalizzazione).

    Gli oggetti che gestiscono risorse esterne (es. FileStream, SqlConnection, ecc.) hanno quindi generalmente un metodo che consente di rilasciare la risorsa sottostante (es. Close()): questo vuol dire che il file o la connessione saranno "chiusi", liberando tali risorse nel sistema (es. permettendo ad altri di collegarsi al DB) ma l'oggetto che consente di interoperare con loro rimarrà ancora in memoria e verrà liberato dal GC quando possibile, assieme a tutti gli altri.

    Il pattern che deriva dall'implementazione dell'interfaccia IDisposable consente di identificare tutti quegli oggetti che gestiscono risorse esterne e che implementano quindi un metodo Dispose() da invocare esplicitamente quando la risorsa non serve più. Ad esempio, per le classi indicate sopra, la chiamata a Dispose() si traduce in una chiamata a Close() per chiudere il file e/o la connessione.

    In genere quindi si tende a usare il costrutto try...finally o ancora meglio using() per "circondare" il codice che fa uso di un oggetto in grado di gestire una risorsa esterna: al termine del blocco, viene richiamato in automatico il metodo Dispose() per rilasciare la risorsa:
    
    using (var obj = new DisposableClass())
    {
      // TODO: Codice che usa l'oggetto...
    } // Chiamata automatica a Dispose() alla fine
    
    // ...il GC interverrà per rilasciare l'oggetto quando ne ha voglia... :)
    
    Il metodo Dispose() non va confuso con "finalizzatore" Finalize(): questo metodo è la controparte del costruttore e viene chiamato dal GC nel momento in cui è necessario distruggere un oggetto, prima di rimuoverlo.

    Molte classi che gestiscono risorse esterne possiedono questo metodo e si accertano nel finalizzatore che la risorsa sia stata rilasciata (es. chiusura del file o della connessione) prima di uccidere definitivamente l'oggetto. In pratica, chiamano Dispose() se non è già stato chiamato.

    Nel Dispose() è frequente trovare una chiamata al metodo GC.SuppressFinalizer() che permette di indicare al GC che, in fase di distruzione, non deve invocare il finalizzatore in quanto la risorsa esterna è stata già chiusa e gestita tramite chiamata a Dispose(): questo permette di risparmiare tempo prezioso ed evitare operazioni inutili nella fase di rilascio della memoria occupata dagli oggetti da parte del GC, anche perché quando questo avviene il programma risulta bloccato, quindi deve essere una operazione il più performante possibile.

    Riassumendo, è bene lasciare al GC il compito di gestire in autonomia la deallocazione delle istanze, anzi non è possibile fare altrimenti.

    Per alcuni oggetti che però gestiscono risorse esterne al framework, occorre curarsi di rilasciarle appena dopo l'uso, in modo che il GC continui solamente a gestire l'oggetto in sé e restituendo quanto prima al sistema la risorsa occupata, per evitare di incappare in un loro esaurimento (es. RAM finita, connessioni max occupate, ecc.).

    Se lo sviluppatore però si dimentica di fare questo, la presenza del finalizzatore garantisce comunque che, alla distruzione dell'oggetto, la risorsa verrà comunque rilasciata, quindi in linea generale il sistema risulterà stabile in ogni caso (nei limiti del possibile).

    Prova a leggere un po' di documentazione sui metodi citati se vuoi ulteriori approfondimenti o chiarire altri dubbi.
    Altrimenti chiedi.

    Ciao!
  • Re: Garbage Collection

    Ciao Alka e grazie mille per la risposta, credo di aver capito.
    Faccio un recap di tutte le cose che ho citato, per avere una tua conferma e per finire una domanda sull'implementazione di IDisposable.

    Partendo dalla mia prima citazione sulle due categorie di tipi:
    Il CLR supporta tipi di valore/riferimento, questi però sono gestiti in modo del tutto differente, i tipi di valori sono gestiti nello stack e vengono rimossi dopo il loro utilizzo tramite Pop, mentre i tipi di riferimento sono situati all'interno dell'heap che è gestito dal GC, questo si occupa di rimuovere i tipi di riferimento tramite algoritmo non deterministico.
    Quindi in sostanza sbagliavo a pensare che in base ai tipi potevo capire se si trattava di risorse gestite o non, visto che il CLR li gestisce entrambi indipendentemente dal loro "contenitore".

    Passando alla mia seconda citazione sulle risorse gestite e non:
    Posso capire se una risorsa non è gestita quando ho a disposizione un metodo Dispose() o simile da poter utilizzare dopo il suo utilizzo.

    Una cosa sull'interfaccia IDisposable:
    Se ad esempio una mia classe usa più risorse non gestite, posso implementare l'interfaccia IDisposable andando a scrivere nel metodo Dispose() la chiusura delle risorse non gestitre tramite il loro Dispose()? così che quando vado a richiamare il Dispose della mia classe in automatico vengono chiuse le risorse non gestite tramite il loro Dispose.
    Guardando da https://docs.microsoft.com/it-it/dotnet/api/system.idisposable?view=netcore-3.1 nel primo esempio mostra come bisogna implementare l'interfaccia. Però in questo caso utilizza una struttura dove all'interno del Dispose ha due blocchi separati, uno per le risorse gestite dove li libera con il loro Dispose() e l'altro per le risorse non gestite settandole a 0, come dovrei gestirla?
  • Re: Garbage Collection

    STAddo ha scritto:


    Il CLR supporta tipi di valore/riferimento, questi però sono gestiti in modo del tutto differente, i tipi di valori sono gestiti nello stack e vengono rimossi dopo il loro utilizzo tramite Pop, mentre i tipi di riferimento sono situati all'interno dell'heap che è gestito dal GC, questo si occupa di rimuovere i tipi di riferimento tramite algoritmo non deterministico.
    Corretto.

    STAddo ha scritto:


    Quindi in sostanza sbagliavo a pensare che in base ai tipi potevo capire se si trattava di risorse gestite o non, visto che il CLR li gestisce entrambi indipendentemente dal loro "contenitore".
    Il CLR gestisce tramite il GC la cancellazione degli oggetti: se a questi corrisponde qualcos'altro da fare, deve essere eseguito dallo sviluppatore, ma se lui se ne scorda, il CLR lo fa comunque (a meno che non vi sia un bug).

    STAddo ha scritto:


    Posso capire se una risorsa non è gestita quando ho a disposizione un metodo Dispose() o simile da poter utilizzare dopo il suo utilizzo.
    Più che altro, la presenza di Dispose() ti fa capire se potenzialmente l'oggetto va in qualche modo chiuso/finalizzato/sganciato/ecc. prima di essere sottoposto a GC. Questo metodo viene fornito in quanto non sai quando il GC interverrà, e devi programmare come se questo tempo di dilazione fosse 1 secondo oppure paradossalmente due mesi.

    La distinzione tra risorsa "non gestita" sta a indicare quegli oggetti che si creano a fronte dell'interazione del CLR con qualcosa di "esterno", tipo l'allocazione di un handle di Windows, la creazione di un processo, l'uso di un oggetto COM tramite P/Invoke, tutto quello che non è creato dal CLR.

    STAddo ha scritto:


    Se ad esempio una mia classe usa più risorse non gestite, posso implementare l'interfaccia IDisposable andando a scrivere nel metodo Dispose() la chiusura delle risorse non gestitre tramite il loro Dispose()? così che quando vado a richiamare il Dispose della mia classe in automatico vengono chiuse le risorse non gestite tramite il loro Dispose.
    Direi proprio di sì. Accertati che la procedura non sia suscettibile a facili errori, o comunque cerca di garantire che una risorsa venga rilasciata anche se l'operazione su un'altra non va a buon fine.

    STAddo ha scritto:


    Guardando da https://docs.microsoft.com/it-it/dotnet/api/system.idisposable?view=netcore-3.1 nel primo esempio mostra come bisogna implementare l'interfaccia. Però in questo caso utilizza una struttura dove all'interno del Dispose ha due blocchi separati, uno per le risorse gestite dove li libera con il loro Dispose() e l'altro per le risorse non gestite settandole a 0, come dovrei gestirla?
    Parli di questa?
    https://docs.microsoft.com/it-it/dotnet/api/system.idisposable?view=netcore-3.1#examples

    Direi che i commenti spiegano bene. Cosa non è chiaro?
  • Re: Garbage Collection

    Grazie ancora per l'immediata risposta, si l'esempio è quello però in questo caso lui identifica
    // Other managed resource this class uses.
    private Component component = new Component();
    come una risorsa gestita e nel Dispose dell'interfaccia IDisposable va a richiamare:
    component.Dispose();
    quello che non ho capito è perchè lui la sta identificando come risorsa gestita quando abbiamo detto che se è presente il Dispose() sta a significare che si tratta di una risorsa non gestita.

    Però dalla tua risposta:
    Più che altro, la presenza di Dispose() ti fa capire se potenzialmente l'oggetto va in qualche modo chiuso/finalizzato/sganciato/ecc. prima di essere sottoposto a GC. Questo metodo viene fornito in quanto non sai quando il GC interverrà, e devi programmare come se questo tempo di dilazione fosse 1 secondo oppure paradossalmente due mesi.
    deduco che anche le risorse gestite posso avere tranquillamente Dispose per un fatto di liberazione deterministica.

    Proseguendo nell'esempio:
    Identifica
    // Pointer to an external unmanaged resource.
    private IntPtr handle;
    come una risorsa non gestita, e nel dispose va a liberarla in questo modo
    CloseHandle(handle);
    handle = IntPtr.Zero;
    In questo caso da come ho capito il "Dispose" di questa risorsa non gestita è
    CloseHandle(handle);
    ma non capisco perchè poi dopo la porti a IntPtr.Zero se è stata chiusa tramite CloseHandler.
  • Re: Garbage Collection

    STAddo ha scritto:


    Però dalla tua risposta [...]
    deduco che anche le risorse gestite posso avere tranquillamente Dispose per un fatto di liberazione deterministica.
    Esatto.

    Supponi ad esempio una libreria che gestisce file in formato PDF interamente in .NET, dalla logica di "parsing" fino alla lettura del file: anche se una classe di quella libreria si potesse considerare come "gestita" interamente, magari perché scritta tutta in linguaggio IL, sarebbe comunque opportuno chiudere quanto prima un file in lettura/scrittura senza attendere l'intervento del GC.

    Nella maggior parte dei casi, essendo che .NET gira su sistemi operativi che non si basano su .NET stesso, è normale che certe risorse siano comunque sempre "non gestite", perché lo strato del sistema operativo rimane comunque qualcosa di esterno al CLR ed è obbligatorio interagire con esso.

    Inoltre, classi interamente "gestite" potrebbero comunque implementare Dispose() perché devono per forza richiamare quel metodo su oggetti creati al proprio interno, che lo chiamano a loro volta su altri oggetti ancora, fino a quelli che rilasciano effettivamente la risorsa.

    STAddo ha scritto:


    Proseguendo nell'esempio [...]
    ma non capisco perchè poi dopo la porti a IntPtr.Zero se è stata chiusa tramite CloseHandler.
    Quell'esempio ti mostra invece come potrebbe essere il rilascio di una risorsa effettivamente "non gestita", ad esempio chiamando una API di Windows importata come funzione (quindi usando P/Invoke) che dato un handle di Windows (una struttura comune del SO che può rappresentare un file, una finestra, un processo, ecc.) lo va a chiudere, cioè lo rilascia al SO stesso.

    CloseHandle e la funzione da richiamare. L'assegnazione a IntPtr.Zero credo serva solo a resettare l'handle a un valore "nullo" (equivalente del nullo per i puntatori) in modo che, qualora si chiamasse CloseHandle una seconda volta, questa non vada in errore.

    La precauzione ci sta perché anche il CLR potrebbe chiamare Dispose() all'interno del metodo Finalize(), per garantire che l'oggetto "smolli" la risorsa non gestita (l'handle) in ogni caso; questo è un caso da manuale in cui nella Dispose() andrebbe aggiunto SuppressFinalize(this) per dire al GC "guarda che questo oggetto è già stato finalizzato, quindi non è necessario che tu lo faccia di nuovo", ottimizzando il processo. Il GC in quel caso andrebbe solo a rilasciare la memoria dell'oggetto (che è e rimane sempre il suo ineluttabile compito).

    Ciao!
  • Re: Garbage Collection

    Grazie mille, mi hai fatto uscire dal pallone
    Per finire, come faccio a sapere se una classe al suo interno sta usando una risorsa non gestita?
    Ho pensato "Posso andare a vedere dalla docs se la classe implementa IDisposable" però come mi hai detto, IDisposable non è per forza implementata per rimuovere solo le risorse non gestite.
  • Re: Garbage Collection

    STAddo ha scritto:


    Per finire, come faccio a sapere se una classe al suo interno sta usando una risorsa non gestita?
    L'unica è avere il codice sorgente, oppure esaminare il codice IL (se lo si sa leggere) o in alternativa decompilarlo per renderlo chiaro, esaminando cosa succede all'interno della classe. Non c'è un metodo "secco", ma del resto non ne vedrei l'utilità se non per soddisfare una curiosità personale.

    STAddo ha scritto:


    Ho pensato "Posso andare a vedere dalla docs se la classe implementa IDisposable" però come mi hai detto, IDisposable non è per forza implementata per rimuovere solo le risorse non gestite.
    Esatto, però si può ipotizzare che alla fine - allo stato attuale - se una classe implementa IDisposable, allora direttamente o indirettamente userà una risorsa "non gestita".

    Dal punto di vista dello sviluppatore, il trattamento di queste classi non deve comunque cambiare: se viene usato un oggetto "disposable" nel corpo di un metodo, va inserito nel costrutto using(); se viene usato da una classe nel proprio codice, allora occorre implementare IDisposable e quindi anche il metodo Dispose(), e all'interno di quel metodo chiamare Dispose() sull'oggetto in questione, che a sua volta farà lo stesso con eventuali dipendenze e istanze "disposable" di cui è responsabile, se ci sono.

    Completate queste operazioni, prima o poi nel tempo, quando ne avrà voglia o penserà che sia il momento giusto per farlo, interverrà il GC per rimuovere gli oggetti in quanto tali dalla memoria.

    Ciao!
  • Re: Garbage Collection

    Esatto, però si può ipotizzare che alla fine - allo stato attuale - se una classe implementa IDisposable, allora direttamente o indirettamente userà una risorsa "non gestita".
    Quindi in sostanza, una classe che al suo interno utilizza risorse non gestite ma che implementa l'interfaccia IDisposable per liberare queste risorse, anche se utilizza delle risorse non gestite è da vedere come una risorsa gestita.
    Mentre per quelle classi che utilizzano risorse non gestite senza implimentare IDisposable per la liberazione di queste risorse, è da vedere come una risorsa non gestita.

    Ho creato una piccola mindmap per riordinarmi le idee: https://atlas.mindmup.com/dstaddo/staddo_gc_p/index.html
  • Re: Garbage Collection

    STAddo ha scritto:


    Una classe che al suo interno utilizza risorse non gestite ma che implementa l'interfaccia IDisposable per liberare queste risorse, anche se utilizza delle risorse non gestite è da vedere come una risorsa gestita.
    Sì, gestita (o managed) significa che la memoria viene istanziata e liberata dal CLR, niente di più, niente di meno.
    Qualsiasi chiamata al costruttore di una classe per creare una istanza di un oggetto in .NET di fatto crea una risorsa gestita.

    Non gestita (o unmanaged) significa che non è visibile al CLR. Ad esempio, se in una classe chiamo la funzione API nativa CreateWindow(), sto invocando la funzione di una DLL che serve per definire un oggetto "finestra" generico nel sistema operativo Windows: questa API crea la finestra come risorsa, ma il CLR non sa nulla di essa né sa come va gestita nel momento in cui la risorsa deve essere rilasciata, quindi devo implementare IDisposable per scrivere la logica che, chiamata dallo sviluppatore o dal GC nel mondo del CLR, vada a invocare l'API (es. CloseHandle()) che permette di rilasciare la memoria o la risorsa occupata e precedentemente richiesta tramite la CreateWindow().

    La classe che chiama queste API è comunque una risorsa gestita, perché di quella se ne occupa il CLR.
    La finestra che viene creata e rilasciata con la chiamata alle funzioni API menzionate rappresenta la risorsa "non gestita".

    Le risorse non gestite sono tutte quelle che *NON* vengono istanziate o create dal CLR.

    STAddo ha scritto:


    Mentre per quelle classi che utilizzano risorse non gestite senza implimentare IDisposable per la liberazione di queste risorse, è da vedere come una risorsa non gestita.
    No, quelle classi che usano risorse non gestite e non implementano IDisposable per liberarle, sono da vedere come... errori e potenziali generatori di problemi seri.

    Ciao!
  • Re: Garbage Collection

    Ciao Alka, scusami per il ritardo.
    L'altro giorno mi sono spiegato male, quando ho scritto:
    Una classe che al suo interno utilizza risorse non gestite ma che implementa l'interfaccia IDisposable per liberare queste risorse, anche se utilizza delle risorse non gestite è da vedere come una risorsa gestita.
    Intendevo che è da vedere come una risorsa gestita nell'implementazione di un altro IDisposable.
    Ad esempio, ho una classe A che implementa l'interfaccia per rilasciare le sue risorse non gestite, quando la utilizzo in una nuova classe B che a sua volta implementa delle risorse non gestite, vado a scrivere nel Dispose(Bool) della classe B in questo modo:
    	protected virtual void Dispose(bool disposing)
            {
                if(!this.disposed)
                {
                    if(disposing)
                    {
                        // Dispose managed resources.
                        //Rilascio risorse non gestite della classe A
                        A.Dispose();
                    }
                    
                    //Rilascio risorse non gestite della classe B
    
                    disposed = true;
                }
            }
    
    Quindi nel blocco dove mi dice "// Dispose managed resources." vado ad inserire la classe A che di base è gestita ma che richiamando Dispose elimina le risorse non gestite, poi proseguo andando a dichiarare il rilascio delle risorse non gestite della classe B.
    Volendo posso anche lasciar stare A in quando alla sua eliminazione il GC avvia il Finalizzatore che mi elimina le risorse non gestite, ma è sempre meglio rilasciarle prima che il GC entri in azione.
  • Re: Garbage Collection

    Direi che (per me) è tutto corretto.
Devi accedere o registrarti per scrivere nel forum
11 risposte