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<BarCollectionDescriptor, BarFromTickFactory> FactoriesByDesc =
    new Dictionary<BarCollectionDescriptor, BarFromTickFactory>();

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<int, List<BarFromTickFactory>> FactoryListBySymbolId =
    new Dictionary<int, List<BarFromTickFactory>>();

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.

3 Responses to “Managed memory leak nyomozás WinDBG-vel”

  1. erseka Says:

    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.

  2. Aikon Says:

    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á?

  3. Soczó Zsolt Says:

    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.

Leave a Reply