Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

Diagnostyka wycieków pamięci

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.

Opublikowane 24 września 2008 21:38 przez Bysza
Filed under: ,

Komentarze:

# re: Diagnostyka wycieków pamięci

25 września 2008 09:24 by arkadiusz.wasniewski

Super. Bez dwóch zdań.

# re: Diagnostyka wycieków pamięci

26 września 2008 00:12 by mgrzeg

Nic dodac, nic ujac - czad! :) Wiecej, prosze!

Komentarze anonimowe wyłączone

About Bysza

http://www.linkedin.com/in/bysza