Nye SOS-kommandoer i .NET 4 – første del

Den kommende version af .NET byder på flere nyheder i selve afviklingsmiljøet. Derfor er den medfølgende SOS også blevet opdateret til at arbejde med den nyeste version af CLRen, der meget passende er blevet omdømt fra mscorwks.dll til clr.dll. Den første synlige ændring er således måden, hvorpå vi indlæser SOS.

Normalt har jeg blot benyttet mig af, at jeg har .NET framework i min path, og derfor kan jeg blot skrive.

.load sos

Det finder den korrekte version af SOS til framework version 2.0. Det virker ikke umiddelbart længere, da vi har brug for at kunne indlæse den korrekte version, afhængig af om vi debugger en applikation til .NET 4.0 eller tidligere. For at indlæse SOS til .NET 4.0, skal vi derfor bruge .loadby således:

.loadby sos clr

Det indlæser den version af sos.dll, der ligger sammen med clr.dll. Vi kan verificere, at vi har fået indlæst den korrekte version ved hjælp af .chain.

0:004> .chain
...
Extension DLL chain:
    C:\Windows\Microsoft.NET\Framework\v4.0.21006\sos: image 4.0.21006.1, API 1.0.0, built Tue Oct 06 23:13:16 2009
        [path: C:\Windows\Microsoft.NET\Framework\v4.0.21006\sos.dll]
...

Som det fremgår af ovenstående udsnit, har vi fået indlæst den version af SOS, der hører til version 4 af .NET framework. Med det på plads, er vi klar til at se på de nye kommandoer.

Nye kommandoer

I Beta 2 er der, så vidt jeg kan se følgende nye kommandoer: AnalyzeOOM, DumpSigElem, FindRoots, GCWhere, HeapStat, HistClear, HistInit, HistObj, HistObjFind, HistRoot, ListNearObj, VerifyObj og ThreadState. I Beta 1 var der desuden en HistStat-kommando, der er forsvundet hjælpen. I dette indlæg skal vi se på nogle af de nye kommandoer. Jeg vender tilbage til de resterende kommandoer i efterfølgende indlæg.

FindRoots og GCWhere

Formålet med FindRoots er at finde ud af, hvorfor instanser, der ikke længere bliver refereret, ikke er blevet fjernet af GC. Det er ikke ualmindelig, at objekter i generation 1 eller 2 ligger på heapen uden aktive rødder. Hvis en oprydning af generation 0 tilgodeser applikationens behov, vil eventuelle instanser i generation 1 og 2 ikke blive ryddet op uanset, at de ikke længere er i brug af applikationen. De vil først blive opryddet, når deres respektive generationer bliver opryddet.

Vi kan bruge FindRoots til at finde objekter i den tilstand. Kommandoen fungerer lidt anderledes end de kendte SOS-kommandoer, idet den udføres af to omgange. Først bruger vi -gen til at sætte et breakpoint, når en given generation bliver ryddet op. Derefter kører vi programmet og venter på at vores breakpoint bliver aktiveret. Når dette er sket, kan vi undersøge om specifikke objekter vil blive ryddet op i denne ombæring.

Lad os se på et eksempel. Betragt nedenstående stump kode.

class Foo {
   public byte[] buffer { get; set; }
}

public static class Program {
    static void Main() {
       Console.ReadLine();
       var f = new Foo();
       GC.Collect();
       GC.Collect(); // f peger på instans i gen2

       Debugger.Break(); 

       var buffer = new byte[1000]; // gen0
       f.buffer = buffer; // gen2 objekt peger på gen0
       Console.WriteLine(f.buffer.Length);

       Debugger.Break(); 

       buffer = null;
       Console.WriteLine(f);
       f = null;
       GC.Collect(0);
   }
}

Kører vi programmet, attacher mens vi venter på ReadLine() og fortsætter med g, indtil vi rammer Debugger.Break(), kan vi se på tilstanden.

Vi kan bruge DumpStackObjects (dso) til at lokalisere vores instans af Foo. Vi ved, at der er en reference til den på stakken, og i og med vi er stoppet som følge af Debugger.Break(), vil vi være på den korrekt tråd.

0:000> !dso
OS Thread Id: 0x654 (0)
ESP/REG  Object   Name
003FEED8 0251d0b8 System.Security.Permissions.SecurityPermission
003FEEEC 0251d0b8 System.Security.Permissions.SecurityPermission
003FEF80 0251d094 TestApp.Foo
003FEF8C 0251d094 TestApp.Foo
003FF3C8 03513250 System.Object[]    (System.Object[])

Det ser ud til at vores instans er på adressen 0251d094. Lad os se efter.

0:000> !do 0251d094
Name:        TestApp.Foo
MethodTable: 00193880
EEClass:     001914a0
Size:        12(0xc) bytes
File:        c:\dev2010\TestApp\TestApp\bin\Release\TestApp.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
589ff9f8  4000001        4        System.Byte[]  0 instance 00000000 <buffer>k__BackingField

Yep, det er vores instans af Foo, og som vi kan se, er der ikke noget i vores buffer-property. Eftersom at vi har kørt GC.Collect() to gange, er vores instans nu i generation 2. Det kan vi verificere med GCWhere.

0:000> !gcwhere 0251d094
Address    Gen   Heap   segment    begin      allocated   size
0251d094   2      0     02510000   02511000   0251d8cc    0xc(12)

Som forventet er vores objekt i generation 2. Lad os forsætte.

0:000> g
(744.18f8): Break instruction exception - code 80000003 (first chance)
…

Vi udskriver vores objekt igen og skulle gerne kunne se en reference til en instans af byte[].

0:000> !do 0251d094
Name:        TestApp.Foo
MethodTable: 00193880
EEClass:     001914a0
Size:        12(0xc) bytes
File:        c:\dev2010\TestApp\TestApp\bin\Release\TestApp.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
589ff9f8  4000001        4        System.Byte[]  0 instance 0251d0c4 <buffer>k__BackingField

Vores instans af Foo refererer nu en instans af byte[] på adressen 0251d0c4. Denne instans er i generation 0, da vi lige har allokeret den, og nu kan vi bruge FindRoots til at indskyde et breakpoint, når denne generation bliver ryddet op.

0:000> !findroots -gen 0

Herefter fortsætter vi afviklingen og venter på den næste oprydning af generation 0 (som vi i dette tilfælde selv fremprovokerer ved kald af GC.Collect()).

0:000> g
(744.18f8): CLR notification exception - code e0444143 (first chance)
CLR notification: GC - Performing a gen 0 collection. Determined surviving objects...
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0041e95c ebx=00000000 ecx=00000003 edx=00000000 esi=0041ea08 edi=00000003
eip=771fb727 esp=0041e95c ebp=0041e9ac iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
KERNELBASE!RaiseException+0x58:
771fb727 c9              leave

Vi kan nu bruge den anden version af FindRoots til at finde ud af, hvorfor vores objekt ikke er blevet ryddet op, skønt det ligger i generation 0 og ikke længere er refereret af applikationen.

0:000> !findroots 0251d0c4
older generations::Root:  0251d094(TestApp.Foo)->
  0251d0c4(System.Byte[])

FindRoots fortæller os, at et objekt i en ældre generation holder vores instans i live. Hvad siger FindRoots til vores objekt i generation 2.

0:000> !findroots 0251d094
Object 0251d094 will survive this collection:
	gen(0x251d094) = 2 > 0 = condemned generation.

Her kan vi se, at vores instans ikke bliver ryddet op fordi, der er et objekt i generation 2, der stadig refererer det, og eftersom generation 2 ikke bliver ryddet op i denne ombæring, er vores instans stadig i brug.

Eksemplet her er en smule konstrueret, så derfor vil den ivrige læser sikkert opdage, at GCRoot også melder vores buffer-instans som værende i brug, skønt vi kan se på koden, at dette ikke er tilfældet. Grunden til GCRoots udmelding skyldes, at vi stadig har en stakreference til vores instans af Foo, eftersom vores stak stadig er i live. Under mere realistiske forhold ville denne information ikke længere være i spil. Kører vi således GCRoot med -nostacks, er udmeldingen da også som forventet, at der ikke er nogen rødder til vores instans.

HeapStat

HeapStat giver et hurtig overblik over størrelsen og den tilgængelige plads for de forskellige heaps. Oversigten er inddelt per generation, så det er let at få indsigt i forbruget. Der er også en procentvis angivelse af hvor meget plads, der er tilbage i de forskellige heaps. SOH står for Small Object Heap og dækker generation 0 til 2. LOH står for Large Object Heap og dækker den del af heapen, der bruges til opbevaring af instanser på mere end 85000 bytes.

Output ser ud som følger:

0:002> !heapstat
Heap     Gen0         Gen1         Gen2         LOH
Heap0    177904       12           306956       8784
Heap1    159652       12           12           16
Total    337556       24           306968       8800        

Free space:                                                 Percentage
Heap0    28           12           12           64          SOH:  0% LOH:  0%
Heap1    104          12           12           16          SOH:  0% LOH:100%
Total    132          24           24           80

ListNearObj

ListNearObj leder efter instanser umiddelbart før og efter en given instans på heapen. Samtidig foretager den en validering af heapens tilstand.

Kommandoen er brugbar i to situationer: Hvis heapen er i god stand, fortæller den os noget om hvordan objekterne ligger indbyrdes. Det kan f.eks. være brugbart i forbindelse med Free-områder, da de ofte opstår som følge af mellemliggende pinned objekter.

Hvis vi har en smadret heap og ser på en instans, hvis tilstand er forkert, kan det være en hjælp at kunne finde objekter i nærheden af dette, eftersom de meget vel kan være medvirkende til problemerne.

Kommandoen bruges som følger:

0:004> !listnearobj 02912010
Before:  02911010         4096 (0x1000)	System.Object[]
Current: 02912010           16 (0x10)	Free object
After:   02912020          528 (0x210)	System.Object[]
Heap local consistency confirmed.

ThreadState

ThreadState er analog til min lille kommando af same navn. Den tager en samling flag fra Threads-kommandoen og oversætter dem fra deres hex-repræsentation til noget mere sigende. Så hvis Threads fortæller os, at en given tråd har State b220, kan vi bruge ThreadState til at få dette oversat som vist nedenfor.

0:004> !threadstate b220
    Legal to Join
    Background
    CLR Owns
    CoInitialized
    In Multi Threaded Apartment

Det er måske stadig ikke så uddybende, som man kunne ønske sig, men det er da en anelse mere forståelig end hex-værdien.

Det var ordene for denne gang. Jeg vender tilbage med flere detaljer om de nye kommandoer i senere indlæg.

Leave a Reply