C#静态集合导致的内存泄漏探究
起因是早上组里发的问题
原话:
函数尽量不要直接返回静态列表 要返回集合结果的
直接从外面传个列表进来就好了 这里就全泄漏了
描述:
有这样一个类
类里有个static List<xx>
有个返回值是IEnumerable<xx>的函数
在函数里面给list添加元素
最后把这个List作为迭代器的返回值返回出去
实际上泄露跟迭代器本身并没有什么关系
一开始ugg给我提供的思路是C++里面的集合增减后 迭代器失效的设定
但实际上C#并没有这样的东西
而且就把静态列表作为返回值传出去本身也挺奇怪的
所以咱就看返回static List<xx>到底导致了什么 最终产生了泄露
测试用类
Normal类作为对照组 验证正常GC后析构函数会被调用
Test类作为实验组一 有一个静态列表存放Normal类和一个静态函数
静态函数里执行add然后return
Test类作为实验组二 有一个静态列表存放自己
相比实验组一直接在构造函数里执行add操作
public class Normal { public Normal() { Console.WriteLine("对照组创建"); } ~Normal() { Console.WriteLine("对照组回收"); } } public class Test { public static List<Normal> TestList = new(); public static List<Normal> TestReturn() { TestList.Add(new Normal()); return TestList; } public Test() { TestReturn(); Console.WriteLine("First创建"); } ~Test() { Console.WriteLine("First回收"); } } public class Test2 { public static List<Test2> TestList = new(); public Test2() { Console.WriteLine("Second创建"); TestList.Add(this); } ~Test2() { Console.WriteLine("Second回收"); } }
失败的测试方法
var test = new Test(); test = null;//null后 正常情况下GC会来回收这个对象 var test1 = new Test(); test1 = null; var test2 = new Test(); test2 = null; var testSecond = new Test2(); testSecond = null; var normal = new Normal(); normal = null; GC.Collect(); GC.WaitForPendingFinalizers(); Console.WriteLine("疯狂GC"); GC.Collect(); GC.Collect(); GC.Collect(); GC.Collect(); GC.Collect(); GC.Collect(); GC.Collect(); GC.Collect(); GC.Collect(); GC.Collect();
我发现这么做无论怎么改
对照组析构函数都不会打印
让我百思不得其解
最后通过多方查证查明了原因(nerd 我的nerd!)
在main声明的类对象在程序结束之前不会调用析构函数
也就是说调用析构函数的时候你已经看不到他能打印的语句了
因为控制台程序已经结束了
要解决这个问题就要在其它方法中声明 然后在main里面调用这个方法
如何测试?
按照上面的思路改写了一版
void DoSomething() { var test = new Test(); test = null;//null后 正常情况下GC会来回收这个对象 var test1 = new Test(); test1 = null; var test2 = new Test(); test2 = null; var testSecond = new Test2(); testSecond = null; var testSecond1 = new Test2(); testSecond1 = null; var testSecond2 = new Test2(); testSecond2 = null; var normal = new Normal(); normal = null; } DoSomething(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Console.ReadKey();
其实测试的思路还是有很多的
例如@一沙鸥 通过维护一个count做标识
例如@剑圣提供的两种思路
一是弱表(不大了解这东西) 二是多线程(根本想不起来)
测试结果
对照组析构函数正常执行
实验组一析构函数正常执行 new的类对象的析构函数不执行
实验组二析构函数不执行
也就是说 在构造函数里 对静态列表有add自身的操作的最后无法回收
结果分析
你还记得GC的标记清除的操作吗
Finalizer(终结器)被GC调用 用于释放托管资源
GC会遍历GC Root对象将其标记为“不可收集”
然后GC转到它们引用的所有对象 把他们也标记为“不可收集”
最后收集剩下的所有内容
那么什么会被视作是GC Root呢?
- 正在运行的线程的实时堆栈
- 静态变量
- 通过interop传递到COM对象的托管对象(内存回收将通过引用计数来完成)
也就是说静态变量和他引用的所有内容都不会被垃圾回收
至于为什么要两次GC
那是因为终结器第一次并不会回收
而是把标记的对象添加进freachable队列
第二次GC时候放入队列的对象才会真正的回收
add自身的时候意味着自己被引用了
所以最终自己没有被回收
对于实验组1来说 add的不是自身而是new出来的Normal
虽然自己这个类被回收了
但是静态列表所引用的所有内容仍然不会被回收
也就是在这里被new的所有Normal对象仍然是没有回收的
表现出来就是四个Normal对象的析构函数只有一个执行了
参考文献
http://www.skcircle.com/?id=1889
https://www.cnblogs.com/Areas/p/2726198.html
https://zhuanlan.zhihu.com/p/141032986
一条评论