in C#, Programmazione

DotNet e Memory Leak

Reading Time: 4 minutes

Nello sviluppo di applicazioni enterprise, è fondamentale tenere traccia dell’allocazione di memoria: una gestione non corretta della memoria potrebbe passare inosservata per molto tempo (senza generare problemi evidenti) per poi diventare devastante sotto determinate condizioni.

Fondamentale per individuare memory leak sono gli strumenti integrati in Visual Studio e l’utilizzo di strumenti di monitoraggio messi a disposzione dal sistema operativo.

Ma ha senso parlare di monitoraggio della memoria al giorno d’oggi? I personal computer (per non parlare dei server) sono dotati di quantità di memoria enormi e sempra piuttosto difficile “saturarli” completamente.

Inoltre, la presenza del Garbage Collector (GC) dovrebbe garantire la “pulizia” ed il “rilascio” della memoria, liberando lo sviluppatore da qualsiasi tipo di considerazione sull’allocazione di memoria.

In realtà non è proprio così.

Il primo problema riguarda gli oggetti allocati in memoria, referenziati ma non utilizzati. Proprio perchè sono referenziati, il garbage collector non interverrà per liberare la loro memoria, mantenendola allocata per un tempo molto lungo (per non dire all’infinito). Un problema, di questo tipo, è rappresentato da procedure all’interno delle quali viene effettuata la registrazione di eventi che non vengono mai deregistrati.

Il secondo problema riguarda l’allocazione di memoria non gestita: in questo caso il GC non interverrà per liberarla. A differenza di quello che si può pensare, il framework .NET presenta numero classi con memoria non gestita. Altre classi, gestiscono la memoria attraverso il metodo Dispose(), che può essere implementato ereditando dall’interfaccia IDisposable.

Visual Studio e lo strumento di diagnostica

Lo strumento di diagnostica di Visual Studio consente di visualizzare l’allocazione della memoria durante il “run” dell’applicazione. Nella versione Enterprise di Visual Studio è anche presente un profilatore di memoria che consente di ottenere maggiori dettagli dell’utilizzo della memoria.

E’ possibile visualizzare l’evoluzione del grafico della memoria utilizzando il menu:

DEBUG | WINDOWS | MOSTRA STRUMENTI DI DIAGNOSTICA

Il grafico consente di analizzare due tipi di problema: il memory leak e il GC Pressure. Il memory leak indica l’utilizzo della memoria, solitamente in crescita, all’aumentare del tempo di esecuzione. Il GC Pressure si verifica al momento dell’allocazione e la deallocazione di oggetti troppo velocemente da poter far intervenire il GC . Molto frequentemente in questo caso, l’utilizzo della memoria è molto vicino al limite fisico.

L’utilizzo di un profilatore di memoria, consente di analizzare in tempo reale lo stato dell’heap dell’applicazione. Solitamente è possibile analizzare quante istanze di ciascun tipo sono state allocate, quante memoria viene utilizzata ed il percorso di riferimento .

Se si sta utilizzando la versione enterprise di Visual Studio è presente una versione free del profilatore di memoria. Alcuni tools di terze parti sono:

Il metodo piu’ rapido per effettuare l’analisi dei dati di un profiler è quella di confrontare due snapshot della memoria: il primo effettuato prima di eseguire l’operazione da analizzare, ed il secondo subito dopo il suo termine. A questo punto l’analisi delle differenze può evidenziare la presenza di memory leak .

Tipiche operazioni che possono generare memory leak

Potenzialmente qualsiasi codice può generare memory leak. In realtà, statisticamente, ci sono alcuni pattern che hanno una maggiore probabilità di generare un’allocazione non corretta della memoria:

  • utilizzo di eventi: l’iscrizione ad un evento può generare l’utilizzo senza rilascio delle risorse di memoria
  • variabili statiche: le risorse statiche sono GC Root, ovvero non vengono gestite dal GC
  • utilizzo di funzioni di cache: la memorizzazione di dati all’interno di cache può portare alla generazione di eccezioni OutOfMemory continuando ad allocare dati

E se non siamo programmatori?

Nel caso in cui non si disponga di strumenti di sviluppo come Visual Studio, è possibile utilizzare i tools di sistema : Task Manager, Process Explorer e PerfMon.

Analizzando un determinato periodo l’utilizzo della memoria di un’applicazione, se questa aumenta può indircare la presenza di memory leak. Perfon è sicuramente lo strumento piu’ completo dei tre, ma è anche quello che consente di ottenere maggiore precisione.

Come liberare memoria?

Le classi .NET che implementato codice non gestito, generalmente devono implementare l’interfaccia IDisposable. In pratica, le risorse devono essere liberate utilizzando il metodo Dispose(). Il compito dello sviluppatore è quello di liberare risorse richiamando il metodo Dispose oppure utilizzando using, che lo farà in maniera automatica. Consideriamo, ad esempio, il codice seguente:

public void Foo()
{
    using (var stream = new FileStream(@"C:\Temp\SomeFile.txt",
                                       FileMode.OpenOrCreate))
    {
        // do stuff
 
    }// stream.Dispose() will be called even if an exception occurs
}

dove viene creato un nuovo FileStream, che corrisponde al file SomeFile.txt.

L’utilizzo di using consente di allocare la memoria per l’oggetto stream e di liberarla subito dopo averla utilizzata: alla chiusura della parentesi graffa riferita ad using. L’utilizzo di using garantisce la liberazione in maniera automatica delle risorse, trasformando di fatto le istruzioni come se fossero all’interno di un try/finally con l’esecuzione del metodo Dispose all’interno di finally. Anche se non si chiama in maniera esplicita il metodo Dispose, verrà richiamato dal Finalizer quando verrà intercettato dal GC.

Una classe che implementa IDisposable ha la seguente forma:

public class MyClass : IDisposable
{
    private bool _disposed = false;
     
    public MyClass()
    {
    }
 
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;
 
        if (disposing)
        {
            // Free any other managed objects here.
        }
 
        // Free any unmanaged objects here.
        _disposed = true;
    }
 
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
 
    ~MyClass()
    {
        Dispose(false);
    }
}

L’utilizzo di GC.SuppressFinalize(this) consente effettuare la liberazione delle risorse solo nel caso in cui sull’oggetto non si sia ancora effettuato il Dispose.

Telemetria del codice e memory leak test

Nello sviuluppo di applicazioni dove l’utilizzo della memoria assume un ruolo fondamentale a livello prestazionale, può essere utile inserire all’interno del codice particolari accorgimenti per loggare ed evidenziare memory leak. All’interno del codice è possibile ottenere le informazioni relative all’utilizzo della memoria utilizzando:

Process currentProc = Process.GetCurrentProcess();
var bytesInUse = currentProc.PrivateMemorySize64;

dove PrivateMemorySize64 permette di ottenere l’utilizzo della memoria. Inoltre è sempre possibile utilizzare la classe PerformanceCounter , utilizzata da Perfmon.