Location>code7788 >text

NET Finalize process in a single diagram

Popularity:276 ℃/2024-10-11 15:54:36

synopsis

"Termination" is generally categorized into deterministic termination (explicit removal) and non-deterministic termination (implicit removal).

  1. Deterministic endpoints are mainly
    Provide developers with a way to clean up explicitly, such as try-finally,using.
  2. 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)  
As you can see, the C# destructor is just a syntactic sugar. il overrides the method. In the underlying assembly, the direct call is Finalize()

workflow

image

seeing is believing

Use windbg to look at the bottom.

  1. When creating a Person object, does it automatically enter the finalize queue?
    image

As you can see, the finalize queue already had a destructor for the Person object when new obj was created.

  1. Does it move to F-Reachable queue after GC starts?
    image

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

  1. Are destructured objects "resurrected"?
    Before GC occurs, two variables are created in the TestFinalize method, person=0x02a724c0 and personNoFinalize=0x02a724cc.
    image
    image
    You can see that the generations are all 0, and they are all found in the managed heap.

After GC occurs
image
image
image
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.

  1. Whether the terminal thread executes and is moved out of the F-Reachable queue
    image
    image
    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
    image

  2. 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.
    image

  3. If the destructor doesn't finish in time and another GC is triggered, will it be promoted again?
    image
    The answer is yes.

terminal expenses

  1. 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.
  2. 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).
  3. 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;

            }
        }
    }

image

The very obvious disparity needs no explanation.