synopsis
"Termination" is generally categorized into deterministic termination (explicit removal) and non-deterministic termination (implicit removal).
- Deterministic endpoints are mainly
Provide developers with a way to clean up explicitly, such as try-finally,using. - Non-deterministic end major
Provide an entry point for registration, only knowing that it will be executed, but it is not clear when it will be executed. For example, IDisposable,destructor.
Why do we need an end mechanism?
First of all, let's correct the notion that the termination mechanism is not the same as garbage collection. It just means that when an object is no longer needed, we have to perform some actions along the way. It's more like attaching an event.
So there is a notion on the web that IDisposable is about freeing up memory. This notion is not accurate. It should be more aptly described as a kind of pocket.
If it's a scenario where managed code is used exclusively, and the entire object graph is managed by the GC, it really isn't needed. In a managed environment, the termination mechanism is mainly used to handle resources held by objects that are not managed by the GC and runtime.
For example, HttpClient, if there is no termination mechanism, then when the object is released, the GC does not know that the object holds unmanaged resources (handles), resulting in the underlying socket connection will never be released.
As mentioned earlier, terminators don't have to be related to unmanaged resources. It is essentially a "do something after the object is unreachable".
For example, if you want to collect object creation and deletion, you can write the logging code in the constructor and terminator
Source code for the termination mechanism
source code (computing)
namespace Example_12_1_3
{
internal class Program
{
static void Main(string[] args)
{
TestFinalize();
("GC is start. ");
();
("GC is end. ");
();
(1000);
("GC2 is start. ");
();
("GC2 is end. ");
();
();
}
static void TestFinalize()
{
var person=new Person();
var personNoFinalize = new Person2();
("person/personNoFinalizeDistribution completed");
();
}
}
public class Person
{
~Person()
{
("this is finalize");
(); ;
}
}
public class Person2
{
}
}
IL
// Methods
.method family hidebysig virtual
instance void Finalize () cil managed
{
.override method instance void [mscorlib]::Finalize()
// Method begins at RVA 0x2090
// Header size: 12
// Code size: 30 (0x1e)
.maxstack 1
IL_0000: nop
.try
{
// {
IL_0001: nop
// ("this is finalize");
IL_0002: ldstr "this is finalize"
IL_0007: call void [mscorlib]::WriteLine(string)
// ();
IL_000c: nop
IL_000d: call string [mscorlib]::ReadLine()
IL_0012: pop
// }
IL_0013: IL_001d
} // end .try
finally
{
// (no C# code)
IL_0015: ldarg.0
IL_0016: call instance void [mscorlib]::Finalize()
IL_001b: nop
IL_001c: endfinally
} // end handler
IL_001d: ret
} // end of method Person::Finalize
compile
0199097B nop
0199097C mov ecx,dword ptr ds:[4402430h]
01990982 call () (72CB2FA8h)
01990987 nop
01990988 call () (733BD9C0h)
0199098D mov dword ptr [ebp-40h],eax
01990990 nop
01990991 nop
01990992 mov dword ptr [ebp-20h],offset Example_12_1_3.()+045h (00h)
01990999 mov dword ptr [ebp-1Ch],0FCh
019909A0 push offset Example_12_1_3.()+06Ch (019909BCh)
019909A5 jmp Example_12_1_3.()+057h (019909A7h)
workflow
seeing is believing
Use windbg to look at the bottom.
- When creating a Person object, does it automatically enter the finalize queue?
As you can see, the finalize queue already had a destructor for the Person object when new obj was created.
- Does it move to F-Reachable queue after GC starts?
You can see that the destructor of the 1000 Person created in the code has entered the F-Reachable queue
The sosex !finq/!frq command also outputs the
- Are destructured objects "resurrected"?
Before GC occurs, two variables are created in the TestFinalize method, person=0x02a724c0 and personNoFinalize=0x02a724cc.
You can see that the generations are all 0, and they are all found in the managed heap.
After GC occurs
As you can see, the Person2 object can't be found in the managed heap because it was reclaimed, and the Person object still exists in gcroot because it hasn't executed the destructor yet . Therefore, it is not reclaimed, and the memory generation is raised from 0 to 1.
-
Whether the terminal thread executes and is moved out of the F-Reachable queue
The terminating thread will execute out of order after the GC has brought the managed thread back to normal from a hang and the F-Reachable queue has a value.
and move them out of the queue -
Is the object of the destructor function released in the second GC?
By the time the second GC occurs, the managed heap eventually releases the object, since the object destructor has been executed and no longer has a gcroot. -
If the destructor doesn't finish in time and another GC is triggered, will it be promoted again?
The answer is yes.
terminal expenses
- If a type has a terminator, a slow branch will be used to perform the allocation operation
and the additional overhead introduced by having to enter the finalize queue at allocation time. - The terminator object has to go through at least 2 GCs before it can actually be released
At least twice, possibly more. The terminating thread may not always be able to process all destructors between GCs. At this point the object is upgraded from generation 1 to generation 2, and generation 2 objects trigger GC much less frequently. This results in the object not being released in a timely manner (the destructor functions have been executed, but the object itself waits a long time to be released). - The finalize queue is also repeatedly adjusted when objects are upgraded/downgraded
As with GC generations, there are 3 generations and LOHs. when an object is moved in a GC generation, the address of the object also needs to be moved in the finalization queue to the corresponding generation.
Since the finalize queue is managed by the same array as the f-reachable queue at the bottom, and there is no space left between elements. So unlike the GC generation, which can place objects on sight, finalize inserts them at the end of the corresponding generation and shifts all the objects behind it one position to the right.
seeing is believing
Click to view code
public class BenchmarkTester
{
[Benchmark]
public void ConsumeNonFinalizeClass()
{
for (int i = 0; i < 1000; i++)
{
var obj = new NonFinalizeClass();
= i;
}
}
[Benchmark]
public void ConsumeFinalizeClass()
{
for (int i = 0; i < 1000; i++)
{
var obj = new FinalizeClass();
= i;
}
}
}
The very obvious disparity needs no explanation.