.NET 内存泄漏的争议
前几天发布了几篇关于要小心使用 Task.Run
的文章,看了博客园的所有评论。发现有不少人在纠结示例中的现象是不是属于内存泄漏,本文分享一下我个人的看法,大家可以保留自己的意见。
在阅读本文前,如果你对 GC 分代算法还不了解,建议先阅读我的上一篇文章:小心使用 Task.Run 终篇解惑。
背景
还是先把前面两篇文章的示例贴出来:
class Program
{
static void Main(string[] args)
{
Test();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
// 程序保活
while (true)
{
Thread.Sleep(100);
}
}
static void Test()
{
var myClass = new MyClass();
myClass.Foo();
// 到这,myClass 实例不再需要了
}
}
public class MyClass
{
private int _id;
public Task Foo()
{
return Task.Run(() =>
{
Console.WriteLine($"Task.Run is executing with ID {_id}");
Thread.Sleep(100); // 模拟耗时操作
});
}
~MyClass()
{
Console.WriteLine("MyClass instance has been colleted.");
}
}
或许是我表述的问题,更或许是我把原本是一篇的文章折成了两篇发布,造成了一些误解。所以在这里我对后两篇的内容再解释一下。
有的童鞋可能误解了这个示例要演示的是什么。我演示的是,myClass
实例对象不再需要使用时,GC 在其成员被捕获的情况下能否把它回收掉。我特意用 Test()
方法包装了一下 MyClass
实例的创建和调用,当 Test()
方法执行结束时,myClass
对象则变成了不再需要使用的对象。为了保证 GC 强制回收时,myClass
对象的成员是被引用(捕捉)着的,我在 Task.Run
的匿名方法中使用了 Thread.Sleep(100)
。
如果在 while
循环内不断执行强制回收或者在强制回收前等待足够长的时间,保证 Task.Run
执行完,myClass
对象当然会被回收,因为此时它不存在被不可回收的资源捕获的成员,这点我本以为不需要示例演示大家应该也是这么认为的。如果你了解 GC 的分代算法,你关注的会是,当 myClass
对象变成不再需要使用的资源时,它能否被 GC 在 Gen 0 阶段被回收;而不是关注它最终会不会被回收。
在实际 GC 自动回收的情况下(非手动强制回收),如果第一次扫描到 myClass
发现它被其它对象引用,则会把它标记为 Gen 1,再扫描到它时就会把它标记为 Gen 2。每错过一次回收时机,在内存驻留的时间就越长,它就越难被回收。GC 进行 Root 搜索时,它是否会去搜索某个对象是有统计学基础的。
好了,现在切入正题。问:示例中的现象在 .NET 中是否属于内存泄漏?
正题
我们知道,.NET 应用程序主要使用三种类型的内存:堆栈、托管堆和非托管堆。绝大多数我们在 .NET 中使用的引用类型都是分配在托管堆上的,例如本文示例中的 myClass
对象。发生在托管堆上的内存泄漏我们可以把它称为托管内存泄漏。
关于 .NET 托管堆上的内存泄漏,我直接引用其它两篇文章的现象描述吧(文章地址在文末)。
第一篇[1]描述的一个内存泄漏的现象是:
If the reference is stored in a field reference in the class where the method is declared, it’s not so smart, since it’s impossible to determine whether it will be reused later on, or at least very very hard. If this data structure becomes unnecessary, you should clear the reference you’re holding to it so that GC will pick it up later.
也说是在方法中捕获类成员的现象,和本文示例相符。如果对象不再需要使用了,你应该清除掉它“身上”的引用,以让 GC 在下一次搜索时把它回收掉。
第二篇[2](我的《为什么要小心使用Task.Run》文章就参考了这篇文章)是这样描述的:
There are 2 related core causes for memory leaks. The first core cause is when you have objects that are still referenced but are effectually unused. Since they are referenced, the GC won’t collect them and they will remain forever, taking up memory. This can happen, for example, when you register to events but never unregister. Let’s call this a managed memory leak.
和第一篇的意思差不多,也是说当对象实际上不再使用了,但因为它还被引用,GC 则不会回收它们,这种现象作者把它归为导致内存泄漏的一个主要原因。
第二篇[2]文中还有这么一段:
Many share the opinion that managed memory leaks are not memory leaks at all since they are still referenced and theoretically can be de-allocated. It’s a matter of definition and my point of view is that they are indeed memory leaks. They hold memory that can’t be allocated for another instance and will eventually cause an out-of-memory exception.
翻译如下:
很多人都认为,托管内存泄漏根本不是内存泄漏,因为它们仍然被引用,理论上可以去分配。这是一个定义的问题,我的观点是,它们确实是内存泄漏。它们持有的内存无法分配给另一个实例,最终可能会造成内存溢出异常。
简单概括就是很多人认为托管内存泄漏不属于内存泄漏,这具有争议性,作者认为这是定义问题。
维基上的定义是这样的:
内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。
这个定义并没有对内存泄漏在时间上设限,请注意“由于疏忽或错误”和“不再使用”这两个重要关键词。”未能释放“是永久还是长时间?并没有明确定义。如果你要说我是在咬文嚼字,嗯,随你吧。
一个 .NET 应用,托管堆中处于 Gen 2 的未回收资源会有很多,其中基本上都是需要使用的。
而不需要再使用的资源长时间驻留在内存的托管堆上,它逃过了 Gen 0,逃过了 Gen 1,甚至逃过了 N 次 Gen 2,这是否属于内存泄漏,存在很大的争议。我认为这也是定义问题,站在操作系统的视角和托管堆“分代”的视角自然会得到不一样的理解。
就像最近头条上很多人对 1=0.999...(无限循环)
这个数学问题的争议一样,有的人认为这个等式是对的,有的人认为它是错的。
最后,我选择以托管堆的视角来理解,我的观点和第二篇引用文的作者一样,因编码不当导致不再需要使用的资源长时间驻留内存(延迟回收),属于内存泄漏。延迟回收也属于代码缺陷,虽然,很多场景大可不必在意这点性能。大家随意,哪种更能帮助你理解你便选择哪种。
文中链接:
[1]. http://dwz.date/d48W
[2]. http://dwz.date/d48U
附前两篇文章链接:
小心使用 Task.Run 续篇
小心使用 Task.Run 终篇解惑
.NET 内存泄漏的争议的更多相关文章
- 使用Memory Analyzer tool(MAT)分析内存泄漏(一)
转载自:http://www.blogjava.net/rosen/archive/2010/05/21/321575.html 前言 在平时工作过程中,有时会遇到OutOfMemoryError,我 ...
- (转)从内存管 理、内存泄漏、内存回收探讨C++内存管理
http://www.cr173.com/html/18898_all.html 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟 ...
- 调不尽的内存泄漏,用不完的Valgrind
调不尽的内存泄漏,用不完的Valgrind Valgrind 安装 1. 到www.valgrind.org下载最新版valgrind-X.X.X.tar.bz2 2. 解压安装包:tar –jxvf ...
- ThreadLocal以及内存泄漏
ThreadLocal是什么 ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度.但是如果滥用Thr ...
- 【知识必备】内存泄漏全解析,从此拒绝ANR,让OOM远离你的身边,跟内存泄漏say byebye
一.写在前面 对于C++来说,内存泄漏就是new出来的对象没有delete,俗称野指针:而对于java来说,就是new出来的Object放在Heap上无法被GC回收:而这里就把我之前的一篇内存泄漏的总 ...
- Android性能优化之利用Rxlifecycle解决RxJava内存泄漏
前言: 其实RxJava引起的内存泄漏是我无意中发现了,本来是想了解Retrofit与RxJava相结合中是如何通过适配器模式解决的,结果却发现了RxJava是会引起内存泄漏的,所有想着查找一下资料学 ...
- Android性能优化之利用LeakCanary检测内存泄漏及解决办法
前言: 最近公司C轮融资成功了,移动团队准备扩大一下,需要招聘Android开发工程师,陆陆续续面试了几位Android应聘者,面试过程中聊到性能优化中如何避免内存泄漏问题时,很少有人全面的回答上来. ...
- C++的内存泄漏检测
C++大量的手动分配.回收内存是存在风险的,也许一个函数中一小块内存泄漏被重复放大之后,最后掏空内存. 这里介绍一种在debug模式下测试内存泄漏的方法. 首先在文件的开头以确定的顺序写下这段代码: ...
- 使用 Android Studio 检测内存泄漏与解决内存泄漏问题
本文在腾讯技术推文上 修改 发布. http://wetest.qq.com/lab/view/63.html?from=ads_test2_qqtips&sessionUserType=BF ...
随机推荐
- 【Flutter 1-7】Flutter教程Dart语言——变量
作者 | 弗拉德 来源 | 弗拉德(公众号:fulade_me) 2011年10月10日的GOTO大会上,谷歌的两位工程师发布了"Dart":Dart是一种全新的编程语言,旨在帮助 ...
- c语言: 错误:只允许在 C99 模式下使用‘for’循环初始化声明 用gcc编译出现
在gcc编译中如果使用 for(int i=0;i<n;++i){}会提示错误 错误:只允许在 C99 模式下使用'for'循环初始化声明 用gcc编译出现 就是说你的你的c编译器不是c99标准 ...
- linux之DNS服务
1.DNS (Domain Name Service 域名解析) DNS是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网而不需要记忆能够直接被机器识别的IP. BI ...
- MYSQL学习(三) --索引详解
创建高性能索引 (一)索引简介 索引的定义 索引,在数据结构的查找那部分知识中有专门的定义.就是把关键字和它对应的记录关联起来的过程.索引由若干个索引项组成.每个索引项至少包含两部分内容.关键字和关键 ...
- 测试_appium测试工具
一.Appium介绍 Appium是一个开源的自动化测试工具,其支持iOS和安卓平台上的原生的,基于移动浏览器的,混合的应用. 1.Appium 理念 Appium是基于以下的四个理念设计来满足移动平 ...
- Vue-router插件使用
单页面原理 Vue是单页面开发,即页面不刷新. 页面不刷新,而又要根据用户选择完成内容的更新该怎么做?Vue中采用锚点来完成. 如访问http://127.0.0.1#/index就是主页,而访问ht ...
- Java 添加、读取、删除Excel中的图表趋势线
本文以Java示例介绍如何在Excel中添加趋势线,以及读取趋势线公式.通过文中的方法可支持添加6种不同类型的趋势线,包括Linear.Exponential.Logarithmic.Moving A ...
- python+selenium通过加载用户配置实现免登陆
1查看profile路径 在Chrome地址栏访问chrome://version,可以查看个人资料存储位置: 2 python代码如下: from selenium import webdriver ...
- linux常见目录
- 【P2634】聪聪可可——点分治
(题面来自Luogu) 题目描述 聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃.两个人都想玩儿电脑(可是他们家只有一台电脑)--遇到这种问题,一般情况下石头 ...