PDA

View Full Version : [C#] Problema di memory leak


alex783
26-04-2009, 11:04
Ciao a tutti.
Mi sono accorto di un fastidioso problema di memory leak in C# che non riesco davvero a risolvere.

Mi spiego:
ho un form1 con un semplice pulsante che non fa altro che aprire un altro form2 .
Quando chiudo poi questo form2 e lo riapro con lo stesso pulsante del form1, noto dal task manager che la memoria occupata dal mio programma cresce senza limiti ogni qualvolta ripeto questa operazione.

Giusto per rendervi l'idea, se all'apertura l'applicazione occupa circa 3.5MB, se apro e chiudo il form2 per una decina di volte, l'occupazione cresce a 8MB, nonostante io provveda a chiudere la finestra - con il metodo Close() - e non a nasconderla con Hide().

Non so se mi sono spiegato bene... :stordita:

P.S: ho provato anche a liberare le risorse con Dispose() ma non ho risolto assolutamente niente. :fagiano:

Grazie a chi mi vorrà dare un aiuto.

supertonno
26-04-2009, 14:49
Ciao a tutti.
Mi sono accorto di un fastidioso problema di memory leak in C# che non riesco davvero a risolvere.

Mi spiego:
ho un form1 con un semplice pulsante che non fa altro che aprire un altro form2 .
Quando chiudo poi questo form2 e lo riapro con lo stesso pulsante del form1, noto dal task manager che la memoria occupata dal mio programma cresce senza limiti ogni qualvolta ripeto questa operazione.

Giusto per rendervi l'idea, se all'apertura l'applicazione occupa circa 3.5MB, se apro e chiudo il form2 per una decina di volte, l'occupazione cresce a 8MB, nonostante io provveda a chiudere la finestra - con il metodo Close() - e non a nasconderla con Hide().

Non so se mi sono spiegato bene... :stordita:

P.S: ho provato anche a liberare le risorse con Dispose() ma non ho risolto assolutamente niente. :fagiano:

Grazie a chi mi vorrà dare un aiuto.

In teoria fatta la Dispose() dovresti essere a posto.
Attenzione però, la dispose, non implica le deallocazione istantanea dell'oggetto, ma "marca" l'oggetto come deallocabile. Ci pensa piu il garbage colletor di .Net a liberare la memoria, quando lo ritiene piu opportuno...

tomminno
26-04-2009, 14:58
Dovresti postare un esempio di codice che verifica il tuo problema.
Se ci sono apparenti memory leak è perchè mantieni qualche riferimento appeso da qualche parte per cui il GC non interviene.
Oppure il GC non interviene nonostante che il tuo codice sia corretto perchè non ritiene che sia necessario, ma è decisamente l'opzione meno probabile.

alex783
26-04-2009, 15:31
In teoria fatta la Dispose() dovresti essere a posto.
Attenzione però, la dispose, non implica le deallocazione istantanea dell'oggetto, ma "marca" l'oggetto come deallocabile. Ci pensa piu il garbage colletor di .Net a liberare la memoria, quando lo ritiene piu opportuno...

Dovresti postare un esempio di codice che verifica il tuo problema.
Se ci sono apparenti memory leak è perchè mantieni qualche riferimento appeso da qualche parte per cui il GC non interviene.
Oppure il GC non interviene nonostante che il tuo codice sia corretto perchè non ritiene che sia necessario, ma è decisamente l'opzione meno probabile.

Allora... innanzittutto grazie per le vostre risposte...

Allego al post un esempio: un'applicazione banale con due form.
Provate ad aprire la finestra Form2 dal pulsante del Form1, poi chiudete la finestra Form2 e ripetete l'operazione. Dal task manager si vede che l'occupazione di memoria dell'applicazione cresce ogni volta. :stordita:
Non credo sia normale un comportamento del genere.

Qui il codice:
Form1:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
Form2 Window = new Form2();
Window.Show();
}
}

Form2:
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{

this.Dispose();
this.Close();
}
}

Come si può fare per risolvere questo problema?

P.S:
in questo esempio la memoria cresce lentamente perché si tratta di due stupidi form senza alcun controllo, ma vi assicuro che se si tratta di una finestra piena di controlli, l'occupazione di memoria cresce in modo considerevole. :D

:help:

alex783
27-04-2009, 09:41
up :stordita:

Ah, tra l'altro mi sono accorto anche che dopo aver richiamato il metodo "Close", l'oggetto Form2 viene effettivamente distrutto, tant'è che se provo a richiamarlo, mi viene sollevata un'eccezione di NullObjectReference (o qualcosa del genere). Ma allora come mai non viene liberata la memoria dal GC?

||ElChE||88
27-04-2009, 09:45
Provato con GC.Collect()?

MarcoGG
27-04-2009, 10:47
up :stordita:

Ah, tra l'altro mi sono accorto anche che dopo aver richiamato il metodo "Close", l'oggetto Form2 viene effettivamente distrutto, tant'è che se provo a richiamarlo, mi viene sollevata un'eccezione di NullObjectReference (o qualcosa del genere). Ma allora come mai non viene liberata la memoria dal GC?


Infatti. .Close() già da solo basterebbe a fare tutto.
.Dispose() non forza l'eliminazione dell'oggetto e il rilascio della memoria occupata nello stesso preciso istante in cui viene invocato, ma piuttosto "marca" l'oggetto in questione per il prossimo passaggio di GC.
In soldoni, senza stare a fare grandi discorsi sui massimi sistemi, direi che il comportamento "anomalo" riscontrato nella gestione della memoria in .Net, è dovuto principalmente a 2 fattori :

1. Il GC ha dei tempi "tutti suoi", spesso imperscrutabili. Bisogna lasciarlo fare. Tu dici che aprendo N Form2 da Form1 la memoria cresce continuamente, ma non è corretto. In effetti GC interviene ( nel suo solito modo "pigro", ma credo sia volutamente così ). Se ad esempio apri 10 Form2, poi chiudi la decima, non notando alcun decremento di uso della Ram, poi magari riapri una decima Form2 e stranamente la memoria occupata diminuisce...
GC.Collect() a mio parere non è una buona idea : anzitutto nella maggior parte dei casi non noterai alcuna ottimizzazione della Ram, secondariamente può essere anche "pericoloso" ( su molti Forum, anche su MSDN, spesso e volentieri ne viene sconsigliato l'uso )...

2. Non dimentichiamo che lo stesso Windows gioca un ruolo importante e che "storicamente" l'ottimizzazione della Ram non è mai stata un cavallo di battaglia. Perciò la colpa di tutto ciò si distribuisce nella catena Applicazione-FrameWork-Windows...

Possibile soluzione : tempo fa ho avuto il tuo stesso problema e l'ho risolto creandomi una piccola classe static con qualche metodo di gestione della Ram, prova questo ( dovrebbe dare una bella scrollatina alla memoria... ) :

public static void OttimizzaMemoria()
{
System.Diagnostics.Process CP = System.Diagnostics.Process.GetCurrentProcess();
try
{
CP.MaxWorkingSet = (IntPtr)((int)CP.MaxWorkingSet - 1);
}
catch (System.Exception)
{
//...
}
}

Chiamalo semplicemente dopo il .Close() del Form2... e dimmi che effetto ti fa ! :D ;)

sottovento
27-04-2009, 10:49
Sicuro che sia DAVVERO un memory leak? Mi sembra un po' presto per dirlo.
Prova a fare dei test piu' lunghi. Tutto sommato 8Mb sono ancora pochi, magari gc non si e' ancora deciso ad intervenire

||ElChE||88
27-04-2009, 10:51
GC.Collect() a mio parere non è una buona idea : anzitutto nella maggior parte dei casi non noterai alcuna ottimizzazione della Ram, secondariamente può essere anche "pericoloso" ( su molti Forum, anche su MSDN, spesso e volentieri ne viene sconsigliato l'uso )...
GC.Collect() è assoluatmente da evitare nel codice normale, ma in questo caso potrebbe indicare se il problema è del GC o del codice...

alex783
27-04-2009, 14:03
Infatti. .Close() già da solo basterebbe a fare tutto.
.Dispose() non forza l'eliminazione dell'oggetto e il rilascio della memoria occupata nello stesso preciso istante in cui viene invocato, ma piuttosto "marca" l'oggetto in questione per il prossimo passaggio di GC.
In soldoni, senza stare a fare grandi discorsi sui massimi sistemi, direi che il comportamento "anomalo" riscontrato nella gestione della memoria in .Net, è dovuto principalmente a 2 fattori :

1. Il GC ha dei tempi "tutti suoi", spesso imperscrutabili. Bisogna lasciarlo fare. Tu dici che aprendo N Form2 da Form1 la memoria cresce continuamente, ma non è corretto. In effetti GC interviene ( nel suo solito modo "pigro", ma credo sia volutamente così ). Se ad esempio apri 10 Form2, poi chiudi la decima, non notando alcun decremento di uso della Ram, poi magari riapri una decima Form2 e stranamente la memoria occupata diminuisce...
GC.Collect() a mio parere non è una buona idea : anzitutto nella maggior parte dei casi non noterai alcuna ottimizzazione della Ram, secondariamente può essere anche "pericoloso" ( su molti Forum, anche su MSDN, spesso e volentieri ne viene sconsigliato l'uso )...

2. Non dimentichiamo che lo stesso Windows gioca un ruolo importante e che "storicamente" l'ottimizzazione della Ram non è mai stata un cavallo di battaglia. Perciò la colpa di tutto ciò si distribuisce nella catena Applicazione-FrameWork-Windows...

Possibile soluzione : tempo fa ho avuto il tuo stesso problema e l'ho risolto creandomi una piccola classe static con qualche metodo di gestione della Ram, prova questo ( dovrebbe dare una bella scrollatina alla memoria... ) :

public static void OttimizzaMemoria()
{
System.Diagnostics.Process CP = System.Diagnostics.Process.GetCurrentProcess();
try
{
CP.MaxWorkingSet = (IntPtr)((int)CP.MaxWorkingSet - 1);
}
catch (System.Exception)
{
//...
}
}

Chiamalo semplicemente dopo il .Close() del Form2... e dimmi che effetto ti fa ! :D ;)

Ehm... :sofico: quando invoco questo metodo, la memoria occupata scende da 5MB a 1.5MB :D Ottimo! ;)

:ave: Grazie mille! :)

MarcoGG
27-04-2009, 18:43
Ehm... :sofico: quando invoco questo metodo, la memoria occupata scende da 5MB a 1.5MB :D Ottimo! ;)

:ave: Grazie mille! :)

:cool: ;)

Kralizek
27-04-2009, 19:18
mi spiegheresti cosa fa quella chiamata? :D

alex783
27-04-2009, 21:47
mi spiegheresti cosa fa quella chiamata? :D

Ho trovato questo su MSDN: http://msdn.microsoft.com/it-it/library/system.diagnostics.process.maxworkingset(VS.80).aspx

tomminno
27-04-2009, 23:37
Non è una mossa intelligente ridurre lo spazio di memoria dell'applicativo.

Se si vuole il controllo completo della memoria è bene cambiare linguaggio.

Altrimenti si prendono gli aspetti positivi del .NET trascurando gli inconvenienti (se questi costituiscono effettivamente un problema)

Steel Jans
28-04-2009, 09:08
Si vede che non hai ancora visto all'opera i componenti Timer... quelli si che ciucciano memoria se non stati attento!:D
Comunque non preoccuparti più di tanto perché in ogni caso non puoi cambiare la situazione. dot net comunque ha un sistema per il garbage collection che esegue automaticamente al verificarsi di determinate condizioni.

MarcoGG
28-04-2009, 11:50
Non è una mossa intelligente ridurre lo spazio di memoria dell'applicativo.

Se si vuole il controllo completo della memoria è bene cambiare linguaggio.

Altrimenti si prendono gli aspetti positivi del .NET trascurando gli inconvenienti (se questi costituiscono effettivamente un problema)

Quindi la tua "mossa intelligente", a chi chiede come ottimizzare l'utilizzo di memoria sviluppando in C#, è consigliare... di cambiare linguaggio ? :eh:

!k-0t1c!
28-04-2009, 12:31
Piuttosto che andare a pasticciare con il working set, che in caso può anche causare rallentamenti notevoli ed altri problemi se impostato male, forse vale la pena di forzare la collection di oggetti di generazioni più recenti da parte del GC..!

alex783
28-04-2009, 12:42
Quindi la tua "mossa intelligente", a chi chiede come ottimizzare l'utilizzo di memoria sviluppando in C#, è consigliare... di cambiare linguaggio ? :eh:

Il tuo metodo sta funzionando alla grande e per ora non sto riscontrando alcuna anomalia :D ;)

Purtroppo era un problema a cui tenevo molto risolvere, perché la mia applicazione deve restare caricata nella traybar, e non voglio che occupi 20MB, se non di più, anche quando non fa nulla.

Ora, invece, occupa sì 20 MB quando esegue i task, ma appena finisce, scende ad appena 1.5 MB ;)

gugoXX
28-04-2009, 12:45
Lascia tutto com'e'. Quando il sistema avra' bisogno di memoria, oppure quando il sistema non sara' particolarmente sotto stress, Il Garbage Collector passera' da solo e liberera' la memoria per altri processi.
La memoria resta in questo momento allocata per futuri usi. Poiche' allocare memoria costa tempo, la scelta e' quella di tenerla allocata fino a che non serve effettivamente per altro, sperando in un futuro riuso dello spazio gia' allocato dal Framework.
Il fatto che forzare GC.Collect() liberi la memoria e' segno che non c'e' alcun Memory Leak, in presenza dei quali il Garbage Collector non potrebbe fare molto.

MarcoGG
28-04-2009, 12:46
Piuttosto che andare a pasticciare con il working set, che in caso può anche causare rallentamenti notevoli ed altri problemi se impostato male, forse vale la pena di forzare la collection di oggetti di generazioni più recenti da parte del GC..!

1. Nessun pasticcio. Ho usato quel sistema in un'applicazione recentemente senza alcun problema. Se fosse così dannoso credo che, alla versione 2008 di C#, dovrebbe essere quantomeno deprecato.
Certo non consiglio di farlo in un Timer, o ad ogni click di un pulsante, ovvio. Sempre usare buon senso.

2. Beh, fatti un giretto su MSDN e molti altri Forum di Programmazione, e non troverai poi molta gente che consiglia di forzare GC.

MarcoGG
28-04-2009, 12:52
Il tuo metodo sta funzionando alla grande e per ora non sto riscontrando alcuna anomalia :D ;)

Purtroppo era un problema a cui tenevo molto risolvere, perché la mia applicazione deve restare caricata nella traybar, e non voglio che occupi 20MB, se non di più, anche quando non fa nulla.

Ora, invece, occupa sì 20 MB quando esegue i task, ma appena finisce, scende ad appena 1.5 MB ;)

Certo, anch'io l'ho usato senza alcun problema. :)
Le chiacchiere stanno a zero, perciò prova a fondo quella tecnica, ma non ne abusare, e ricorda che si tratta pur sempre di una "forzatura", come ho già detto in precedenza. ;)

banryu79
28-04-2009, 12:53
Lascia tutto com'e'. Quando il sistema avra' bisogno di memoria, oppure quando il sistema non sara' particolarmente sotto stress, Il Garbage Collector passera' da solo e liberera' la memoria per altri processi.
La memoria resta in questo momento allocata per futuri usi. Poiche' allocare memoria costa tempo, la scelta e' quella di tenerla allocata fino a che non serve effettivamente per altro, sperando in un futuro riuso dello spazio gia' allocato dal Framework.
Il fatto che forzare GC.Collect() liberi la memoria e' segno che non c'e' alcun Memory Leak, in presenza dei quali il Garbage Collector non potrebbe fare molto.
Quoto.
I Garbage Collector sono fatti apposta per gestire queste situazioni in modo trasparente allo sviluppatore; non toccare nulla e non fare nessuno tweak, per quanto possa sembrare valido, lascia lavorare liberamente l'ambiente virtuale .NET.
Come ha detto gugoXX quei 20 mega non sono effettivamente "rubati" al sistema, e il fatto che restino "ipotecati" alla tua applicazione anche se al momento non sono dalla stessa utilizzati significa che al sistema non servono.

||ElChE||88
28-04-2009, 13:31
2. Beh, fatti un giretto su MSDN e molti altri Forum di Programmazione, e non troverai poi molta gente che consiglia di forzare GC.
In pochi consigliano di forzare il GC, nessuno consiglia di abbassare costantemente il Working Set massimo.

E poi che succede quando MaxWorkingSet raggiunge MinWorkingSet? Lancia un'eccezione, 1) rendendo il trucchetto inutile 2) sprecando tempo.

MarcoGG
28-04-2009, 13:58
In pochi consigliano di forzare il GC, nessuno consiglia di abbassare costantemente il Working Set massimo.

E poi che succede quando MaxWorkingSet raggiunge MinWorkingSet? Lancia un'eccezione, 1) rendendo il trucchetto inutile 2) sprecando tempo.

Ma sì. Sul fatto di lasciar fare al GC senza forzature, sono d'accordissimo. L'ho consigliato io stesso prima di suggerire quel metodo.
Ma dal momento che alex783 insisteva nel chiedere comunque un sistema valido per quello scopo, ho proposto una via possibile.
E non ho certo scritto "quello è l'unico modo".
Perciò non ho altro da aggiungere, nell'attesa che qualcuno ponga una eventuale soluzione alternativa e concreta ( così come concreto è stato il mio intervento ) al problema sollevato da alex783.

tomminno
28-04-2009, 13:58
1. Nessun pasticcio. Ho usato quel sistema in un'applicazione recentemente senza alcun problema. Se fosse così dannoso credo che, alla versione 2008 di C#, dovrebbe essere quantomeno deprecato.
Certo non consiglio di farlo in un Timer, o ad ogni click di un pulsante, ovvio. Sempre usare buon senso.


Non è deprecato perchè è un wrapper sopra l'API SetProcessWorkingSetSize (mi chiedo sempre perchè non abbiano mai fatto un wrapper come si deve sopra le Win32 in C++), che generalmente è usata per aumentare il working set del processo, ma può essere lecitamente usata anche per diminuirlo, se questo ha un senso.

alex783
28-04-2009, 14:07
Certo, anch'io l'ho usato senza alcun problema. :)
Le chiacchiere stanno a zero, perciò prova a fondo quella tecnica, ma non ne abusare, e ricorda che si tratta pur sempre di una "forzatura", come ho già detto in precedenza. ;)

No, no, lo richiamo soltanto dopo i task che ciucciano tanta memoria (compressioni tramite DotNetZip). ;)



Comunque mi pare di aver capito, leggendo il libro C# 2005 (Hoepli Editore), che il metodo .Close() dei form non distrugge realmente l'oggetto ma soltanto il riferimento ad esso.

L'oggetto viene distrutto poi dal GC quando non ha più riferimenti attivi.
E' corretto, no?

MarcoGG
28-04-2009, 14:28
Comunque mi pare di aver capito, leggendo il libro C# 2005 (Hoepli Editore), che il metodo .Close() dei form non distrugge realmente l'oggetto ma soltanto il riferimento ad esso.

L'oggetto viene distrutto poi dal GC quando non ha più riferimenti attivi.
E' corretto, no?

Ti riporto quanto dichiarato su MSDN :
Quando un form viene chiuso vengono chiuse tutte le risorse create all'interno dell'oggetto e il form viene eliminato.
Le due condizioni in cui un form non viene eliminato in corrispondenza dell'evento Close sono :
1. quando il form fa parte di un'applicazione MDI e non è visibile
e
2. quando il form è stato visualizzato mediante ShowDialog.
In questi casi, sarà necessario chiamare Dispose manualmente per contrassegnare tutti i controlli del form per la procedura di Garbage Collection.

alex783
28-04-2009, 14:40
Ti riporto quanto dichiarato su MSDN :
Quando un form viene chiuso vengono chiuse tutte le risorse create all'interno dell'oggetto e il form viene eliminato.
Le due condizioni in cui un form non viene eliminato in corrispondenza dell'evento Close sono :
1. quando il form fa parte di un'applicazione MDI e non è visibile
e
2. quando il form è stato visualizzato mediante ShowDialog.
In questi casi, sarà necessario chiamare Dispose manualmente per contrassegnare tutti i controlli del form per la procedura di Garbage Collection.

Ah ecco... avevo capito male allora.
Grazie mille.

!k-0t1c!
28-04-2009, 19:31
1. Nessun pasticcio. Ho usato quel sistema in un'applicazione recentemente senza alcun problema. Se fosse così dannoso credo che, alla versione 2008 di C#, dovrebbe essere quantomeno deprecato.
Certo non consiglio di farlo in un Timer, o ad ogni click di un pulsante, ovvio. Sempre usare buon senso.

2. Beh, fatti un giretto su MSDN e molti altri Forum di Programmazione, e non troverai poi molta gente che consiglia di forzare GC.
Il fatto che in un caso non abbia causato problemi non significa che sia una buona pratica. E' un pasticcio bello e buono e il controllo del working set non dovrebbe essere fatto con sistemi da biscottai, ma se proprio uno vuole smanettarci dovrebbe predisporre algoritmi adeguati. Se forzare la collection da parte del GC recupera la memoria il problema non si pone, perché il GC è abbastanza "intelligente" da recuperare la memoria appena ci sarà urgenza di farlo, ma se uno ha proprio la smania di vedere il numero di KB usati scendere (e quindi nel tradeoff spazio/tempo privilegia lo spazio sacrificando il tempo di esecuzione della collection) basta usare i metodi del GC, e non cose che possono compromettere seriamente il funzionamento o le performance dell'applicazione nel tempo.

MarcoGG
28-04-2009, 21:37
Il fatto che in un caso non abbia causato problemi non significa che sia una buona pratica. E' un pasticcio bello e buono e il controllo del working set non dovrebbe essere fatto con sistemi da biscottai, ma se proprio uno vuole smanettarci dovrebbe predisporre algoritmi adeguati. Se forzare la collection da parte del GC recupera la memoria il problema non si pone, perché il GC è abbastanza "intelligente" da recuperare la memoria appena ci sarà urgenza di farlo, ma se uno ha proprio la smania di vedere il numero di KB usati scendere (e quindi nel tradeoff spazio/tempo privilegia lo spazio sacrificando il tempo di esecuzione della collection) basta usare i metodi del GC, e non cose che possono compromettere seriamente il funzionamento o le performance dell'applicazione nel tempo.

:blah: :rolleyes:
Mah sììììììì...
Non ho niente da ridire su questo, quante volte lo devo ripetere ?
Se hai letto il mio intervento direi che è chiaro il mio consiglio :
"Lascia fare a GC" !
Secondariamente ho consigliato quel metodo, vista l'insistenza dell'utente che ha aperto il Thread, e ho aggiunto quel "...e dimmi che effetto ti fa !"... Pensavo ormai fosse chiaro a tutti.
Non raccomando a nessuno di usare sistematicamente nè GC.Collect(), nè MaxWorkingSet, ma sulla scorta di una mia pregressa situazione ho postato quel codice per alex783 ( che mi pare abbia gradito molto, e che ne farà l'uso che crede opportuno ).
Non la considero affatto una buona pratica, anche se nel mio caso specifico si è rivelata efficace ( e non ci sono santi che sarei pronto a consigliarla di nuovo ! ).
Detto questo, visto che sei così bravo, predisponi tu l'algoritmo più adeguato, e magari posta un po' di codice, invece di chiacchiere.