This is the ninth part of the .NET Internals Cookbook series. For your convenience you can find other parts in the table of contents in Part 0 – Table of contents
Table of Contents
59. What happens if an exception is thrown in finalizer? What about infinite loop?
Let’s see this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
using System; namespace Program { public class Program { public static void Main(string[] args) { Console.WriteLine("Testing Foo"); var foo = new Foo(); foo = null; GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Testing Bar"); var bar = new Bar(); bar = null; GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Done!"); } } } class Foo { ~Foo() { Console.WriteLine("Good destructor"); } } class Bar { ~Bar() { Console.WriteLine("Destructor with exception"); throw new Exception(); } } |
If you run it in dotnetfiddle you get this:
1 2 3 4 5 6 |
Run-time exception (line -1): Exception of type 'System.Exception' was thrown. Stack Trace: [System.Exception: Exception of type 'System.Exception' was thrown.] at Bar.Finalize() |
So you don’t see anything printed out but you do see the exception.
If you try it in tio:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Testing Foo Good destructor Testing Bar Done! Destructor with exception Microsoft (R) Build Engine version 16.0.0.0 for Mono Copyright (C) Microsoft Corporation. All rights reserved. Unhandled Exception: System.Exception: Exception of type 'System.Exception' was thrown. at Bar.Finalize () [0x0000d] in <fb493746d55b41718381c31bf369d391>:0 [ERROR] FATAL UNHANDLED EXCEPTION: System.Exception: Exception of type 'System.Exception' was thrown. at Bar.Finalize () [0x0000d] in <fb493746d55b41718381c31bf369d391>:0 Real time: 2.698 s User time: 2.186 s Sys. time: 0.489 s CPU share: 99.17 % Exit code: 255 |
So you see all lines and exception thrown out of bands.
And if I try it locally with VS 2017:
1 2 3 4 5 6 7 8 |
Testing Foo Good destructor Testing Bar Destructor with exception Unhandled Exception: System.Exception: Exception of type 'System.Exception' was thrown. at Bar.Finalize() in C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\Program.cs:line 41 Press any key to continue . . . |
So it looks like your mileage may vary. However, it kills the application, generally, as specified in documentation:
1 |
If Finalize or an override of Finalize throws an exception, and the runtime is not hosted by an application that overrides the default policy, the runtime terminates the process and no active try/finally blocks or finalizers are executed. This behavior ensures process integrity if the finalizer cannot free or destroy resources. |
Since finalizers are run on a separate thread, you get deadlock with infinite loop:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
using System; namespace Program { public class Program { public static void Main(string[] args) { Console.WriteLine("Testing Foo"); var foo = new Foo(); foo = null; GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Testing Bar"); var bar = new Bar(); bar = null; GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("Done!"); } } } class Foo{ ~Foo(){ Console.WriteLine("Good destructor"); } } class Bar{ ~Bar(){ Console.WriteLine("Destructor with infinite loop"); while(true){} } } |
1 2 3 4 5 |
Testing Foo Good destructor Testing Bar Destructor with infinite loop Fatal Error: Execution time limit was exceeded |
60. What is finalization queue? What is f-reachable queue?
When your object has a destructor (finalizer), it is stored in finalization queue when allocated. Later, when the object is not reachable anymore and GC detects that, it puts it to f-reachable queue. Finalizers are not called directly, they are called on a separate thread which gets the objects to cleanup from f-reachable queue.
Let’s take this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
using System; namespace Program { public class Program { public static void Main(string[] args) { Console.WriteLine("Allocating"); var foo = new Foo(); Console.ReadLine(); Console.WriteLine("Collecting"); foo = null; GC.Collect(); Console.ReadLine(); Console.WriteLine("Finalizing"); GC.WaitForPendingFinalizers(); Console.ReadLine(); Console.WriteLine("Done!"); } } } class Foo { ~Foo() { Console.WriteLine("Good destructor"); } } |
Let’s start it and attach debugger on first line read:
1 |
0:006> .loadby sos clr |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
0:006> !finalizequeue SyncBlocks to be cleaned up: 0 Free-Threaded Interfaces to be released: 0 MTA Interfaces to be released: 0 STA Interfaces to be released: 0 ---------------------------------- generation 0 has 8 finalizable objects (0063a018->0063a038) generation 1 has 0 finalizable objects (0063a018->0063a018) generation 2 has 0 finalizable objects (0063a018->0063a018) Ready for finalization 0 objects (0063a038->0063a038) Statistics for all finalizable objects (including all objects ready for finalization): MT Count TotalSize Class Name 02484dc0 1 12 Foo 7253c188 1 20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle 7253c138 1 20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle 7252dde8 1 20 Microsoft.Win32.SafeHandles.SafePEFileHandle 72531888 2 40 Microsoft.Win32.SafeHandles.SafeFileHandle 725312e0 1 44 System.Threading.ReaderWriterLock 7252f734 1 52 System.Threading.Thread Total 8 objects |
So we can see that there is a Foo
object in finalization queue. We can see its roots:
1 2 3 4 5 6 7 8 |
0:006> !dumpheap -type Foo Address MT Size 02684170 02484dc0 12 Statistics: MT Count TotalSize Class Name 02484dc0 1 12 Foo Total 1 objects |
1 2 3 4 5 6 7 |
0:006> !gcroot -all 02684170 Thread 2e74: 0055f150 025604a0 Program.Program.Main(System.String[]) [C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\Program.cs @ 11] ebp+8: 0055f160 -> 02684170 Foo Found 1 roots. |
1 2 3 4 5 6 7 8 9 |
0:006> !gcroot -nostacks 02684170 Finalizer Queue: 02684170 -> 02684170 Foo Warning: These roots are from finalizable objects that are not yet ready for finalization. This is to handle the case where objects re-register themselves for finalization. These roots may be false positives. Found 1 unique roots (run '!GCRoot -all' to see all roots). |
So there is one normal root (our variable) and a finalization queue.
Before we move on, let’s freeze the GC thread:
1 2 3 4 5 6 7 8 9 10 11 |
0:006> !threads ThreadCount: 2 UnstartedThread: 0 BackgroundThread: 1 PendingThread: 0 DeadThread: 0 Hosted Runtime: no Lock ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception 0 1 2e74 0062a498 2a020 Preemptive 02684548:00000000 006241e8 1 MTA 5 2 2464 0063afd0 2b220 Preemptive 00000000:00000000 006241e8 0 MTA (Finalizer) |
1 2 3 4 5 6 7 8 |
0:006> ~ 0 Id: 484.2e74 Suspend: 1 Teb: 0021d000 Unfrozen 1 Id: 484.2368 Suspend: 1 Teb: 00220000 Unfrozen 2 Id: 484.1e10 Suspend: 1 Teb: 00223000 Unfrozen 3 Id: 484.1ef0 Suspend: 1 Teb: 00226000 Unfrozen 4 Id: 484.3530 Suspend: 1 Teb: 00229000 Unfrozen 5 Id: 484.2464 Suspend: 1 Teb: 0022c000 Unfrozen . 6 Id: 484.1c6c Suspend: 1 Teb: 0022f000 Unfrozen |
1 |
0:006> ~5f |
Let’s carry on and look for a trash (just hit enter twice and get to the third line read), break in debugger and check:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
0:006> g System 0: 1 of 7 threads are frozen System 0: 1 of 8 threads were frozen System 0: 1 of 8 threads are frozen System 0: 1 of 7 threads were frozen System 0: 1 of 7 threads are frozen System 0: 1 of 8 threads were frozen System 0: 1 of 8 threads are frozen System 0: 1 of 9 threads were frozen System 0: 1 of 9 threads are frozen System 0: 1 of 9 threads were frozen (484.34d0): Break instruction exception - code 80000003 (first chance) eax=00238000 ebx=00000000 ecx=772e9e80 edx=772e9e80 esi=772e9e80 edi=772e9e80 eip=772b48b0 esp=04f4fc80 ebp=04f4fcac iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 ntdll!DbgBreakPoint: 772b48b0 cc int 3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
0:008> !finalizequeue SyncBlocks to be cleaned up: 0 Free-Threaded Interfaces to be released: 0 MTA Interfaces to be released: 0 STA Interfaces to be released: 0 ---------------------------------- generation 0 has 0 finalizable objects (0063a034->0063a034) generation 1 has 7 finalizable objects (0063a018->0063a034) generation 2 has 0 finalizable objects (0063a018->0063a018) Ready for finalization 1 objects (0063a034->0063a038) Statistics for all finalizable objects (including all objects ready for finalization): MT Count TotalSize Class Name 02484dc0 1 12 Foo 7253c188 1 20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle 7253c138 1 20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle 7252dde8 1 20 Microsoft.Win32.SafeHandles.SafePEFileHandle 72531888 2 40 Microsoft.Win32.SafeHandles.SafeFileHandle 725312e0 1 44 System.Threading.ReaderWriterLock 7252f734 1 52 System.Threading.Thread Total 8 objects |
So our object is still there, let’s find its roots:
1 2 |
0:008> !gcroot -all 02684170 Found 0 roots. |
1 2 |
0:008> !gcroot -nostacks 02684170 Found 0 unique roots (run '!GCRoot -all' to see all roots). |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
0:008> !finalizequeue -allReady SyncBlocks to be cleaned up: 0 Free-Threaded Interfaces to be released: 0 MTA Interfaces to be released: 0 STA Interfaces to be released: 0 ---------------------------------- generation 0 has 0 finalizable objects (0063a034->0063a034) generation 1 has 7 finalizable objects (0063a018->0063a034) generation 2 has 0 finalizable objects (0063a018->0063a018) Finalizable but not rooted: Ready for finalization 1 objects (0063a034->0063a038) Statistics for all finalizable objects that are no longer rooted: MT Count TotalSize Class Name 02484dc0 1 12 Foo Total 1 objects |
So it is not held by anything and ready to be cleaned up — it is now in f-reachable queue:
1 2 3 |
0:008> s -d 0 L?0xFFFFFFFF 02684170 0055f15c 02684170 00000000 026823b4 0055f174 pAh......#h.t.U. 0063a034 02684170 00000000 00000000 00000000 pAh............. |
The address 0063a034
was in a finalization queue (see previous outputs). Since we froze the finalizer thread, the object is not cleaned up.
61. What is object pinning?
When you pass an object to some external (non-managed) code it is important to not move the object as the external code may have a pointer to it. Since GC can move the object when compacting the heap, we need to ask GC to not move it. This is called pinning.
To pin an object you can use either fixed keyword or GC.Alloc.
62. Does P/Invoke pins objects?
Yes, but only for the time of the method execution. You need to pin them manually if you pass them to some asynchronous code (which will use the object after P/Invoke call returns).
63. What is a write barrier?
This is a general term used in garbage collection, not something internal to .NET. Some GC algorithms need to be aware of any write you perform, for instance to update the card tables or reference counters. So when you perform assignment, you call write barrier doing some bookkeeping. See this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
using System; namespace Program { public class Program { public static void Main(string[] args) { var foo = new Foo(); GC.Collect(); Console.WriteLine(GC.GetGeneration(foo)); foo.Bar = new Bar(); Console.ReadLine(); } } } class Foo { public Bar Bar; } class Bar { } |
When you run it you should see that after the collection foo
object is in generation 1. Let’s decompile it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
// Metadata version: v4.0.30319 .assembly extern mscorlib { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. .ver 4:0:0:0 } .assembly '3aecb992-09ec-4656-9a7f-15f021299e85' { .hash algorithm 0x00008004 .ver 0:0:0:0 } .module '3aecb992-09ec-4656-9a7f-15f021299e85.dll' // MVID: {62F0FDE5-D531-4A79-97F4-C94E38ADF85C} .imagebase 0x10000000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 // WINDOWS_CUI .corflags 0x00000001 // ILONLY // Image base: 0x00F00000 // =============== CLASS MEMBERS DECLARATION =================== .class private auto ansi beforefieldinit Foo extends [mscorlib]System.Object { .field public class Bar Bar .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Foo::.ctor } // end of class Foo .class private auto ansi beforefieldinit Bar extends [mscorlib]System.Object { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Bar::.ctor } // end of class Bar .class public auto ansi beforefieldinit Program.Program extends [mscorlib]System.Object { .method public hidebysig static void Main(string[] args) cil managed { // .maxstack 2 .locals init ([0] class Foo foo) .language '{3F5162F8-07C6-11D3-9053-00C04FA302A1}', '{994B45C4-E6E9-11D2-903F-00C04FA302A1}', '{5A869D0B-6611-11D3-BD2A-0000F80849BD}' .line 8,8 : 9,10 '' IL_0000: nop .line 9,9 : 13,33 '' IL_0001: newobj instance void Foo::.ctor() IL_0006: stloc.0 .line 10,10 : 13,26 '' IL_0007: call void [mscorlib]System.GC::Collect() IL_000c: nop .line 12,12 : 13,54 '' IL_000d: ldloc.0 IL_000e: call int32 [mscorlib]System.GC::GetGeneration(object) IL_0013: call void [mscorlib]System.Console::WriteLine(int32) IL_0018: nop .line 13,13 : 13,33 '' IL_0019: ldloc.0 IL_001a: newobj instance void Bar::.ctor() IL_001f: stfld class Bar Foo::Bar .line 14,14 : 13,32 '' IL_0024: call string [mscorlib]System.Console::ReadLine() IL_0029: pop .line 15,15 : 9,10 '' IL_002a: ret } // end of method Program::Main .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { // .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: nop IL_0007: ret } // end of method Program::.ctor } // end of class Program.Program // ============================================================= // |
There is nothing special in line foo.Bar = new Bar();
. Let’s see it with WinDBG:
1 2 3 4 5 6 7 |
0:005> !name2ee ConsoleApp3!Program.Main Module: 005d4024 Assembly: ConsoleApp3.exe Token: 06000003 MethodDesc: 005d4d34 Name: Program.Program.Main(System.String[]) JITTED Code Address: 007a0448 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
0:005> !U 007a0448 Normal JIT generated code Program.Program.Main(System.String[]) Begin 007a0448, size 93 *** WARNING: Unable to verify checksum for C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\bin\Debug\ConsoleApp3.exe C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\Program.cs @ 8: >>> 007a0448 55 push ebp 007a0449 8bec mov ebp,esp 007a044b 83ec18 sub esp,18h 007a044e 33c0 xor eax,eax 007a0450 8945f0 mov dword ptr [ebp-10h],eax 007a0453 8945f8 mov dword ptr [ebp-8],eax 007a0456 8945ec mov dword ptr [ebp-14h],eax 007a0459 8945e8 mov dword ptr [ebp-18h],eax 007a045c 894dfc mov dword ptr [ebp-4],ecx 007a045f 833dd0425d0000 cmp dword ptr ds:[5D42D0h],0 007a0466 7405 je 007a046d 007a0468 e8c33e0d73 call clr!JIT_DbgIsJustMyCode (73874330) 007a046d 33d2 xor edx,edx 007a046f 8955f4 mov dword ptr [ebp-0Ch],edx 007a0472 90 nop C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\Program.cs @ 9: 007a0473 b9bc4d5d00 mov ecx,5D4DBCh (MT: Foo) 007a0478 e84b2ce2ff call 005c30c8 (JitHelp: CORINFO_HELP_NEWSFAST) 007a047d 8945f0 mov dword ptr [ebp-10h],eax 007a0480 8b4df0 mov ecx,dword ptr [ebp-10h] 007a0483 ff15dc4d5d00 call dword ptr ds:[5D4DDCh] (Foo..ctor(), mdToken: 06000001) 007a0489 8b45f0 mov eax,dword ptr [ebp-10h] 007a048c 8945f4 mov dword ptr [ebp-0Ch],eax C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\Program.cs @ 10: 007a048f e850f84372 call mscorlib_ni+0xb5fce4 (72bdfce4) (System.GC.Collect(), mdToken: 06000e9b) 007a0494 90 nop C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\Program.cs @ 12: 007a0495 8b4df4 mov ecx,dword ptr [ebp-0Ch] 007a0498 e863abe072 call clr!GCInterface::GetGeneration (735ab000) 007a049d 8945f8 mov dword ptr [ebp-8],eax 007a04a0 8b4df8 mov ecx,dword ptr [ebp-8] 007a04a3 e8547f4372 call mscorlib_ni+0xb583fc (72bd83fc) (System.Console.WriteLine(Int32), mdToken: 06000b6e) 007a04a8 90 nop C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\Program.cs @ 13: 007a04a9 b9184e5d00 mov ecx,5D4E18h (MT: Bar) 007a04ae e8152ce2ff call 005c30c8 (JitHelp: CORINFO_HELP_NEWSFAST) 007a04b3 8945ec mov dword ptr [ebp-14h],eax 007a04b6 8b4dec mov ecx,dword ptr [ebp-14h] 007a04b9 ff15384e5d00 call dword ptr ds:[5D4E38h] (Bar..ctor(), mdToken: 06000002) 007a04bf 8b55f4 mov edx,dword ptr [ebp-0Ch] 007a04c2 8b45ec mov eax,dword ptr [ebp-14h] 007a04c5 8d5204 lea edx,[edx+4] 007a04c8 e8b3e2d872 call clr!JIT_WriteBarrierEAX (7352e780) C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\Program.cs @ 14: 007a04cd e82a7e4372 call mscorlib_ni+0xb582fc (72bd82fc) (System.Console.ReadLine(), mdToken: 06000b65) 007a04d2 8945e8 mov dword ptr [ebp-18h],eax 007a04d5 90 nop C:\Users\adafurma\source\repos\ConsoleApp3\ConsoleApp3\Program.cs @ 15: 007a04d6 90 nop 007a04d7 8be5 mov esp,ebp 007a04d9 5d pop ebp 007a04da c3 ret |
You can see the write barrier here:
1 |
007a04c8 e8b3e2d872 call clr!JIT_WriteBarrierEAX (7352e780) |
See the next question to understand how it can be used in .NET.
64. What is a card table? Brick table?
GC is capable of cleaning generation 0 only. What happens if the only reference to some object X (being in generation 0) is in a field of object Y which is in generation 1? How does GC know that it cannot remove X from the memory? This is thanks to the card table.
Object X must have been assigned to field in Y somehow. When this assignment was performed, .NET executed the write barrier (see previous question) which updated the card table.
Card table is a list of bits where each bit maintains 128 (in x86) or 256 (in x64) bytes of memory. When a bit is set, GC knows to include those bytes of memory as roots when performing the marking phase. Since card tables can be pretty big, they are grouped in card bundles (sometimes called card decks).
Brick table is similar structure covering 2kB and 4kB of memory (in x86 and x64 respectively). It is used by the GC to locate objects (technically plugs).
Bonus — where do I learn more?
Pro .NET Memory Management by Konrad Kokosa is a great book describing .NET GC into a great depth. If you want to learn more about memory reclamation internals then it is a must read.