Ricerca…


introduzione

In. Netto, gli oggetti creati con new () sono allocati nell'heap gestito. Questi oggetti non sono mai esplicitamente finalizzati dal programma che li usa; invece, questo processo è controllato da .Net Garbage Collector.

Alcuni degli esempi riportati di seguito sono "casi di laboratorio" per mostrare il Garbage Collector al lavoro e alcuni dettagli significativi del suo comportamento, mentre altri si concentrano su come preparare le classi per una corretta gestione da parte di Garbage Collector.

Osservazioni

Il Garbage Collector ha lo scopo di ridurre il costo del programma in termini di memoria allocata, ma farlo ha un costo in termini di tempo di elaborazione. Per raggiungere un buon compromesso generale, ci sono alcune ottimizzazioni che dovrebbero essere prese in considerazione durante la programmazione con il Garbage Collector in mente:

  • Se il metodo Collect () deve essere invocato in modo esplicito (che comunque non dovrebbe essere il caso), si consideri l'utilizzo della modalità "ottimizzata" che finalizza l'oggetto morto solo quando la memoria è effettivamente necessaria
  • Invece di invocare il metodo Collect (), considerare l'utilizzo dei metodi AddMemoryPressure () e RemoveMemoryPressure (), che attivano una raccolta di memoria solo se effettivamente necessario
  • Una raccolta di memoria non è garantita per finalizzare tutti gli oggetti morti; invece, il Garbage Collector gestisce 3 "generazioni", un oggetto che a volte "sopravvive" da una generazione al successivo
  • Possono essere applicati diversi modelli di threading, in base a vari fattori tra cui la messa a punto dell'ottimizzazione, con conseguenti diversi gradi di interferenza tra il thread Garbage Collector e gli altri thread dell'applicazione

Un esempio di base della raccolta (garbage)

Data la seguente classe:

public class FinalizableObject 
{
    public FinalizableObject()
    {
        Console.WriteLine("Instance initialized");
    }

    ~FinalizableObject()
    {
        Console.WriteLine("Instance finalized");
    }
}

Un programma che crea un'istanza, anche senza usarlo:

new FinalizableObject(); // Object instantiated, ready to be used

Produce il seguente risultato:

<namespace>.FinalizableObject initialized

Se non accade nient'altro, l'oggetto non è finalizzato fino alla fine del programma (che libera tutti gli oggetti sull'heap gestito, finalizzandoli nel processo).

È possibile forzare l'esecuzione di Garbage Collector in un determinato punto, come indicato di seguito:

new FinalizableObject(); // Object instantiated, ready to be used
GC.Collect();

Che produce il seguente risultato:

<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized

Questa volta, non appena è stato richiamato il Garbage Collector, l'oggetto non utilizzato (ovvero "morto") è stato finalizzato e liberato dall'heap gestito.

Oggetti in diretta e oggetti morti - le basi

Regola pratica: quando si verifica la garbage collection, gli "oggetti live" sono quelli ancora in uso, mentre gli "oggetti morti" sono quelli non più utilizzati (qualsiasi variabile o campo che li fa riferimento, se presente, è andato fuori campo prima che si verifichi la raccolta) .

Nell'esempio seguente (per praticità, FinalizableObject1 e FinalizableObject2 sono sottoclassi di FinalizableObject dell'esempio precedente e quindi ereditano il comportamento del messaggio di inizializzazione / finalizzazione):

var obj1 = new FinalizableObject1(); // Finalizable1 instance allocated here
var obj2 = new FinalizableObject2(); // Finalizable2 instance allocated here
obj1 = null; // No more references to the Finalizable1 instance 
GC.Collect();

L'output sarà:

<namespace>.FinalizableObject1 initialized
<namespace>.FinalizableObject2 initialized
<namespace>.FinalizableObject1 finalized

Nel momento in cui viene richiamato il Garbage Collector, FinalizableObject1 è un oggetto morto e viene finalizzato, mentre FinalizableObject2 è un oggetto live e viene mantenuto nell'heap gestito.

Più oggetti morti

Cosa succede se due (o diversi) oggetti altrimenti morti si riferiscono l'un l'altro? Questo è mostrato nell'esempio seguente, supponendo che OtherObject sia una proprietà pubblica di FinalizableObject:

var obj1 = new FinalizableObject1(); 
var obj2 = new FinalizableObject2();
obj1.OtherObject = obj2;
obj2.OtherObject = obj1;
obj1 = null; // Program no longer references Finalizable1 instance
obj2 = null; // Program no longer references Finalizable2 instance
// But the two objects still reference each other
GC.Collect();

Questo produce il seguente risultato:

<namespace>.FinalizedObject1 initialized
<namespace>.FinalizedObject2 initialized
<namespace>.FinalizedObject1 finalized
<namespace>.FinalizedObject2 finalized

I due oggetti sono finalizzati e liberati dall'heap gestito malgrado il riferimento reciproco (poiché nessun altro riferimento esiste a nessuno di essi da un oggetto effettivamente in esecuzione).

Riferimenti deboli

I riferimenti deboli sono ... riferimenti, ad altri oggetti (noti anche come "target"), ma "deboli" in quanto non impediscono che tali oggetti vengano raccolti con garbage collection. In altre parole, i riferimenti deboli non vengono conteggiati quando Garbage Collector valuta gli oggetti come "live" o "dead".

Il seguente codice:

var weak = new WeakReference<FinalizableObject>(new FinalizableObject());
GC.Collect();

Produce l'output:

<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized

L'oggetto viene liberato dall'heap gestito nonostante venga fatto riferimento dalla variabile WeakReference (ancora nell'ambito quando è stato richiamato il Garbage Collector).

Conseguenza n. 1: in qualsiasi momento, non è sicuro assumere se una destinazione WeakReference è ancora allocata nell'heap gestito o meno.

Conseguenza 2: ogni volta che un programma deve accedere al target di una Weakreference, deve essere fornito il codice per entrambi i casi, del target ancora assegnato o meno. Il metodo per accedere al target è TryGetTarget:

var target = new object(); // Any object will do as target
var weak = new WeakReference<object>(target); // Create weak reference
target = null; // Drop strong reference to the target

// ... Many things may happen in-between

// Check whether the target is still available
if(weak.TryGetTarget(out target))
{
    // Use re-initialized target variable
    // To do whatever the target is needed for
}
else
{
    // Do something when there is no more target object
    // The target variable value should not be used here
}

La versione generica di WeakReference è disponibile da .Net 4.5. Tutte le versioni del framework forniscono una versione non generica, non tipizzata, costruita nello stesso modo e verificata come segue:

var target = new object(); // Any object will do as target
var weak = new WeakReference(target); // Create weak reference
target = null; // Drop strong reference to the target

// ... Many things may happen in-between

// Check whether the target is still available
if (weak.IsAlive)
{
    target = weak.Target;

    // Use re-initialized target variable
    // To do whatever the target is needed for
}
else
{
    // Do something when there is no more target object
    // The target variable value should not be used here
}

Dispose () rispetto ai finalizzatori

Implementa il metodo Dispose () (e dichiara la classe contenitore come IDisposable) come mezzo per garantire che tutte le risorse che pesano sulla memoria vengano liberate non appena l'oggetto non viene più utilizzato. Il "trucco" è che non vi è alcuna garanzia che il metodo Dispose () venga mai richiamato (a differenza dei finalizzatori che vengono sempre richiamati alla fine della vita dell'oggetto).

Uno scenario è un programma che chiama Dispose () sugli oggetti che crea esplicitamente:

private void SomeFunction()
{
    // Initialize an object that uses heavy external resources
    var disposableObject = new ClassThatImplementsIDisposable();

    // ... Use that object

    // Dispose as soon as no longer used
    disposableObject.Dispose();

    // ... Do other stuff 

    // The disposableObject variable gets out of scope here
    // The object will be finalized later on (no guarantee when)
    // But it no longer holds to the heavy external resource after it was disposed
}

Un altro scenario è la dichiarazione di una classe da istanziare dal framework. In questo caso la nuova classe solitamente eredita una classe base, ad esempio in MVC si crea una classe controller come sottoclasse di System.Web.Mvc.ControllerBase. Quando la classe base implementa l'interfaccia IDisposable, questo è un buon suggerimento che Dispose () sarebbe invocato correttamente dal framework - ma ancora una volta non c'è una forte garanzia.

Così Dispose () non è un sostituto di un finalizzatore; invece, i due dovrebbero essere usati per diversi scopi:

  • Un finalizzatore alla fine libera risorse per evitare perdite di memoria che si verifichino diversamente
  • Dispose () libera risorse (probabilmente le stesse) non appena queste non sono più necessarie, per alleggerire la pressione sull'assegnazione complessiva della memoria.

Smaltimento e finalizzazione adeguati degli oggetti

Dato che Dispose () ei finalizzatori sono finalizzati a scopi diversi, una classe che gestisce risorse di memoria esterne pesanti dovrebbe implementare entrambi. La conseguenza è scrivere la classe in modo che gestisca bene due possibili scenari:

  • Quando viene invocato solo il finalizzatore
  • Quando viene invocato Dispose () prima e successivamente viene anche chiamato il finalizzatore

Una soluzione è scrivere il codice cleanup in modo tale che eseguirlo una o due volte produca lo stesso risultato di una sola volta. La fattibilità dipende dalla natura della pulizia, ad esempio:

  • La chiusura di una connessione al database già chiusa non avrebbe probabilmente alcun effetto, quindi funziona
  • L'aggiornamento di alcuni "conteggi di utilizzo" è pericoloso e produrrebbe un risultato errato se chiamato due volte anziché una volta.

Una soluzione più sicura garantisce che il codice di pulizia venga chiamato una sola volta, una sola volta, indipendentemente dal contesto esterno. Questo può essere ottenuto con il "modo classico" usando una bandiera dedicata:

public class DisposableFinalizable1: IDisposable
{
    private bool disposed = false;

    ~DisposableFinalizable1() { Cleanup(); }

    public void Dispose() { Cleanup(); }

    private void Cleanup()
    {
        if(!disposed)
        {
            // Actual code to release resources gets here, then
            disposed = true;
        }
    }
}

In alternativa, Garbage Collector fornisce un metodo specifico SuppressFinalize () che consente di ignorare il finalizzatore dopo l'invocazione di Dispose:

public class DisposableFinalizable2 : IDisposable
{
    ~DisposableFinalizable2() { Cleanup(); }

    public void Dispose()
    {
        Cleanup();
        GC.SuppressFinalize(this);
    }

    private void Cleanup()
    {
        // Actual code to release resources gets here
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow