Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

Memory Leak con Garbage Collection: WeakReference

Posted by Ricibald on 27th November 2009

Come tutti sapete, la GC è una modalità automatica di gestione della memoria tramite cui vengono liberate porzioni non più referenziate. In questo modo il programmatore non si deve più preoccupare di deallocare esplicitamente gli oggetti e si evitano i tipici problemi legati alla gestione della memoria (dangling pointer e memory leak).

Se per memory leak intendiamo il mancato rilascio di memoria non più referenziata allora il GC effettivamente evita qualunque memory leak, ma se per memory leak intendiamo il mancato rilascio di memoria non più utilizzata, allora la GC non è sufficiente: non può conoscere le nostre intenzioni!!

Ora mi spiegherò meglio, ma è necessario prima notare una cosa importante: il problema che descriverò è generale per la GC, ma non significa che implementazioni più raffinate non abbiano risolto il problema. Infatti implementazioni C# o Java non prevedono questo problema, ma si noti anche che, per quanto raffinata sia l’implementazione della GC, questi errori non potranno mai essere completamente impediti, quindi è necessario un approccio consapevole alle tematiche.

Dunque mi spiego meglio: immaginiamo di avere un oggetto che rappresenta la struttura di una società. Avremo quindi una lista di dipendenti, di cui ognuno avrà un superiore (se esiste) e avrà dei dipendenti da coordinare (se non si trova all’ultimo gradino). Di seguito riporto un esempio di istanza dopo il suo utilizzo:

Come osserviamo l’applicazione dopo aver utilizzato la struttura, ora ha ormai perso il riferimento a qualunque dipendente. Sarebbe lecito aspettarsi che gli oggetti vengano deallocati in quanto non più utilizzati. Infatti “Riccardo” non viene più usato da nessuno e quindi deve essere deallocato sé stesso e tutti i dipendenti che referenzia in quanto a loro volta perdono il riferimento “Riccardo”.

Bene, questo non è così!!! I nostri dipendenti rimarranno allocati per sempre nella nostra applicazione, sebbene nessuno li utilizzi più! Questo può essere definito come vero e proprio memory leak, anche se devia leggermente dal significato originale.

La presenza di memory leak dipende da come realizziamo la classe “Dipendente”. Andiamo per gradi: nel seguente esempio NON avviene alcun memory leak:

public class Dipendente {
	public List<Dipendente> Subordinati = new List<Dipendente>();
}

Infatti se l’applicazione perde il riferimento ai dipendenti, il primo che verrà deallocato è il capo “Riccardo” poiché non ha più alcun riferimento. Successivamente verrà deallocata la List poiché non ha più riferimenti e in seguito verranno deallocati tutti e tre i dipendenti referenziati precedentemente dalla lista. Quindi in questo caso non avremo alcun memory leak. Osserviamo invece il seguente caso:

public class Dipendente {
    public Dipendente Superiore;
	public List<Dipendente> Subordinati = new List<Dipendente>();
	public Dipendente(Dipendente superiore) {
	   this.Superiore = superiore;
	}
}

In questo caso vogliamo mantenere per comodità anche un riferimento al superiore di ogni dipendente. Ma analizziamo stavolta cosa succede nel caso in cui l’applicazione non fa più riferimento ai dipendenti. Il capo “Riccardo” dovrebbe essere il primo candidato all’eliminazione, ma non può essere eliminato poiché i tre subordinati mantengono un riferimento. Viceversa, i tre subordinati non possono essere eliminati poiché il capo ne mantiene il riferimento tramite la lista. Questo significa che la struttura non verrà MAI deallocata!! Memory leak!! Una definizione più formale:

Groups of mutually referencing objects which are not directly referenced by other objects and are unreachable can thus become permanently resident; if an application continually generates such unreachable groups of unreachable objects this will have the effect of a memory leak. Weak references may be used to solve the problem of circular references if the reference cycles are avoided by using weak references for some of the references within the group.

In molti articoli come http://www.ibm.com/developerworks/library/j-leaks/index.html o http://blogs.msdn.com/davidklinems/archive/2005/11/16/493580.aspx spesso si risolvono questi problemi eliminando i riferimenti che causerebbero il memory leak adottando soluzioni del tipo:

public class Dipendente : IDisposable {
    public Dipendente Superiore;
	public List<Dipendente> Subordinati = new List<Dipendente>();
	public Dipendente(Dipendente superiore) {
	   this.Superiore = superiore;
	}
	
	public void Dispose() {
	   foreach(Dipendente subordinato in this.Subordinati) {
	       subordinato.Superiore = null;
	   }
	}
}

public class Application {
    static void main() {
	   using(Dipendente riccardo = new Dipendente(null)) {
	       riccardo.Superiori.Add(new Dipendente(riccardo));
		   riccardo.Superiori.Add(new Dipendente(riccardo));
		   riccardo.Superiori.Add(new Dipendente(riccardo));
	   } // esegue la dispose
	   // perde il riferimento
	}
}

Sinceramente non mi piace l’approccio di “ricordarsi” lo using: troverete ovunque forum del tipo “avete memory leak? La colpa è tua che non fai la dispose degli oggetti Drawing!”. Cosa?? La colpa è mia??? No, le cose non stanno così: chi scrive la libreria non deve caricare agli utilizzatori di dettagli che possono causare problemi!!

La soluzione esiste ed è molto semplice: bisogna pensare i riferimenti a oggetti in termini di POSSESSO (OWNERSHIP). Possiedo, e quindi ho diritto di vita e morte, dell’oggetto a cui mi sto riferendo? Se sì, utilizza un normale riferimento come hai sempre fatto (Strong Reference). Altrimenti usa un WeakReference. Cosa è un WeakReference? Semplicemente è un riferimento debole a un oggetto, che ha validità fin quando esistono Strong Reference che mantengono in vita l’oggetto. Risulta comodo vedere le cose in termini di composizione UML: tutte le composizioni sono Strong Reference, altrimenti sono WeakReference.

Si noti un aspetto importante: queste sono regole generali di buona programmazione che si applicano a prescindere dalla implementazione della GC. Infatti linguaggi come C# o Java la GC elimina riferimenti non più utilizzati dal contesto di esecuzione corrente: eventuali riferimenti circolari isolati saranno quindi eliminati. Questo significa quindi che riferimenti circolari causati nel contesto di esecuzione corrente provocano lo stesso identico problema descritto. Un esempio semplice? La nostra applicazione mantiene un riferimento all’ultimo studente selezionato, lo eliminiamo ma non verrà mai cancellato poiché mantenuto dalla app.

Per evitare quindi memory leak in modo corretto e senza impatti sugli utilizzatori della nostra classe dovremo scrivere:

[
public class Dipendente {
    private WeakReference<Dipendente> _superioreWeak;
    public Dipendente Superiore { get { return _superioreWeak.Target; } }
	public List<Dipendente> Subordinati = new List<Dipendente>();
	
	public Dipendente(Dipendente superiore) {
	   this._superioreWeak =  new WeakReference<Dipendente>(superiore);
	}
}

public class Application {
    static void main() {
	   Dipendente riccardo = new Dipendente(null);
	   riccardo.Superiori.Add(new Dipendente(riccardo));
	   riccardo.Superiori.Add(new Dipendente(riccardo));
	   riccardo.Superiori.Add(new Dipendente(riccardo));
	} // perde il riferimento
}

I WeakReference risultano quindi molto utili per impedire memory leak causati da riferimenti circolari. Utilizzi tipici sono situazioni composite con riferimento al padre o situazioni di caching, in cui tramite un weak hash map vengono memorizzati gli oggetti senza impedirne la deallocazione (altrimenti nella cache vivrebbero oggetti per sempre!!). Una buona regola è comunque utilizzare un tool per la rilevazione di memory leak, come CLR Profiler.

Posted in .net, design, java, performance | 6 Comments »

Il Problema della Non-Covarianza nei Generics in Java e C#

Posted by Ricibald on 11th April 2008

Java, C# e C++ non contemplano il concetto di covarianza:

Una classe C<T> si dice covariante rispetto a T se, date due classi A e B tali che B deriva da A, risulta anche che C<B> deriva da C<A>.

Significa quindi che questa istruzione solleva un’eccezione:

List<Persona> studenti;
List<Object> objects = (List<Object>) studenti; // Errore

Questo deriva da un motivo di logica Object Oriented, non strutturale del linguaggio. Infatti il cast verso “objects” altera il comportamento di “studenti”, che accetta come elementi della lista solo oggetti che derivano da “Studente”. Se fosse stato ammesso, ciclare la collezione “studenti” avrebbe potuto restituire oggetti anche non di tipo “Studente” che non è ammesso. In altre parole, il problema è che la covarianza consente di definire sottoclassi che restringono la classe base.

Il concetto di covarianza si ripercuote sull’utilizzo dei metodi. Ad esempio: immaginiamo il metodo di una nostra implementazione della classe List:

class List<T> {
    AddAll(List<T> l);
}

Immaginiamo di avere una classe Studente che deriva da Persona:

List<Studente> studenti = new List<Studente>();
List<Persona> persone = new List<Persona>();
studenti.AddAll(studenti); // Ok
studenti.AddAll(persone);  // Errore

Il codice funziona se introduciamo wildcard (Java) o naked constraint (C#):

// Java
class List<T> {
    addAll(List<? extends T> l);
}
// C#
class List<T> {
    AddAll<U>(List<U> l) where U : T;
}

Se invece vogliamo staccarci dalla genericità e trattare solo istanze di tipo “Object” osserviamo (come abbiamo già visto) che questo non compila:

List<Object> l = (List<Object>)persone; // Errore

Ma utilizzando wildcard (Java) o collezioni raw (C#) la classe compila:

List<?> l = persone; // Java
IList l = persone;   // C#

Da cui deriva che in C#, quando realizziamo una classe generica, è necessario fornire sempre un’implementazione non generica per supportare upcast non generici:

// C#
class List {
    Add(object item);
    AddAll(List l);
}
class List<T> : List {
    new Add(T item);
    new AddAll<U>(List<U> l) where U : T;
}

Si noti infine il differente approccio Java/C#:

  • Le librerie Java forniscono un supporto al problema dell’upcast:
    • I metodi sono già predisposti (si veda addAll, che accetta in input una Collection<? extends E>)
    • Il cast verso tipi raw è di default supportato (List<?>)
  • Le librerie C# non sono pensate per il supporto verso l’upcast:
    • I metodi non prevedono naked constraint (AddAll accetta in input solo una List<T>). I naked constraint sembrano più che altro un workaround non dichiarato
    • Il cast verso tipi raw è fornito solo se viene fornita un’implementazione non generica della classe (è quindi richiesto l’intervento implementativo dello sviluppatore)

Posted in .net, java | 3 Comments »