Dette indlæg omhandler forholdsvis eksotiske detaljer omkring implementeringen af garbage collection i .NET. Det vil næppe være relevant i ret mange tilfælde, men at kortlægge disse detaljer var en underholdende og lærerig øvelse, så dette indlæg er i realiteten mere et vidnesbyrd om, hvad vi kan lære om .NETs afviklingsmiljø ved hjælp af WinDbg og SOS.
En kollega kom for nogle dage siden forbi og spurgte, om jeg vidste noget om en funktion ved navn gc_heap::relocate_in_large_objects. Det gjorde jeg tilfældigvis, for jeg var selv stødt på denne lidt spøjst navngivne funktion i anden sammenhæng. Navnet kan jo få en til at tro, at objekter på Large Object Heap (LOH) bliver flyttet, hvilket jo som bekendt ikke er tilfældet. Funktionen står derimod for at justere referencer i objekter på LOH.
Betragt nedenstående figur.

Her har vi et array på LOH med referencer til objekter på den almindelige heap. Oprydning af sidstnævnte kan føre til at objekterne bliver rykket sammen for at forhindre fragmentering. Hvis dette sker, skal referencerne i vores array justeres, så de igen peger på de faktiske instanser. Efter en oprydning, sammenrykning og justering kan billedet se ud som følger:

Det er der næppe noget nyt eller overraskende i, men min kollega synes at have observeret, at justering af referencer også skete, hvis den omtalte array ikke længere var i brug.
Kunne det være tilfældet?
Tænk lidt over det: Bliver referencer virkelig justeret for objekter, der ikke længere er i brug af applikationen?
Mit første svar var forkert. Jeg var overbevist om, at der ikke var nogen grund til at justere disse referencer i fald vores array ikke længere var i brug, men i stedet for at lade det komme an på, hvad jeg troede, satte jeg mig for at undersøge det.
Til formålet lavede jeg en lille testapplikation som angivet nedenfor:
sealed class SomeType {
public int X { get; set; }
public int Y { get; set; }
public SomeType(int x, int y) {
X = x;
Y = y;
}
}
sealed class Program {
static void GenerateRandomObjects() {
string s = null;
for (int i = 0; i < 1000; i++) {
s += i.ToString();
}
Console.WriteLine(s.Length);
}
static void Main() {
Console.WriteLine("Press Enter to begin");
Console.ReadLine();
GenerateRandomObjects();
var smallbuffer = new SomeType[100];
var largebuffer = new SomeType[85000];
for (int i = 0; i < smallbuffer.Length; i++) {
var st = new SomeType(1, 2);
smallbuffer[i] = st;
largebuffer[i] = st;
}
Trace.WriteLine("Buffers filled");
Debugger.Break();
Trace.WriteLine(string.Format("smallbuffer is in gen{0}", GC.GetGeneration(smallbuffer)));
Trace.WriteLine("Nulling root to largebuffer");
largebuffer = new SomeType[200];
Debugger.Break();
Trace.WriteLine(string.Format("smallbuffer is in gen{0}", GC.GetGeneration(smallbuffer)));
Trace.WriteLine("Collecting ...");
GC.Collect(1);
Trace.WriteLine(string.Format("smallbuffer is in gen{0}", GC.GetGeneration(smallbuffer)));
GenerateRandomObjects();
Trace.WriteLine("After GC");
Debugger.Break();
Console.WriteLine(smallbuffer.Length);
Console.WriteLine(largebuffer.Length);
}
}
Applikationen gør som følger:
- Først opretter den et array, der er stort nok til, at det ryger på LOH.
- Dette array sættes til at pege på et antal objekter allokeret på den almindelige heap. For at gøre det lettere at spore disse objekter, lader jeg også et mindre array referere disse.
- Det næste skridt er, at sørge for at det store array ikke længere er refereret. Det er ikke helt så let, som det lyder. Det er ofte utilstrækkeligt at sætte den lokale reference til
null. I mange tilfælde vil vi nemlig stadig have en gyldig reference på stakken eller i et register. Derfor er den bedste fremgangsmåde, at lade referencen pege på en ny instans. Det sikrer, at vi få overskrevet eventuelle gemte referencer.
- Dernæst skal jeg iværksætte en garbage collection, der også laver compaction, så objekterne bliver flyttet på heapen. Dette er også mere tricky end som så, da vi ikke har mulighed for at angive, at der skal foretages compaction ved kald til
GC.Collect(). Ergo, var jeg nødt til at prøve mig frem. Ved at lave en del tilfældige objekter rundt om mit lille array (og de refererede objekter) og derefter lave et kald tilGC.Collect(1), fik jeg sat gang i den nødvendige oprydning. Så langt så godt.
For at undersøge om referencerne faktisk blev justeret i det store array, gjorde jeg følgende:
Ved første break brugte jeg !dumpheap til at finde mine arrays. Det mindste array har plads til 100 referencer, så jeg filtrerede listen, så jeg kun fik instanser på mere end 400 bytes.
0:004> g
Buffers filled
(1bdc.1918): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0000a020 edx=0030f230 esi=00687048 edi=00000064
eip=761d22a1 esp=0030f1a0 ebp=0030f23c iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
KERNELBASE!DebugBreak+0x2:
761d22a1 cc int 3
0:000> !dumpheap -type Object[] -min 400
Address MT Size
02427060 64cf6c28 8208
026dfc7c 64cf6c28 416
026f9758 64cf6c28 528
03421010 64cf6c28 4096
03422020 64cf6c28 528
03422240 64cf6c28 4096
03423250 64cf6c28 8176
03425250 64cf6c28 340016
total 0 objects
Statistics:
MT Count TotalSize Class Name
64cf6c28 8 366064 System.Object[]
Total 8 objects
De to interessante instanser var henholdsvis 026dfc7c (smallbuffer) og 03425250 (largebuffer).
For at verificere, at de begge pegede på samme elementer, udskrev jeg første element i hver array.
Først smallbuffer:
0:000> !da -details -length 1 026dfc7c
Name: LOHRoots.SomeType[]
MethodTable: 64cf6c28
EEClass: 64a79698
Size: 416(0x1a0) bytes
Array: Rank 1, Number of elements 100, Type CLASS
Element Methodtable: 001638cc
[0] 026dfe1c
Name: LOHRoots.SomeType
MethodTable: 001638cc
EEClass: 001614e4
Size: 16(0x10) bytes
File: C:\dev2010\LOHRoots\LOHRoots\bin\Release\LOHRoots.exe
Fields:
MT Field Offset Type VT Attr Value Name
64d42978 4000001 4 System.Int32 1 instance 1 <X>k__BackingField
64d42978 4000002 8 System.Int32 1 instance 2 <Y>k__BackingField
Og derefter largebuffer:
0:000> !da -details -length 1 03425250
Name: LOHRoots.SomeType[]
MethodTable: 64cf6c28
EEClass: 64a79698
Size: 340016(0x53030) bytes
Array: Rank 1, Number of elements 85000, Type CLASS
Element Methodtable: 001638cc
[0] 026dfe1c
Name: LOHRoots.SomeType
MethodTable: 001638cc
EEClass: 001614e4
Size: 16(0x10) bytes
File: C:\dev2010\LOHRoots\LOHRoots\bin\Release\LOHRoots.exe
Fields:
MT Field Offset Type VT Attr Value Name
64d42978 4000001 4 System.Int32 1 instance 1 <X>k__BackingField
64d42978 4000002 8 System.Int32 1 instance 2 <Y>k__BackingField
Læg mærke til, at begge elementer peger på 026dfe1c. Så langt så godt.
Herefter kørte jeg videre til det punkt, hvor referencen til largebuffer overskrives:
0:000> g smallbuffer is in gen0 Nulling root to largebuffer (1bdc.1918): Break instruction exception - code 80000003 (first chance) eax=00000000 ebx=00000000 ecx=0000a020 edx=0030f230 esi=00687048 edi=0242b3f8 eip=761d22a1 esp=0030f1a0 ebp=0030f23c iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 KERNELBASE!DebugBreak+0x2: 761d22a1 cc int 3
Herefter verificerede jeg, at applikationen ikke længere havde roots til largebuffer.
0:000> !gcroot 03425250 Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info. Scan Thread 0 OSTHread 1918 Scan Thread 2 OSTHread 1d34
Ingen roots, så den instans largebuffer peger på, kan ryddes op på dette tidspunkt. Videre til næste stop.
0:000> g smallbuffer is in gen0 Collecting ... smallbuffer is in gen1 After GC (1bdc.1918): Break instruction exception - code 80000003 (first chance) eax=00000000 ebx=00000000 ecx=0000a020 edx=0030f230 esi=00687048 edi=0242865c eip=761d22a1 esp=0030f1a0 ebp=0030f23c iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 KERNELBASE!DebugBreak+0x2: 761d22a1 cc int 3
På dette tidspunkt har der netop været en garbage collection. Vi ved endnu ikke, om der også har været compaction. For at undersøge dette kan vi se, om vores arrays er blevet flyttet.
0:000> !dumpheap -type Object[] -min 400
Address MT Size
0242566c 64cf6c28 8208
0242a3d0 64cf6c28 416
0243fb64 64cf6c28 816
03421010 64cf6c28 4096
03422020 64cf6c28 528
03422240 64cf6c28 4096
03423250 64cf6c28 8176
03425250 64cf6c28 340016
total 0 objects
Statistics:
MT Count TotalSize Class Name
64cf6c28 8 366352 System.Object[]
Total 8 objects
Læg mærke til, at vores lille array er flyttet fra 026dfc7c til 0242a3d0, mens vores store array stadig befinder sig på 03425250. Der har altså været compaction i forbindelse med denne garbage collection.
Det interessante er således at finde ud af, om referencerne i vores store (og ubrugte) array er blevet justeret. Lad os se på det lille array:
0:000> !da -details -length 1 0242a3d0
Name: LOHRoots.SomeType[]
MethodTable: 64cf6c28
EEClass: 64a79698
Size: 416(0x1a0) bytes
Array: Rank 1, Number of elements 100, Type CLASS
Element Methodtable: 001638cc
[0] 0242a570
Name: LOHRoots.SomeType
MethodTable: 001638cc
EEClass: 001614e4
Size: 16(0x10) bytes
File: C:\dev2010\LOHRoots\LOHRoots\bin\Release\LOHRoots.exe
Fields:
MT Field Offset Type VT Attr Value Name
64d42978 4000001 4 System.Int32 1 instance 1 <X>k__BackingField
64d42978 4000002 8 System.Int32 1 instance 2 <Y>k__BackingField
Og derefter det store:
0:000> !da -details -length 1 03425250
Name: LOHRoots.SomeType[]
MethodTable: 64cf6c28
EEClass: 64a79698
Size: 340016(0x53030) bytes
Array: Rank 1, Number of elements 85000, Type CLASS
Element Methodtable: 001638cc
[0] 0242a570
Name: LOHRoots.SomeType
MethodTable: 001638cc
EEClass: 001614e4
Size: 16(0x10) bytes
File: C:\dev2010\LOHRoots\LOHRoots\bin\Release\LOHRoots.exe
Fields:
MT Field Offset Type VT Attr Value Name
64d42978 4000001 4 System.Int32 1 instance 1 <X>k__BackingField
64d42978 4000002 8 System.Int32 1 instance 2 <Y>k__BackingField
Læg mærke til at begge arrays peger på samme element. Referencen i det store array er således rettet fra 026dfe1c til 0242a570, og dermed kan vi konkludere, at det store array er blevet opdateret på trods af, at applikationen ikke længere har nogen gældende reference til dette array.
Hvordan kan det være?
Tænker vi lidt over, hvad der sker under en garbage collection, giver det faktisk god mening, men det er måske ikke så intuitivt ved første øjekast.
En garbage collection lægger ud med at lave en analyse af hvilke objekter på den aktuelle del af heapen, der stadig er i brug. Disse markeres som værende i brug. Alle andre objekter kan ryddes op. Nøgleordet her er den aktuelle del af heapen. I og med at testprogrammet kører en GC.Collect(1) foretages denne analyse således kun for objekter der ligger i generation 0 og 1. Vores store array ligger på LOH, og er således ikke berørt af denne analyse.
Da oprydningen også iværksætter en flytning af objekterne i dette tilfælde, er garbage collection-rutinen nødt til at opdatere referencerne til de flyttede objekter, men da den på dette tidspunkt ikke ved, at vores store array ikke længere er i brug, er den nødt til at opdatere disse referencer.
Disponeringen giver god mening, hvis analysen af aktive rødder er mere omfattende end justering af referencerne. Skønt det optimale ville være at springe opdateringen over, kan det sagtens give mening at udføre denne – i teorien – overflødige handling, hvis disse forhold er til stede. Den analyse har jeg svært ved at lave, men jeg har været i kontakt med en af de ansvarlige udviklere, og Microsoft kender til problematikken, og derfor har jeg tillid til, at de har lavet de nødvendige analyser og undersøgelser.
Ændrer vi testapplikationen så den kalder GC.Collect(2) i stedet (eller blot GC.Collect(), der ligeledes laver en komplet oprydning), laver garbage collection-rutinen en fuld analyse af heapen og finder således ud af, at det store array ikke længere er i brug. I det tilfælde opdateres referencerne naturligvis ikke, da den nødvendige viden er tilgængelig i dette tilfælde.
Som nævnt tror jeg ikke, at dette er et reelt problem i nogen væsentlig udstrækning. For at denne situation skal have indflydelse på afviklingen, skal applikationen have mange midlertidige objekter på LOH, disse skal i stor udstrækning referere objekter på den almindelige heap, og compaction skal foretages jævnligt. I det tilfælde vil man muligvis kunne spore en effekt, men det er i realiteten et afledt problem af at have mange midlertidige objekter på LOH. Det er af flere grunde en dårlig ide, og dette er således kun en af flere uheldige konsekvenser af den situation.