Anatomia di una "perdita di memoria"


173

Nella prospettiva .NET:

  • Che cos'è una perdita di memoria ?
  • Come puoi determinare se la tua applicazione perde? Quali sono gli effetti?
  • Come puoi prevenire una perdita di memoria?
  • Se l'applicazione presenta perdite di memoria, scompare quando il processo termina o viene interrotto? O le perdite di memoria nell'applicazione influiscono su altri processi sul sistema anche dopo il completamento del processo?
  • E che dire del codice non gestito a cui si accede tramite interoperabilità COM e / o P / Invoke?

Risposte:


110

La migliore spiegazione che ho visto è nel capitolo 7 del libro elettronico gratuito Foundations of Programming .

Fondamentalmente, in .NET si verifica una perdita di memoria quando gli oggetti referenziati sono radicati e quindi non possono essere raccolti. Ciò si verifica accidentalmente quando si mantengono i riferimenti oltre l'ambito previsto.

Saprai che hai delle perdite quando inizi a ottenere OutOfMemoryExceptions o l'utilizzo della memoria va oltre ciò che ti aspetteresti ( PerfMon ha dei bei contatori di memoria).

Comprendere il modello di memoria di .NET è il modo migliore per evitarlo. In particolare, capire come funziona il Garbage Collector e come funzionano i riferimenti - ancora, ti rimando al capitolo 7 dell'e-book. Inoltre, fai attenzione alle insidie ​​comuni, probabilmente gli eventi più comuni. Se oggetto A è registrato ad un evento su un oggetto B , allora oggetto A si attacca intorno fino all'oggetto B scompare perché B contiene un riferimento a A . La soluzione è annullare la registrazione dei tuoi eventi quando hai finito.

Naturalmente, un buon profilo di memoria ti permetterà di vedere i grafici dei tuoi oggetti ed esplorare la nidificazione / riferimento dei tuoi oggetti per vedere da dove provengono i riferimenti e quale oggetto radice è responsabile ( profilo delle formiche del cancello rosso , JetBrains dotMemory, memprofiler sono davvero buoni scelte, oppure puoi usare solo WinDbg e SOS di testo , ma consiglio vivamente un prodotto commerciale / visivo a meno che tu non sia un vero guru).

Credo che il codice non gestito sia soggetto alle sue tipiche perdite di memoria, tranne per il fatto che i riferimenti condivisi sono gestiti dal Garbage Collector. Potrei sbagliarmi su quest'ultimo punto.


11
Oh, ti piace il libro, vero? Di tanto in tanto ho visto l'autore apparire su StackOverflow.
Johnno Nolan,

Alcuni oggetti .NET possono anche eseguire il root e diventare non collezionabili. Qualunque cosa IDisposable dovrebbe essere eliminata per questo motivo.
Kyoryu,

1
@kyoryu: in che modo un oggetto esegue il root stesso?
Andrei Rînea,

2
@Andrei: Penso che un Thread in esecuzione sia forse il miglior esempio di rooting di un oggetto. Un oggetto che inserisce un riferimento a se stesso in una posizione statica non pubblica (come la sottoscrizione a un evento statico o l'implementazione di un singleton mediante l'inizializzazione del campo statico) potrebbe anche aver effettuato il root, poiché non esiste un modo ovvio per ... um ... "sradicarlo" dal suo ormeggio.
Jeffrey Hantin,

@Jeffry questo è un modo non convenzionale per descrivere cosa sta succedendo e mi piace!
Exitos,

35

A rigor di termini, una perdita di memoria sta consumando memoria che "non è più utilizzata" dal programma.

"Non più utilizzato" ha più di un significato, potrebbe significare "non più riferimento ad esso", cioè totalmente irrecuperabile, o potrebbe significare, referenziato, recuperabile, inutilizzato ma il programma mantiene comunque i riferimenti. Solo in seguito si applica a .Net per oggetti perfettamente gestiti . Tuttavia, non tutte le classi sono perfette e ad un certo punto un'implementazione non gestita sottostante potrebbe perdere risorse permanentemente per quel processo.

In tutti i casi, l'applicazione consuma più memoria di quanto strettamente necessario. Gli effetti collaterali, a seconda della quantità trapelata, potrebbero andare da nessuno, al rallentamento causato da un'eccessiva raccolta, a una serie di eccezioni di memoria e infine a un errore fatale seguito da una chiusura forzata del processo.

Sai che un'applicazione ha un problema di memoria quando il monitoraggio mostra che sempre più memoria è allocata al tuo processo dopo ogni ciclo di garbage collection . In tal caso, si sta conservando troppa memoria o si sta verificando una perdita di implementazione non gestita sottostante.

Per la maggior parte delle perdite, le risorse vengono recuperate al termine del processo, tuttavia alcune risorse non vengono sempre recuperate in alcuni casi precisi, gli handle di cursore GDI sono noti per questo. Naturalmente, se si dispone di un meccanismo di comunicazione tra processi, la memoria allocata nell'altro processo non verrebbe liberata fino a quando tale processo non lo libera o termina.


32

Penso che le domande "cos'è una perdita di memoria" e "quali sono gli effetti" abbiano già ricevuto una risposta, ma volevo aggiungere qualche altra cosa alle altre domande ...

Come capire se l'applicazione perde

Un modo interessante è quello di aprire perfmon e aggiungere tracce per # byte in tutte le pile e raccolte # Gen 2 , in ogni caso guardando solo al tuo processo. Se l'esercizio di una particolare funzione fa aumentare i byte totali e la memoria rimane allocata dopo la prossima raccolta Gen 2, si potrebbe dire che la funzione perde memoria.

Come prevenire

Altre buone opinioni sono state date. Vorrei solo aggiungere che forse la causa più comunemente trascurata delle perdite di memoria di .NET è quella di aggiungere gestori di eventi agli oggetti senza rimuoverli. Un gestore di eventi collegato a un oggetto è una forma di riferimento a quell'oggetto, quindi impedirà la raccolta anche dopo che tutti gli altri riferimenti sono andati. Ricorda sempre di staccare i gestori di eventi (usando la -=sintassi in C #).

La perdita scompare quando termina il processo e per quanto riguarda l'interoperabilità COM?

Quando il processo termina, tutta la memoria mappata nel suo spazio degli indirizzi viene recuperata dal sistema operativo, inclusi eventuali oggetti COM serviti da DLL. Comparativamente raramente, gli oggetti COM possono essere serviti da processi separati. In questo caso, al termine del processo, è possibile che l'utente sia comunque responsabile della memoria allocata in tutti i processi del server COM utilizzati.


19

Definirei perdite di memoria come un oggetto che non libera tutta la memoria allocata dopo che è stata completata. Ho scoperto che ciò può accadere nella tua applicazione se stai utilizzando Windows API e COM (ovvero codice non gestito che contiene un bug o non è gestito correttamente), nel framework e nei componenti di terze parti. Ho anche scoperto che non riordinare dopo aver usato determinati oggetti come penne può causare il problema.

Personalmente ho subito eccezioni di memoria insufficiente che possono essere causate ma non sono esclusive delle perdite di memoria nelle applicazioni dot net. (OOM può anche provenire dal pinning vedi Pinning Artical ). Se non si verificano errori OOM o è necessario confermare se si tratta di una perdita di memoria che la causa, l'unico modo è creare un profilo dell'applicazione.

Vorrei anche provare a garantire quanto segue:

a) Tutto ciò che implementa Idisposable viene eliminato utilizzando un blocco finally o l'istruzione using che include pennelli, penne ecc. (alcune persone sostengono di non impostare nulla su in aggiunta)

b) Tutto ciò che ha un metodo close viene chiuso di nuovo usando finalmente o l'istruzione using (anche se ho trovato che l'utilizzo non si chiude sempre a seconda che tu abbia dichiarato l'oggetto al di fuori dell'istruzione using)

c) Se si utilizzano codice non gestito / API di Windows, queste vengono trattate correttamente dopo. (alcuni hanno metodi di pulizia per liberare risorse)

Spero che questo ti aiuti.


19

Se è necessario diagnosticare una perdita di memoria in .NET, controllare questi collegamenti:

http://msdn.microsoft.com/en-us/magazine/cc163833.aspx

http://msdn.microsoft.com/en-us/magazine/cc164138.aspx

Questi articoli descrivono come creare un dump della memoria del processo e come analizzarlo in modo da poter prima determinare se la perdita non è gestita o gestita, e se è gestita, come capire da dove proviene.

Microsoft ha anche uno strumento più recente per aiutare a generare crash dump, in sostituzione di ADPlus, chiamato DebugDiag.

http://www.microsoft.com/downloads/details.aspx?FamilyID=28bd5941-c458-46f1-b24d-f60151d875a3&displaylang=en



15

La migliore spiegazione di come funziona il Garbage Collector è in Jeff Richters CLR tramite il libro C # (Cap. 20). Leggere questo fornisce un'ottima base per comprendere come gli oggetti persistono.

Una delle cause più comuni del rooting degli oggetti accidentalmente è l'aggancio di eventi al di fuori di una classe. Se si collega un evento esterno

per esempio

SomeExternalClass.Changed += new EventHandler(HandleIt);

e dimentica di sganciarlo quando lo smaltisci, quindi SomeExternalClass ha un riferimento alla tua classe.

Come accennato in precedenza, il profiler di memoria SciTech è eccellente nel mostrarti radici di oggetti che sospetti stiano perdendo.

Ma c'è anche un modo molto veloce per verificare che un tipo particolare sia solo usare WnDBG (puoi anche usarlo nella finestra immediata di VS.NET mentre sei collegato):

.loadby sos mscorwks
!dumpheap -stat -type <TypeName>

Ora fai qualcosa che pensi possa disporre degli oggetti di quel tipo (es. Chiudere una finestra). È utile qui avere un pulsante di debug da qualche parte che verrà eseguito System.GC.Collect()un paio di volte.

Quindi corri di !dumpheap -stat -type <TypeName>nuovo. Se il numero non è diminuito o non è diminuito tanto quanto ti aspetti, allora hai una base per ulteriori indagini. (Ho ricevuto questo suggerimento da un seminario tenuto da Ingo Rammer ).


14

Immagino che, in un ambiente gestito, ti mancherebbe un riferimento inutile a un grosso pezzo di memoria in giro.


11

Perché le persone pensano che una perdita di memoria in .NET non sia uguale a qualsiasi altra perdita?

Una perdita di memoria si verifica quando ci si collega a una risorsa e non si lascia andare. Puoi farlo sia nella codifica gestita che non gestita.

Per quanto riguarda .NET e altri strumenti di programmazione, ci sono state idee sulla raccolta dei rifiuti e altri modi per ridurre al minimo le situazioni che potrebbero far perdere la tua applicazione. Ma il metodo migliore per prevenire perdite di memoria è che devi capire il tuo modello di memoria sottostante e come funzionano le cose sulla piattaforma che stai utilizzando.

Credere che GC e altra magia ripuliranno il tuo pasticcio è la via più breve per le perdite di memoria, e sarà difficile da trovare in seguito.

Quando si codifica unmanaged, di solito ci si assicura di ripulire, si sa che le risorse di cui si prende possesso, sarà la propria responsabilità di ripulire, non quella del bidello.

In .NET d'altra parte, molte persone pensano che il GC pulirà tutto. Bene, fa un po 'per te, ma devi assicurarti che sia così. .NET racchiude molte cose, quindi non sempre sai se hai a che fare con una risorsa gestita o non gestita e devi assicurarti di cosa stai trattando. La gestione di caratteri, risorse GDI, active directory, database ecc. È in genere ciò che devi cercare.

In termini gestiti, metterò il mio collo sulla linea per dire che scompare una volta che il processo viene ucciso / rimosso.

Vedo che molte persone hanno questo, e spero davvero che finirà. Non puoi chiedere all'utente di terminare la tua app per ripulire il tuo casino! Dai un'occhiata a un browser, che può essere IE, FF ecc., Quindi apri, diciamo, Google Reader, lascialo riposare per alcuni giorni e guarda cosa succede.

Se poi apri un'altra scheda nel browser, navighi in qualche sito, quindi chiudi la scheda che ha ospitato l'altra pagina che ha fatto trapelare il browser, pensi che il browser rilascerà la memoria? Non così con IE. Sul mio computer IE consumerà facilmente 1 GiB di memoria in un breve lasso di tempo (circa 3-4 giorni) se utilizzo Google Reader. Alcune pagine sono anche peggiori.


10

Immagino che, in un ambiente gestito, ti mancherebbe un riferimento inutile a un grosso pezzo di memoria in giro.

Assolutamente. Inoltre, non utilizzare il metodo .Dispose () su oggetti usa e getta quando appropriato può causare perdite di mem. Il modo più semplice per farlo è con un blocco using perché esegue automaticamente .Dispose () alla fine:

StreamReader sr;
using(sr = new StreamReader("somefile.txt"))
{
    //do some stuff
}

E se crei una classe che utilizza oggetti non gestiti, se non stai implementando IDisposable correttamente, potresti causare perdite di memoria per gli utenti della tua classe.


9

Tutte le perdite di memoria vengono risolte al termine del programma.

Perdita di memoria sufficiente e il sistema operativo potrebbe decidere di risolvere il problema per tuo conto.


8

Concordo con Bernard su come .net quale sarebbe una perdita di mem.

È possibile creare un profilo dell'applicazione per vederne l'utilizzo della memoria e determinare che se gestisce molta memoria quando non dovrebbe essere, si potrebbe dire che ha una perdita.

In termini gestiti, metterò il mio collo sulla linea per dire che scompare una volta che il processo viene ucciso / rimosso.

Il codice non gestito è la sua bestia e se esiste una perdita al suo interno, seguirà un mem standard. definizione della perdita.


7

Tieni inoltre presente che .NET ha due heap, uno dei quali è l'heap di oggetti di grandi dimensioni. Credo che oggetti di circa 85k o più siano messi su questo mucchio. Questo heap ha regole di durata diverse rispetto all'heap normale.

Se stai creando grandi strutture di memoria (Dizionario o Elenco), sarebbe prudente cercare quali sono le regole esatte.

Per quanto riguarda il recupero della memoria al termine del processo, a meno che non sia in esecuzione Win98 o equivalenti, tutto viene rilasciato al sistema operativo al termine. Le uniche eccezioni sono le cose che vengono aperte tra processi e un altro processo ha ancora la risorsa aperta.

Gli oggetti COM possono essere complicati. Se usi sempre il IDisposemodello, sarai al sicuro. Ma ho incontrato alcuni assembly di interoperabilità che implementano IDispose. La chiave qui è chiamare Marshal.ReleaseCOMObjectquando hai finito. Gli oggetti COM utilizzano ancora il conteggio dei riferimenti COM standard.


6

Ho trovato .Net Memory Profiler un ottimo aiuto per trovare perdite di memoria in .Net. Non è gratuito come il Profiler CLR Microsoft, ma è più veloce e più a mio avviso. UN


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.