继上一篇文章以后,这篇文章主要解答如下两个疑惑:程序员
_id
改成引用类型(如 StringBuilder),那二者指向的就是同一个对象值,那是否是意味着即使使用本地变量也仍是没法避免内存泄漏的问题?myClass
实例存在被捕获的成员,则认为它不该该被回收。那当 Task.Run
执行完后,GC 再次搜索时不就能够回收 myClass
对象吗?只是晚了一些时间回收而已。为了方便理解,我再把昨天的关键代码贴出来:算法
public class MyClass { private int _id; public Task Foo() { var localId = _id; return Task.Run(() => { Console.WriteLine($"Task.Run is executing with ID {localId}"); Thread.Sleep(100); // 模拟耗时操做 }); } }
先来看第一个疑惑。经实测,把 _id
改成 StringBuilder
类型运行结果是和 int
同样的,说明和值类型或引用类型无关。个人理解是这样的:性能
咱们知道,引用类型的变量在声明的时候就会在栈中分配一个空间,用来存放地址引用,而给它的赋值则存储在托管堆中。虽然本地变量 localId
和类的成员 _id
的地址都指向的是托管堆中同一块空间,但他们在栈中的地址却分属不一样的做用域。所谓被捕获就是被做用域捕获,当一个做用域结束时,该做用域内的成员的地址空间都会随着一块儿被释放。至于地址指向的托管堆中的字符串值,则不是做用域关心的事情。当该字符串值所在的空间没有地址指向它时,就会被 GC 回收。 有点抽象,但应该还好理解。优化
再来看第二个疑惑。在此以前,咱们先来了解一下 GC 的分代算法。ui
当 CLR 试图搜索再也不使用的对象的时,它须要遍历托管堆上的对象。随着程序的持续运行,托管堆可能愈来愈大,若是要对整个托管堆进行垃圾回收,势必会严重影响性能。因此,为了优化这个过程,CLR 中使用了分代算法。编码
简单来讲,分代算法就是把内存中的资源划分为三代:Gen 0、Gen 一、Gen 2,它们被 GC 遍历的频率依次从高到低。全部新建立的对象属于 Gen 0,GC 扫描它的频率最高。进行一次扫描后,处于 Gen 0 的不可回收对象就会被标记为 Gen 1。相似的,GC 扫描 Gen 1 时,若是 Gen 1 的对象依然不可回收,就会标记为 Gen 2。有点像马太效应,资源停留在内存时间越长,就越不容易被回收。code
Gen 2 的回收被称为 Full GC。而 Full GC 只有在知足必定的条件才会执行,具体请阅读这篇官方文档:对象
https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/notifications#full-garbage-collection
也就是说,进入 Gen 2 的资源,若条件没有达到,就会一直不被回收。blog
理解了分代算法和 Full GC,第二个疑惑就迎刃而解了。第二个疑惑关键在三个时间点上:内存
myClass
对象做用域结束的时间点Task.Run
匿名方法执行完成的时间点若是程序执行的时间点顺序是:一、三、2,那么不会有内存漏泄的问题,这点很容易理解。
因为实际状况 Task.Run
通常为耗时操做(非耗时任务通常没有必要使用 Task.Run
),因此时间点的顺序极有多是:一、二、3。若是是此执行的顺序,那么 GC 在回收时就会由于 myClass
对象存在成员被引用而把它标记为 Gen 1。若是 Task.Run 耗时足够长, myClass
就可能会进入 Gen 2,进而可能很难被回收,甚至可能永远不被回收。
其实大部分场景,咱们也没必要过于当心,即便在 Task.Run
匿名方法捕获了类的成员使该类的实例进入了 Gen 2,Gen 2 中留存的再也不使用的资源也是有限的。根据官方文档对 Full GC 的介绍(地址在前文),当 Gen 2 积累到必定的量时便知足了执行回收的条件,在 GC 下一次回收时便会回收 Gen 2 中再也不使用的资源。固然,做为一个优秀的程序员,咱们仍是得养成好的编码习惯,不要在 Task.Run
中的匿名方法捕获类的成员。
最后,郑重声明,最近三篇关于当心使用 Task.Run 的文章皆属我我的理解,知识水平有限,不免存在遗漏和错误。如有发现,请你们不吝指正。
PS:本人博客园文章通常晚于公众号一天发布,望你们见谅。关因而否属于内存泄漏问题,我在今天的文章中有讨论:《.NET内存泄漏的争议》