Could you hire me? Contact me if you like what I’ve done in this article and think I can create value for your company with my skills.

November 30, 2009 / by Zsolt Soczó

Managed memory leak nyomozás WinDBG-vel

Imádom a WinDbg-t, mondtam már? Az egyik kis programom módszeresen eszegette a memóriát. Ciklusban végez feldolgozást, rettentő sok adattal, és ezek egy része szépen bennragadt a memóriában. .NET-ben nincs memory leak a klasszikus értelemben, de van oly módon, hogy a rootokból marad referencia egyes objektumokra, így azok élve maradnak, szándékaink ellenére. Néha azonban nem triviális kinyomozni, melyik root miatt ragadt be valami a memóriába.
A VSTS profiler segítségével odáig el lehet jutni, hogy ki ragad be a memóriába. Az Object LifeTime nézetben az Instances alive at end oszlopra rendeztetve láthatjuk, kik maradnak élve a program végén. Én beraktam egy force-olt GC-zést a program végére, így aki ezek után még benn maradt, azok egy része szándékaim ellenére tette ezt, ezért azt leaknek tekintem, és meg kell szüntetni.
Jöhet a WinDbg. File, Open Executable, F5. A program végén a GC után raktam még egy Console.ReadLine()-t. Amikor ide eljut, a debuggerben CTRL-Breakkel megállítom a program futását (várakozását). Betöltöm az sos-t:
.load C:\Windows\Microsoft.NET\Framework64\v4.0.21006\sos.dll

Kilistáztatom a GC heapen levő ojjektumokat:
!DumpHeap -stat

000007ff005c2278 2766 221280 System.Data.Metadata.Edm.TypeUsage
000007fef1b90d00 370 222136 System.Byte[]
000007fef1b89be0 13379 1330896 System.String
000007fef1b8c8a8 1125 1447552 System.Int32[]
000007fef1b3bef0 9298 3224392 System.Object[]
000007ff00274988 245954 29514480 ATS.Bar

Akinek nem szabadna már a memóriában lenni, az az ATS.Bar objektumok, 245954 darab, 29514480 bájt méretben. Nézzük megy a példányokat belőle:

!DumpHeap -type ATS.Bar

0000000004952c60 000007ff00274988 120
0000000004952cd8 000007ff00274988 120
0000000004952d50 000007ff00274988 120
0000000004952dc8 000007ff00274988 120
0000000004952e40 000007ff00274988 120
0000000004952eb8 000007ff00274988 120
total 0 objects
Statistics:
MT Count TotalSize Class Name
000007ff00fb38a0 1 24 System.Collections.Generic.GenericEqualityComparer`1[[ATS.BarCollectionDescriptor, Common]]
000007ff00271050 1 24 ATS.BarDAL
000007ff00270e68 1 48 ATS.BarFactory
000007ff00fb31b8 1 88 System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib],[System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]], mscorlib]]
000007ff00fb27a8 1 88 System.Collections.Generic.Dictionary`2[[ATS.BarCollectionDescriptor, Common],[ATS.BarFromTickFactory, Common]]
000007ff00275378 1 88 System.Collections.Generic.Dictionary`2[[ATS.Symbol, Common],[ATS.BarCollectionByInterval, Common]]
000007ff00fb3ee0 1 96 System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]], mscorlib]][]
000007ff00fb3b10 1 96 System.Collections.Generic.Dictionary`2+Entry[[ATS.BarCollectionDescriptor, Common],[ATS.BarFromTickFactory, Common]][]
000007ff00fb0498 1 96 System.Collections.Generic.Dictionary`2+Entry[[ATS.Symbol, Common],[ATS.BarCollectionByInterval, Common]][]
000007ff00fb2d30 3 120 System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]]
000007ff00790990 3 120 ATS.BarCollectionDescriptor
000007ff00fb1568 7 168 System.Collections.ObjectModel.ObservableCollection`1+SimpleMonitor[[ATS.Bar, Common]]
000007ff00886530 3 240 ATS.BarFromTickFactory
000007ff00fb1648 7 280 System.Collections.Generic.List`1[[ATS.Bar, Common]]
000007ff00273788 7 952 ATS.BarCollection
000007ff00274988 245954 29514480 ATS.Bar
Total 245993 objects

A kis 120 bájtos izék a problémásak (a végén az összefoglaló azért tartalmaz több típust is, mert substring szűrést csinál a -type). Az első oszlop az egyedi objektumok címe, a második a típus metódusleíró táblája.
És most jön a lényeg. Ki miatt érhető el a rootokból mondjuk az utolsó Bar példány?

!GCRoot 0000000004952eb8

DOMAIN(00000000003CF530):HANDLE(Pinned):1217d8:Root: 0000000012657048(System.Object[])->
0000000002bf7f28(System.Collections.Generic.Dictionary`2[[System.Int32, mscorlib],[System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]], mscorlib]])->
0000000002bf94b8(System.Collections.Generic.Dictionary`2+Entry[[System.Int32, mscorlib],[System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]], mscorlib]][])->
0000000002f06ca0(System.Collections.Generic.List`1[[ATS.BarFromTickFactory, Common]])->
0000000002f06cc8(System.Object[])->
0000000002f05860(ATS.BarFromTickFactory)->
0000000004952eb8(ATS.Bar)

Ebben az látszik, az első bejegyzésből, amit még a CLR startup hozzott létre hogyan lehet eljutni a beragadt objektumunkhoz. A CLR generikus neveket C#-ra visszafordítva az látszik, hogy egy Dictionary> hivatkozik egy Dictionary.Entry>-re, az a dictionary belső tárolóeleme. Ez rámutat egy List-re, ami továbbmutat egy ATS.BarFromTickFactory-ra, ami hivatkozik a Bar-ra.

Ez alapján már rekonstruálható a probléma forrása. A probléma elemi oka, hogy túlzásba vittem a statikusok használatát, és nem figyeltem a takarításukra.

static readonly Dictionary FactoriesByDesc =
new Dictionary();

Ebből rendesen kiszedtem a tárolt BarFromTickFactory példányt, ha már nem volt rá szükség. De elfeledkeztem róla, hogy volt egy másik kollekció is:

public static readonly Dictionary> FactoryListBySymbolId =
new Dictionary>();

Ebből viszont nem szedtem ki a hivatkozás az adott BarFromTickFactory-ra, így az szépen beragadt a memóriába.

Tanulságok:
1. Szeretjük a WinDbt-t.
2. Kerüljük a statikusokat. Ha a BarFromTickFactory példányokat egy mások osztály példányai tárolnák, akkor ha azok kifutnak a szkópból, automatikusan a GC martalékai lesznek. A sok statikus sok odafigyelést igényel, kár erőltetni őket.

Could you hire me? Contact me if you like what I’ve done in this article and think I can create value for your company with my skills.

LEAVE A COMMENT

3 COMMENTS

  • erseka November 30, 2009

    Annyit fűznék hozzá, hogy a ‘!dumpheap -type’ helyett egy kicsit közelebbi megoldást ad a ‘!dumpheap -mt’ parancs. A -stat kérésre adott válasz első oszlopa az MT (Method Table) amit felhasználva a -mt parancs után az adott típus példányainak címét listázza.
    Például itt:
    !dumpheap -stat

    000007ff00274988 245954 29514480 ATS.Bar

    !dumpheap -mt 000007ff00274988

    itt válaszként az összes példányt megkapjuk, az első oszlop hordozza a !gcroot vagy !do parancshoz szükséges címet.

  • Aikon November 30, 2009

    Szia! Csak feltételezem, hogy ez egy automatikus kereskedési rendszer lesz. Érdeklődnék – mivel ilyen jellegű fejlesztést én is elkezdtem egykor, csak abbamaradt – hogy már kipróbáltad -e (legalább backtestekkel), és sikerült -e jól működő heurisztikákat találnod hozzá?

  • Soczó Zsolt December 1, 2009

    erseka: köszi a tippet, a legközelebbi nyomozáskor kipróbálom.

    Aikon: igen, ATS-t fejlesztek. Jó trading algom még nincs, még mindig csak az infrastruktúrát fejlesztem. Január csendesnek ígérkezik, akkor végre lesz újra sok időm foglalkozni vele.