W poniższym artykule wykorzystano materiały znajdujące się w następujących artykułach Tess:
http://blogs.msdn.com/tess/archive/2005/11/25/i-have-a-memory-leak-what-do-i-do-defining-the-where.aspx
http://blogs.msdn.com/tess/archive/2005/11/25/dumpheap-stat-explained-debugging-net-leaks.aspx
Uwaga: postanowiłem nie tłumaczyć większości pojęć w CLR, np. takich jak „assemblies”, „loader”, „viewstate”, itp. Wychodzą z tego potworki językowe, które wprowadzają tylko zamieszanie i niekoniecznie muszą być wszystkim znane. Jeżeli kogoś razi takie podejście, to z góry przepraszam.
Jednocześnie proszę o zwrócenie mi na to uwagi – jeżeli liczba niezadowolonych będzie duża, postaram się ustalić jakiś wspólny mianownik.
Rozróżnijmy dwa pojęcia: wyciek pamięci oraz wysokie użycie pamięci.
Z wyciekiem pamięci mamy do czynienia gdy zaalokujemy pamięć i zagubimy do niej wskaźnik, co prowadzi do tego, że nie możemy jej zwolnić.
Dopóki mamy wskaźnik do zaalokowanej pamięci, mówimy o wysokim użyciu pamięci.
Obie sytuacje mogą być równie złe z naszego punktu widzenia i prowadzić do podobnych skutków, jednak nadal są one różne.
Dla uproszczenia, w dalszej części artykułu będziemy używać terminu „wyciek pamięci.”
Wyróżniamy dwa typy wycieków pamięci: stopniowy, gdzie użycie pamięci rośnie w przybliżeniu liniowo, i nagły. Rozwiązujemy je w podobny sposób, jednak przy nagłym wycieku staramy się znaleźć powiązane, wyjątkowe zdarzenie, które zaszło w chwili wycieku, np. bardzo duże obciążenie serwera, itp.
Pamięć procesu zawiera wiele różnych bytów, takich jak wątki, zarządzane sterty, zarządzaną stertę loader’a, natywne sterty, biblioteki dll i alokacje wirtualne dokonywane przez obiekty COM, zatem dobrym miejscem by zacząć, jest sprawdzenie w monitorze wydajności następujących liczników:
| Process/Virtual Bytes | Ilość zaalokowanej pamięci wirtualnej. |
| Process/Private Bytes | Ilość zaalokowanej pamięci fizycznej. |
| .net CLR Memory/# Bytes in all Heaps | Ilość bajtów we wszystkich stertach. |
| .net CLR Memory/% Time in GC | Procent czasu działania aplikacji, spędzony na odśmiecanie. |
| .net CLR Memory/Large Object Heap size | Ilość zaalokowanej pamięci na stercie dla dużych obiektów (Large Object Heap - LOH). Duże obiekty to te, których wielkość przekracza 85 000 bajtów. |
| .net CLR Loading/Bytes in Loader Heap | Ilość bajtów zaalokowanych na stercie loadera. |
| .net CLR Loading/Current Assemblies | Ilość załadowanych assemblies. |
Poszukujemy takiej sytuacji, gdzie ilość bajtów prywatnych rośnie w przybliżeniu w tym samym tempie, co ilość bajtów wirtualnych, oraz czy „#Bytes in all Heaps” podąża tą samą ścieżką.
Jeżeli ilość bajtów prywatnych będzie się zwiększać, ale „#Bytes in all Heaps” pozostanie bez zmian, mamy prawdopodobnie do czynienia z wyciekiem natywnej pamięci, np. z powodu alokacji dokonywanych przez komponenty COM. Jeżeli „#Bytes in all Heaps” zwiększa się w podobnym tempie jak bajty prywatne, wyciek jest zlokalizowany w kodzie zarządzanym.
Podobnie, jeżeli widać zwiększającą się ilość bajtów wirtualnych, ale bajty prywatne pozostają bez zmian, problemem jest pewnie rezerwacja dużej ilość pamięci wirtualnej, która nie jest później używana.
Z chwilą uruchomienia procesu i załadowania wszystkich domen aplikacyjnych (appdomains), liczniki „#Bytes in Loader Heap” i „#Current Assemblies” powinny utrzymywać się na niezmienionym poziomie. Jeżeli jednak liczniki te rosną, bardzo prawdopodobny jest wyciek pamięci z assembly loader’a.
Jeżeli mamy do czynienia z aplikacją w ASP.net, należy sprawdzić w web.config, czy atrybut debug=”false” oraz symptomy, nie są identyczne z opisanymi w artykułach KB:
Memory usage is high when you create several XmlSerializer objects oraz Assembly leak because of script blocks in XSLT's.
Użycie WinDbg do debugowania wycieku pamięci zarządzanej
Najbardziej pomocnym poleceniem WinDbg w przypadku debugowania wycieku pamięci zarządzanej jest !dumpheap. Pokazuje ono listę wszystkich obiektów na zarządzanych stertach, a używając różnych przełączników tego polecenia, możemy formatować wyniki.
!dumpheap jest poleceniem wbudowanym w rozszerzenie SOS, które otrzymujemy wraz z instalacją .Net Framework. Plik zlokalizowany jest w folderze, w którym zainstalowaliśmy .Net Framework. Jeżeli zainstalowaliśmy SDK v1.1 w domyślnym folderze, podstawowy opis użycia SOS możemy znaleźć w C:\Program Files\Microsoft Visual Studio .NET 2003\SDK\v1.1\Tool Developers Guide\Samples\sos.
Rozróżniamy dwie kategorie obiektów przechowywanych na stercie:
- obiekty które są zakorzenione (rooted), np. jakiś obiekt przechowuje do nich wskaźnik (referencję),
oraz - obiekty które zostały właśnie utworzone (młode) lub przestały być zakorzenione od czasu ostatniego odśmiecania.
By uniknąć przeglądania dużej ilości danych, które i tak zostaną usunięte podczas najbliższego odśmiecania, można posłużyć się pożytecznym trikiem:
1. obciążyć aplikację (stress),
2. ręcznie wymusić odśmiecanie, wywołując GC.Collect(3),
3. wykonać zrzut pamięci procesu (dump),
4. obciążyć aplikację (stress) po raz kolejny,
5. ręcznie wymusić odśmiecanie, wywołując GC.Collect(3),
6. wykonać zrzut pamięci procesu (dump),
7. porównać obiekty znajdujące się na stertach w obydwu zrzutach.
Przełącznik -stat polecenia !dumpheap, pokazuje zestawienie statystyk dotyczących obiektów na stercie:
0:000> !dumpheap -stat
0x79c489a0 1 12 System.Runtime.Remoting.Messaging.ClientContextTerminatorSink
0x79bf9aec 1 12 System.IO.TextReader/NullTextReader
0x79be7078 1 12 System.Runtime.Remoting.Proxies.ProxyAttribute
0x79bce8e0 1 12 System.Runtime.InteropServices.ComVisibleAttribute
0x79bce7c8 1 12 System.CLSCompliantAttribute
0x79bc08e0 1 12 System.Empty
0x0618ae68 1 12 System.Web.Configuration.CustomErrorsConfigHandler
0x061887f8 1 12 System.Web.UI.WebControls.UnitConverter
0x06180848 1 12 System.Drawing.ColorConverter
0x05dbfbc4 1 12 System.Data.Res
<… wycięto …>
0x03f1236c 625 2,820,896 System.Char[]
0x04ad88f4 102,874 2,880,472 System.Web.UI.ControlCollection
0x0469bdf0 156,650 3,133,000 System.Collections.Specialized.HybridDictionary
0x04ad91bc 164,516 3,290,320 System.Web.UI.Triplet
0x03f134a8 7,582 3,799,704 System.Collections.Hashtable/bucket[]
0x04ade5e4 47,395 4,549,920 System.Web.UI.WebControls.Label
0x061826bc 58,197 4,888,548 System.Web.UI.DataBoundLiteralControl
0x04adff44 323,119 5,169,904 System.Web.UI.StateItem
0x0618788c 63,437 6,089,952 System.Web.UI.WebControls.TableCell
0x0469c5c4 309,132 6,182,640 System.Collections.Specialized.ListDictionary/DictionaryNode
0x0011cec0 305 6,240,720 Free
0x79ba2ee4 270,831 6,499,944 System.Collections.ArrayList
0x03f16d9c 222 7,703,284 System.DateTime[]
0x04add34c 105,502 8,018,152 System.Web.UI.LiteralControl
0x0615c6f4 558,019 11,160,380 System.Data.DataRowView
0x03f15d1c 3,783 15,447,528 System.Boolean[]
0x060bcc74 570,274 22,810,960 System.Data.DataRow
0x03f15fd4 702 50,930,472 System.Decimal[]
0x03f131e8 21,013 60,573,352 System.Int32[]
0x03f1209c 508,734 75,399,184 System.Object[]
0x79b94638 5,286,303 697,441,440 System.String
Total 9,712,896 objects, Total size: 1,032,127,612
Pierwsza kolumna zawiera identyfikator z tabeli metod danego typu. Jeżeli zrzucimy dane wybranego obiektu, pierwszą wartością DWORD będzie identyfikator tabeli metod, która zawiera wskaźniki do informacji o danym typie, takie jak lista zmiennych składowych, lista implementowanych metod, itp. Tabela metod unikalnie identyfikuje typ.
Druga kolumna zawiera ilość obiektów danego typu na stercie. W powyższym przykładzie na stercie zaalokowanych jest 5.286.303 obiektów typu string.
Trzecia kolumna zawiera całkowity rozmiar pamięci zajmowanych przez te obiekty, zatem nasze stringi zajmują na stercie około 695 MB. Uwaga: całkowity rozmiar obiektów nie zawiera zmiennych składowych.
Rozważmy przykład klasy System.Data.DataSet:
0x060bbd2c 221 17,680 System.Data.DataSet
Widzimy że na stercie znajduje się 221 obiektów klasy DataSet, które zajmują w sumie 17.680 bajtów, co oznacza że średnio zajmują one 80 bajtów. Jeżeli prezentowana wartość reprezentowałby całkowity rozmiar obiektu data set, byłyby to bardzo małe obiekty.
Jeżeli wyświetlimy listę obiektów klasy DataSet, poprzez polecenie !dumpheap –mt <tabela metod>, a następnie wybierzemy jeden z nich i wyświetlimy jego zawartość poprzez !dumpobj <adres>, zobaczymy listing podobny do poniższego:
0:000> !dumpobj 0x3920ed4c
Name: System.Data.DataSet
MethodTable 0x060bbd2c
EEClass 0x060d2614
Size 80(0x50) bytes
GC Generation: 2
mdToken: 0x0200003b (c:\windows\assembly\gac\system.data\1.0.5000.0__b77a5c561934e089\system.data.dll)
FieldDesc*: 0x060bb358
MT Field Offset Type Attr Value Name
0x060b252c 0x4000583 0x4 CLASS instance 0x00000000 site
0x060b252c 0x4000584 0x8 CLASS instance 0x00000000 events
0x060b252c 0x4000582 0 CLASS shared static EventDisposed
>> Domain:Value 0x001192a0:NotInit 0x0017fc40:NotInit 0x044b7b28:0x1c357cb8 <<
0x060bbd2c 0x40003d3 0xc CLASS instance 0x00000000 defaultViewManager
0x060bbd2c 0x40003d4 0x10 CLASS instance 0x3920ee28 tableCollection
0x060bbd2c 0x40003d5 0x14 CLASS instance 0x3920ed9c relationCollection
0x060bbd2c 0x40003d6 0x18 CLASS instance 0x00000000 extendedProperties
0x060bbd2c 0x40003d7 0x1c CLASS instance 0x1c357c90 dataSetName
0x060bbd2c 0x40003d8 0x20 CLASS instance 0x182d0224 _datasetPrefix
0x060bbd2c 0x40003d9 0x24 CLASS instance 0x182d0224 namespaceURI
0x060bbd2c 0x40003da 0x40 System.Boolean instance 0 caseSensitive
0x060bbd2c 0x40003db 0x28 CLASS instance 0x14309a0c culture
0x060bbd2c 0x40003dc 0x41 System.Boolean instance 1 enforceConstraints
0x060bbd2c 0x40003dd 0x42 System.Boolean instance 0 fInReadXml
0x060bbd2c 0x40003de 0x43 System.Boolean instance 0 fInLoadDiffgram
0x060bbd2c 0x40003df 0x44 System.Boolean instance 0 fTopLevelTable
0x060bbd2c 0x40003e0 0x45 System.Boolean instance 0 fInitInProgress
0x060bbd2c 0x40003e1 0x46 System.Boolean instance 1 fEnableCascading
0x060bbd2c 0x40003e2 0x47 System.Boolean instance 0 fIsSchemaLoading
0x060bbd2c 0x40003e3 0x2c CLASS instance 0x00000000 rowDiffId
0x060bbd2c 0x40003e4 0x48 System.Boolean instance 0 fBoundToDocument
0x060bbd2c 0x40003e5 0x30 CLASS instance 0x00000000 onPropertyChangingDelegate
0x060bbd2c 0x40003e6 0x34 CLASS instance 0x00000000 onMergeFailed
0x060bbd2c 0x40003e7 0x38 CLASS instance 0x00000000 onDataRowCreated
0x060bbd2c 0x40003e8 0x3c CLASS instance 0x00000000 onClearFunctionCalled
0x060bbd2c 0x40003e9 0 CLASS shared static zeroTables
>> Domain:Value 0x0017fc40:NotInit 0x044b7b28:0x1c357c80 <<
Wspomniane 80 bajtów wystarczy do przechowania wszystkich wskaźników do zmiennych składowych, ale dane rzeczywiście składowane w DataSet, znajdują się w kolekcji tabel i jej zmiennych składowych. By otrzymać rzeczywisty rozmiar obiektu, włączając w to zmienne składowe tego obiektu, należy użyć polecenia !objsize <adres>.
Zmienne składowe same są obiektami na stercie, zatem w wyniku polecenia !dumpheap –stat zostaną one wykazana osobno, a w naszym przypadku, zagregowany rozmiar 1 032 127 612 bajtów, jest rzeczywistym rozmiarem wszystkich obiektów na stertach.
Niektóre typy, jak string, byte[] czy char[], zawierają rzeczywiste dane w strukturze, co oznacza że całkowity rozmiar wykazany przez !dumpheap -stat, będzie taki sam lub bardzo zbliżony do całkowitego rozmiaru obiektów. Z tego powodu oraz z powodu bycia popularnymi typami, w większości przypadków będziemy widzeli je w roli największych okupantów pamięci. W związku z tym, w przypadku poszukiwania wycieków pamięci, bardziej pomocnym może być szukanie wśród obiektów występujących nieco rzadziej.
W rozważanym przypadku możemy zauważyć ogromną ilość obiektów typu DataRow, zatem dobrym punktem początkowym byłoby znalezienie przyczyny tak dużej ich ilości.
Duża ilość stringów prawdopodobnie jest składową obiektów DataRow, których na stercie zaalokowano 570.274. Biorąc pod uwagę 221 DataSet’ów, każdy z nich średnio zawiera 2.580 wierszy, co oznacza że są to duże DataSet’y. By tego dowieść, moglibyśmy sprawdzić wielkość kilku wybranych, używając do tego polecenia !objsize.
Kolejną ważną rzeczą są obiekty raportowane jako Free. Nie są to obiekty, a pamięć zwolniona podczas odśmiecania, której jeszcze nie scalono (not compacted). Pamięć ta może być wykorzystana do kolejnych alokacji. Jeżeli zauważymy dużą ilość wpisów reprezentujących wolną pamięć na stercie (Free), możemy mieć do czynienia z problemem przypinania (pinning) obiektów. Precyzując, możemy przyjąć regułę, że jeżeli 30% pamięci na stercie jest raportowana jako wolna (Free), istnieje duże prawdopodobieństwo występowania powyższego problemu, którego dobre wyjaśnienie możemy znaleźć na blogu Moani, pod adresem http://blogs.msdn.com/maoni.
Innymi, przydatnymi przełącznikami są:
| -mt | zwraca obiekty o wskazanej tabeli metod |
| -type | zwraca obiekty których nazwa pasuje do podanego wzorca |
| -min | zwraca wszystkie obiekty, których rozmiar przekracza podany |
| -fix | zwraca wszystkie obiekty znajdujące się pomiędzy dwoma adresami w pamięci |
| -l <liczba> | zwraca tylko określoną ilość obiektów, podaną w <liczba> |
Powyższe przełączniki możemy ze sobą łączyć, np.:
!dumpheap -type String -min 85000
zwróci nam wszystkie obiekty typu String, zaalokowane na stercie dla dużych obiektów (LOH). Rozmiar 85 000 bajtów jest wielkością, która powoduje że obiekt jest alokowany na LOH, zamiast na zwykłej stercie.
Przykładowym rezultatem powyższego polecenia będzie:
0:067> !dumpheap -type String -min 85000
Using our cache to search the heap.
Address MT Size Gen
0x212d0030 0x79b94638 86,812 -1 System.String dDwtMTE0NjcyOTQ4MTt0PDtsPGk8Mz
0x222d0030 0x79b94638 86,812 -1 System.String dDwtMTE0NjcyOTQ4MTt0PDtsPGk8Mz
0x232d0030 0x79b94638 86,688 -1 System.String dDwtMTE0NjcyOTQ4MTt0PDtsPGk8Mz
Statistics:
MT Count TotalSize Class Name
0x79b94638 3 260,312 System.String
Total 3 objects, Total size: 260,312
Oznacza to że na LOH znajdują się 3 stringi, o wielkości odpowiednio 86 812, 86 812 i 86 688 bajtów, reprezentujące tekst o długości około 40 000 znaków, ponieważ każdy znak w .net jest kodowany przy użyciu 2 bajtów (unicode).
Uwaga: ostatnia kolumna wyniku powyższego polecenia, zawiera kilkanaście pierwszych znaków stringu. Warto wiedzieć że stringi zaczynające się od znaków „dDw” są strigami kodowanymi Base64. W większości przypadków, widząc takie stringi, możemy spodziewać się że reprezentują one viewstate. W rozważanym przykładzie, analizowany proces zawiera strony ASP.net o viewstate znaczącej wielkości.