对 精致码农大佬 说的 Task.Run 会存在 内存泄漏 的思考

存储 存储软件
我最早接触闭包的概念是在 js 中,关于闭包的概念,懂得人自然懂,不懂的人得要挠会头,我准备不从概念而从代码入手,帮你梳理下。

 [[356479]]

一:背景

1. 讲故事

这段时间项目延期,加班比较厉害,博客就稍微停了停,不过还是得持续的技术输出呀!园子里最近挺热闹的,精致码农大佬分享了三篇文章:

  • 为什么要小心使用 Task.Run [https://www.cnblogs.com/willick/p/14078259.html]
  • 小心使用 Task.Run 续篇 [https://www.cnblogs.com/willick/p/14100973.html]
  • 小心使用 Task.Run 终篇解惑 [https://mp.weixin.qq.com/s/IMPgSsxTW0LGArfPP7rQXw]

核心代码如下:

  1. class Program 
  2.    { 
  3.        static void Main(string[] args) 
  4.        { 
  5.            Test(); 
  6.            Console.ReadLine(); 
  7.        } 
  8.  
  9.        static void Test() 
  10.        { 
  11.            var myClass = new MyClass(); 
  12.  
  13.            myClass.Foo(); 
  14.        } 
  15.    } 
  16.  
  17.    public class MyClass 
  18.    { 
  19.        private int _id = 10; 
  20.  
  21.        public Task Foo() 
  22.        { 
  23.            return Task.Run(() => 
  24.            { 
  25.                Console.WriteLine($"Task.Run is executing with ID {_id}"); 
  26.            }); 
  27.        } 
  28.    } 

大意是:Test() 方法执行完之后, myClass 本该销毁,结果发现 Foo() 方法引用了 _id ,导致 GC 放弃了对 myClass 的回收,从而导致内存泄漏。

如果我的理解有误,请大家帮忙指正,挺有意思,评论区也是热闹非凡,总体看下来发现还是有很多朋友对 闭包, 内存泄漏,GC 等概念的认知比较模糊,同样作为技术博主,得要蹭点热度??????,这篇我准备从这三个方面阐述下我的认知,然后大家再回头看一下 精致 大佬的文章。

二:对闭包的认知

1. 什么是闭包

我最早接触闭包的概念是在 js 中,关于闭包的概念,懂得人自然懂,不懂的人得要挠会头,我准备不从概念而从代码入手,帮你梳理下,先看核心代码:

  1. public class MyClass 
  2.     { 
  3.         private int _id = 10; 
  4.  
  5.         public Task Foo() 
  6.         { 
  7.             return Task.Run(() => 
  8.             { 
  9.                 Console.WriteLine($"Task.Run is executing with ID {_id}"); 
  10.             }); 
  11.         } 
  12.     } 

我发现很多人迷惑就迷惑在 Task.Run 委托中的 _id,因为它拿的是 MyClass 中的 _id,貌似实现了时空穿越,其实仔细想想很简单哈, Task.Run 委托中要拿 MyClass._id,就必须把 MyClass 自身的 this 指针作为参数 传递给委托,既然有了这个this,啥值还拿不出来哈???遗憾的是 Run 不接受任何 object 参数,所以伪代码如下:

  1. public Task Foo() 
  2.         { 
  3.             return Task.Run((obj) => 
  4.             { 
  5.                 var self = obj as MyClass; 
  6.  
  7.                 Console.WriteLine($"Task.Run is executing with ID {self._id}"); 
  8.             },this); 
  9.         } 

上面的代码我相信大家都看的很清楚了,有些朋友要说了,空口无凭,凭什么你说的就是对的???没关系,我从 windbg 让你眼见为实就好啦。。。

2. 使用 windbg 验证

想验证其实很简单,用 windbg 在这条语句 Console.WriteLine($"Task.Run is executing with ID {_id}"); 上放一个断点,命中之后看一下这个方法的参数列表就好了。

这句代码在我文件的第 35 行,使用命令 !bpmd Program.cs:35 设置断点。

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

上面的 b__1_0() 方法就是所谓的委托方法,接下来可以用 !clrstack -p 查看这个方法的参数列表。

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

可以看到,这个方法有一个参数 this, 地址是: 0x0000025c26f8ac60,接下来可以用 !do 0x0000025c26f8ac60 试着打印一下,看看到底是什么?

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

观察上面输出,哈哈,果然不出所料,0x0000025c26f8ac60 就是 ConsoleApp4.MyClass,现在对闭包是不是已经有了新的认识啦???

二:对内存泄漏的认识

1. 何为内存泄漏

英文中有一个词组叫做 Out of Control,对,就是失去控制了,要想释放只能 自杀式袭击 了, 比如说:kill 进程,关机器。

好了,再回到这个例子上来,代码如下:

  1. static void Test() 
  2.         { 
  3.             var myClass = new MyClass(); 
  4.  
  5.             myClass.Foo(); 
  6.         } 

当 Test 方法执行完成之后,myClass 的栈上引用地址肯定会被抹掉的, 有意思的是此时 Task.Run 中的委托方法肯定还没有得到线程调度,我又发现很多人在这一块想不通了,以为 内存泄漏 了。对吧 ??????

如果你明白了上一节我所说的,那就很好理解啦,哎,很长时间没有画图分析了,破例了。

可以很清晰的看出,当执行完 myClass.Foo(); 语句后,此时有两个地方引用了 堆上的 MyClass,当 Test 方法执行完后, A 引用 会被抹掉,但此时 还有 B 引用 存在,所以这时你不管怎么 GC,堆上的 MyClass 肯定不会被回收,如果说这种情况也算 内存泄漏 的话...

还是那句话,空口无凭,我得拿出证据来,上 windbg 说话。

2. 使用 windbg 查找 B 引用

要想验证 B 引用的存在,思路很简单,让匿名委托方法得不到退出,然后到 托管堆 找一下 MyClass 到底还在被谁引用 即可,接下来稍微修改一下代码。

  1. class Program 
  2.    { 
  3.        static void Main(string[] args) 
  4.        { 
  5.            Test(); 
  6.  
  7.            Console.WriteLine("主线程全部执行完毕!"); 
  8.            Console.ReadLine();   
  9.        } 
  10.  
  11.        static void Test() 
  12.        { 
  13.            var myClass = new MyClass(); 
  14.  
  15.            myClass.Foo(); 
  16.        } 
  17.    } 
  18.  
  19.    public class MyClass 
  20.    { 
  21.        private int _id = 10; 
  22.  
  23.        public Task Foo() 
  24.        { 
  25.            return Task.Run(() => 
  26.            { 
  27.                Console.WriteLine($"Task.Run is executing with ID {_id}"); 
  28.  
  29.                Thread.Sleep(int.MaxValue);   //故意不让方法退出 
  30.            }); 
  31.        } 
  32.    } 

用 !dumpheap -stat -type MyClass 查看堆上的 MyClass 实例,然后用 !gcroot 查看它的引用链即可,

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

果然不出所料,MyClass 的引用正在 b__1_0() 方法中,这也就验证了 B 引用 的存在。

三:对GC的认知

除了大对象堆,小对象主要还是采用 三代机制 的老方法,没啥好说的,不过有一点要注意了,GC 也不会动不动就出来回收的,毕竟工作站模式的GC 在 64 bit 机器上默认有 256M 的内存大小,这 256 M 会分配给 0代 + 1代,说小也不小,如下图:

其实我想表达的意思是,即使当前有 A,B 两个引用,实际上 99 % 的情况下都会在同一代中被回收,比如说:第 0 代。

现在都过了十多分钟了,可以看下 MyClass 的地址 (00000284e248ac90) 当前有没有被送到 第 1 代?用 !eeheap -gc 把托管堆的 地址段 打出来。

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

可以看到,即使过了十多分钟,当前 MyClass(00000284e248ac90) 还是在 0 代堆上。

 本文转载自微信公众号「 一线码农聊技术  」,可以通过以下二维码关注。转载本文请联系 一线码农聊技术  公众号。

 

责任编辑:武晓燕 来源: 一线码农聊技术
相关推荐

2024-03-06 13:23:56

Task.RunC#异步陷阱

2024-09-29 08:57:25

2021-05-06 07:27:57

面试任务调度器

2019-01-30 18:24:14

Java内存泄漏编程语言

2015-03-30 11:18:50

内存管理Android

2013-08-07 10:47:53

DBA成长

2009-06-16 11:17:49

内存泄漏

2012-06-19 15:12:20

Java内存泄露

2024-02-21 08:00:55

WindowsDWM进程

2009-06-10 22:03:40

JavaScript内IE内存泄漏

2024-10-12 09:31:04

WinForms应用程序线程

2015-04-27 09:41:35

前端质量质量保障

2012-09-18 09:40:24

程序员职场职业

2022-12-05 11:29:14

2011-06-16 09:28:02

C++内存泄漏

2013-08-07 10:16:43

Android内存泄漏

2022-05-26 09:51:50

JavaScrip内存泄漏

2009-06-16 11:20:22

内存泄漏

2016-07-05 14:09:02

AndroidJAVA内存

2024-07-02 12:25:01

点赞
收藏

51CTO技术栈公众号