系列文章目录地址:
.NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引
GC作为.NET的重要核心基础,是必须要了解的。本文主要侧重于GC内存管理中的一些关键点,如要要全面深入了解其精髓,最好还是多看看书。
public class User
{
public int Age { get; set; }
public string Name { get; set; }
public string _Name = "123" + "abc";
public List<string> _Names;
}
托管堆中存放引用类型对象,因此GC的内存管理的目标主要都是引用类型对象,本文中涉及的对象如无明确说明都指的是引用类型对象。
对象创建及生命周期
一个对象的生命周期简单概括就是:创建>使用>释放,在.NET中一个对象的生命周期:
那其中重要的一个环节,就是对象的创建,大部分的对象创建都是开始于关键字new。为什么说是大部分呢,因为有个别引用类型是由专门IL指令的,比如string有ldstr指令(参考前面的文章:.NET面试题解析(03)-string与字符串操作),0基数组好像也有一个专门指令。
引用对象都是分配在托管堆上的, 先来看看托管堆的基本结构,如下图,托管堆中的对象是顺序存放的,托管堆维护着一个指针NextObjPtr,它指向下一个对象在堆中的分配位置。
创建一个新对象的主要流程:
以题目2中的代码为例,模拟一个对象的创建过程:
public class User
{
public int Age { get; set; }
public string Name { get; set; }
public string _Name = "123" + "abc";
public List<string> _Names;
}
GC垃圾回收
GC是垃圾回收(Garbage Collect)的缩写,是.NET核心机制的重要部分。她的基本工作原理就是遍历托管堆中的对象,标记哪些被使用对象(那些没人使用的就是所谓的垃圾),然后把可达对象转移到一个连续的地址空间(也叫压缩),其余的所有没用的对象内存被回收掉。
首先,需要再次强调一下托管堆内存的结构,如下图,很明确的表明了,只有GC堆才是GC的管辖区域,关于加载堆在前面文中有提到过(.NET面试题解析(04)-类型、方法与继承)。GC堆里面为了提高内存管理效率等因素,有分成多个部分,其中 两个主要部分:
图3(Figure-3)
什么是垃圾?简单理解就是没有被引用的对象。
先假设所有对象都是垃圾,根据应用程序根指针Root遍历堆上的每一个引用对象,生成可达对象图,对于还在使用的对象(可达对象)进行标记(其实就是在对象同步索引块中开启一个标示位)。
其中Root根指针保存了当前所有需要使用的对象引用,他其实只是一个统称,意思就是这些对象当前还在使用,主要包含:静态对象/静态字段的引用;线程栈引用(局部变量、方法参数、栈帧);任何引用对象的CPU寄存器;根引用对象中引用的对象;GC Handle table;Freachable队列等。
针对所有不可达对象进行清除操作,针对普通对象直接回收内存,而对于实现了终结器的对象(实现了析构函数的对象)需要单独回收处理。清除之后,内存就会变得不连续了,就是步骤3的工作了。
把剩下的对象转移到一个连续的内存,因为这些对象地址变了,还需要把那些Root跟指针的地址修改为移动后的新地址。
垃圾回收的过程示意图如下:
垃圾回收的过程是不是还挺辛苦的,因此建议不要随意手动调用垃圾回收GC.Collect(),GC会选择合适的时机、合适的方式进行内存回收的。
关于代龄(Generation)
当然,实际的垃圾回收过程可能比上面的要复杂,如果没次都扫描托管堆内的所有对象实例,这样做太耗费时间而且没有必要。分代(Generation)算法是CLR垃圾回收器采用的一种机制,它唯一的目的就是提升应用程序的性能。分代回收,速度显然快于回收整个堆。分代(Generation)算法的假设前提条件:
1、大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长 2、对部分内存进行回收比基于全部内存的回收操作要快undefined3、新创建的对象之间关联程度通常较强。heap分配的对象是连续的,关联度较强有利于提高CPU cache的命中率
如图3,.NET将托管堆分成3个代龄区域: Gen 0、Gen 1、Gen 2:
大部分情况,GC只需要回收0代即可,这样可以显著提高GC的效率,而且GC使用启发式内存优化算法,自动优化内存负载,自动调整各代的内存大小。
非托管资源回收
.NET中提供释放非托管资源的方式主要是:Finalize() 和 Dispose()。
常用的大多是Dispose模式,主要实现方式就是实现IDisposable接口,下面是一个简单的IDisposable接口实现方式。
public class SomeType : IDisposable
{
public MemoryStream _MemoryStream;
public void Dispose()
{
if (_MemoryStream != null) _MemoryStream.Dispose();
}
}
Dispose需要手动调用,在.NET中有两中调用方式:
//方式1:显示接口调用
SomeType st1=new SomeType();
//do sth
st1.Dispose();
//方式2:using()语法调用,自动执行Dispose接口
using (var st2 = new SomeType())
{
//do sth
}
第一种方式,显示调用,缺点显而易见,如果程序猿忘了调用接口,则会造成资源得不到释放。或者调用前出现异常,当然这一点可以使用try…finally避免。
一般都建议使用第二种实现方式,他可以保证无论如何Dispose接口都可以得到调用,原理其实很简单,using()的IL代码如下图,因为using只是一种语法形式,本质上还是try…finally的结构。
首先了解下Finalize方法的来源,她是来自System.Object中受保护的虚方法Finalize,无法被子类显示重写,也无法显示调用,是不是有点怪?。她的作用就是用来释放非托管资源,由GC来执行回收,因此可以保证非托管资源可以被释放。
所有实现了终结器(析构函数)的对象,会被GC特殊照顾,GC的终止化队列跟踪所有实现了Finalize方法(析构函数)的对象。
上面的过程是不是很复杂!是就对了,如果想彻底搞清楚,没有捷径,不要偷懒,还是去看书吧!
简单总结一下:Finalize()可以确保非托管资源会被释放,但需要很多额外的工作(比如终结对象特殊管理),而且GC需要执行两次才会真正释放资源。听上去好像缺点很多,她唯一的优点就是不需要显示调用。
有些编程意见或程序猿不建议大家使用Finalize,尽量使用Dispose代替,我觉得可能主要原因在于:第一是Finalize本身性能并不好;其次很多人搞不清楚Finalize的原理,可能会滥用,导致内存泄露。因此就干脆别用了,其实微软是推荐大家使用的,不过是和Dispose一起使用,同时实现IDisposable接口和Finalize(析构函数),其实FCL中很多类库都是这样实现的,这样可以兼具两者的优点:
性能优化建议
尽量不要手动执行垃圾回收的方法:GC.Collect()
垃圾回收的运行成本较高(涉及到了对象块的移动、遍历找到不再被使用的对象、很多状态变量的设置以及Finalize方法的调用等等),对性能影响也较大,因此我们在编写程序时,应该避免不必要的内存分配,也尽量减少或避免使用GC.Collect()来执行垃圾回收,一般GC会在最适合的时间进行垃圾回收。
而且还需要注意的一点,在执行垃圾回收的时候,所有线程都是要被挂起的(如果回收的时候,代码还在执行,那对象状态就不稳定了,也没办法回收了)。
推荐Dispose代替Finalize
如果你了解GC内存管理以及Finalize的原理,可以同时使用Dispose和Finalize双保险,否则尽量使用Dispose。
选择合适的垃圾回收机制:工作站模式、服务器模式
public class User
{
public int Age { get; set; }
public string Name { get; set; }
public string _Name = "123" + "abc";
public List<string> _Names;
}
40字节内存空间,详细分析文章中给出了。
一个变量如果在其生存期内的某一时刻已经不再被引用,那么,这个对象就有可能成为垃圾
GC是垃圾回收(Garbage Collect)的缩写,是.NET核心机制的重要部分。她的基本工作原理就是遍历托管堆中的对象,标记哪些被使用对象(哪些没人使用的就是所谓的垃圾),然后把可达对象转移到一个连续的地址空间(也叫压缩),其余的所有没用的对象内存被回收掉。
① 标记:先假设所有对象都是垃圾,根据应用程序根Root遍历堆上的每一个引用对象,生成可达对象图,对于还在使用的对象(可达对象)进行标记(其实就是在对象同步索引块中开启一个标示位)。
② 清除:针对所有不可达对象进行清除操作,针对普通对象直接回收内存,而对于实现了终结器的对象(实现了析构函数的对象)需要单独回收处理。清除之后,内存就会变得不连续了,就是步骤3的工作了。
③ 压缩:把剩下的对象转移到一个连续的内存,因为这些对象地址变了,还需要把那些Root跟指针的地址修改为移动后的新地址。
using() 只是一种语法形式,其本质还是try…finally的结构,可以保证Dispose始终会被执行。
C#里的析构函数其实就是终结器Finalize,因为长得像C++里的析构函数而已。
有些编程建议里不推荐使用析构函数要原因在于:第一是Finalize本身性能并不好;其次很多人搞不清楚Finalize的原理,可能会滥用,导致内存泄露,因此就干脆别用了
Finalize() 和 Dispose()都是.NET中提供释放非托管资源的方式,他们的主要区别在于执行者和执行时间不同:
另外一个重点区别就是终结器会导致对象复活一次,也就说会被GC回收两次才最终完成回收工作,这也是有些人不建议开发人员使用终结器的主要原因。
是的,可能会。比如:
版权所有,文章来源:http://www.cnblogs.com/anding 个人能力有限,本文内容仅供学习、探讨,欢迎指正、交流。
.NET面试题解析(00)-开篇来谈谈面试 & 系列文章索引
书籍:CLR via C#
书籍:你必须知道的.NET