Task for the fine code farmer Thoughts on memory leakage in run

1: Background

1. Tell a story

During this period of time, the project was delayed and the overtime was severe. The blog stopped a little, but it still had to continue the technical output! The garden has been very lively recently. The boss of exquisite yard farmer shared three articles:

The core code is as follows:

    class Program
    {
        static void Main(string[] args)
        {
            Test();
            Console.ReadLine();
        }

        static void Test()
        {
            var myClass = new MyClass();

            myClass.Foo();
        }
    }

    public class MyClass
    {
        private int _id = 10;

        public Task Foo()
        {
            return Task.Run(() =>
            {
                Console.WriteLine($"Task.Run is executing with ID {_id}");
            });
        }
    }

After the Test() method is executed, myClass should have been destroyed. It is found that the Foo() method references_ id, which causes the GC to give up recycling of myClass, resulting in memory leakage.

If my understanding is wrong, please help correct it. It's very interesting. The comment area is also very lively. On the whole, I find that many friends still have a vague understanding of closures, memory leaks, GC and other concepts. As a technology blogger, I have to rub some heat 😄😄😄, In this article, I'm going to elaborate my cognition from these three aspects, and then we'll look back at the article of the exquisite boss.

2: Cognition of closure

1. What is closure

I first came into contact with the concept of closure in js. As for the concept of closure, people who know it naturally understand it. Those who don't understand it have to scratch their heads. I'm going to start with the code instead of the concept. I'll help you sort it out and look at the core code first:

    public class MyClass
    {
        private int _id = 10;

        public Task Foo()
        {
            return Task.Run(() =>
            {
                Console.WriteLine($"Task.Run is executing with ID {_id}");
            });
        }
    }

I find that many people are confused in task In the Run delegate_ id, because it takes the id in MyClass_ id, it seems to realize time and space travel. In fact, it's very simple to think about it carefully, ha, task MyClass. Is required in the Run delegate_ id, you must pass this pointer of MyClass itself as a parameter to the delegate. Since you have this, what value can't be taken out??? Unfortunately, Run does not accept any object parameters, so the pseudo code is as follows:

        public Task Foo()
        {
            return Task.Run((obj) =>
            {
                var self = obj as MyClass;

                Console.WriteLine($"Task.Run is executing with ID {self._id}");
            },this);
        }

I believe everyone can see the above code clearly. Some friends have nothing to say. Why are you right??? It doesn't matter. I'll let you see from windbg...

2. Use windbg to verify

It's actually very simple to verify. Use windbg in this statement console WriteLine($"Task.Run is executing with ID {_id}"); Put a breakpoint on it, and after hitting it, just look at the parameter list of this method.

This code is on line 35 of my file. Use the command! bpmd Program. CS: 35set breakpoints.

0:000> !bpmd Program.cs:35
0:000> g
JITTED ConsoleApp4!ConsoleApp4.MyClass.<Foo>b__1_0()
Setting breakpoint: bp 00007FF83B2C4480 [ConsoleApp4.MyClass.<Foo>b__1_0()]
Breakpoint 0 hit
00007ff8`3b2c4480 55              push    rbp

< foo > b above__ 1_ The 0 () method is the so-called delegate method. You can use it next! clrstack -p view the parameter list of this method.

0:009> !clrstack -p
OS Thread Id: 0x964c (9)
        Child SP               IP Call Site
000000BF6DB7EF58 00007ff83b2c4480 ConsoleApp4.MyClass.b__1_0() [E:\net5\ConsoleApp1\ConsoleApp4\Program.cs @ 34]
    PARAMETERS:
        this (<CLR reg>) = 0x0000025c26f8ac60

You can see that this method has a parameter this. The address is 0x0000025c26f8ac60. You can use it next! do 0x0000025c26f8ac60 try printing and see what it is?

0:009> !do 0x0000025c26f8ac60
Name:        ConsoleApp4.MyClass
MethodTable: 00007ff83b383548
EEClass:     00007ff83b3926b8
Size:        24(0x18) bytes
File:        E:\net5\ConsoleApp1\ConsoleApp4\bin\Debug\netcoreapp3.1\ConsoleApp4.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ff83b28b1f0  4000001        8         System.Int32  1 instance               10 _id

Observe the above output, ha ha, as expected, 0x0000025c26f8ac60 is consoleapp4 MyClass, do you have a new understanding of closures now???

2: Understanding of memory leakage

1. What is a memory leak

There is a phrase in English called Out of Control. Yes, it is Out of Control. If you want to release it, you can only commit suicide attacks, such as: kill process, turn off the machine.

Well, back to this example, the code is as follows:

        static void Test()
        {
            var myClass = new MyClass();

            myClass.Foo();
        }

After the Test method is executed, the reference address on the stack of myClass will be erased. Interestingly, at this time, the task The delegate method in run has certainly not been thread scheduled. I found that many people couldn't figure it out and thought there was a memory leak. Right 🤣🤣🤣

If you understand what I said in the last section, it's easy to understand. Hey, I haven't drawn and analyzed for a long time. It's an exception.

It can be clearly seen that after executing MyClass Foo(); After the statement, MyClass on the heap is referenced in two places. When the Test method is executed, the A reference will be erased, but there is also A B reference, so no matter how you GC, MyClass on the heap will not be recycled. If this is also A memory leak

It's still that sentence. There's no basis for empty words. I have to show evidence and talk to windbg.

2. Use windbg to find B reference

To verify the existence of B reference, the idea is very simple. Let the anonymous delegate method not exit, and then go to the managed heap to find out who MyClass is still referenced by. Next, modify the code slightly.

    class Program
    {
        static void Main(string[] args)
        {
            Test();

            Console.WriteLine("All main threads have been executed!");
            Console.ReadLine();  
        }

        static void Test()
        {
            var myClass = new MyClass();

            myClass.Foo();
        }
    }

    public class MyClass
    {
        private int _id = 10;

        public Task Foo()
        {
            return Task.Run(() =>
            {
                Console.WriteLine($"Task.Run is executing with ID {_id}");

                Thread.Sleep(int.MaxValue);   //Deliberately not letting the method exit
            });
        }
    }

Use! Use - mystat - dumptype to view the instance on the heap! gcroot can view its reference chain,

0:000> !dumpheap -stat -type MyClass
Statistics:
              MT    Count    TotalSize Class Name
00007ff839d23548        1           24 ConsoleApp4.MyClass
Total 1 objects
0:000> !DumpHeap /d -mt 00007ff839d23548
         Address               MT     Size
00000284e248ac90 00007ff839d23548       24     
0:000> !gcroot 00000284e248ac90
Thread 4eb0:
    0000009CD68FED60 00007FF839C646A6 ConsoleApp4.MyClass.<Foo>b__1_0() [E:\net5\ConsoleApp1\ConsoleApp4\Program.cs @ 39]
        rbp+10: 0000009cd68feda0
            ->  00000284E248AC90 ConsoleApp4.MyClass

As expected, the reference of MyClass is < foo > B__ 1_ 0 () method, which verifies the existence of B reference.

3: Cognition of GC

In addition to the large object heap, small objects mainly use the old method of the three generation mechanism. There is nothing to say. However, it should be noted that GC will not be recycled at any time. After all, GC in workstation mode has 256M memory size by default on 64 bit machine, which will be allocated to generation 0 + generation 1. It is not small, as shown in the following figure:

In fact, what I want to express is that even if there are two references a and B at present, 99% of the cases will be recycled in the same generation, for example: generation 0.

It's been more than ten minutes now. Can you see if the address of MyClass (00000284e248ac90) has been sent to the first generation? Use! eeheap -gc type out the address segment of the managed heap.

0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00000284E2481030
generation 1 starts at 0x00000284E2481018
generation 2 starts at 0x00000284E2481000

It can be seen that even after more than ten minutes, MyClass(00000284e248ac90) is still on the generation 0 heap.

3: Summary

Well, these three concepts: closure, memory leak and GC are almost finished. I don't know if we can solve your doubts. Finally, thank you for your wonderful blog.

More high quality dry goods: see my GitHub: dotnetfly

Tags: Javascript github readline

Posted by Im Jake on Tue, 03 May 2022 22:10:17 +0300