.NET面试题系列(2)GC
序言
对象生存期
Phone item=new Phone()
在C#中,创建对象使用的是new关键字。 要注意的是new操作返回的并不是对象本身,而是对象的一个引用(Reference)。
如果使用item=null;语句,将上面的item变量赋值为null,不过是切断了变量和对象之间的引用关系,对象并没有销毁,还停留在托管堆上。
并不是将item设为null才会将对象变为无法访问的。 如果方法结束后,再没有其他地方引用对象,那么它也会变成无法访问的。
垃圾回收机制
什么是垃圾?
简单理解就是没有被引用的对象。
GC是垃圾回收(Garbage Collect)的缩写,是.NET核心机制的重要部分。它的基本工作原理就是遍历托管堆中的对象,标记哪些被使用对象(那些没人使用的就是所谓的垃圾),然后把可达对象转移到一个连续的地址空间(也叫压缩),其余的所有没用的对象内存被回收掉。
托管资源:
由CLR管理的存在于托管堆上的称为托管资源,注意这里有2个关键点,第一是由CLR管理,第二存在于托管堆上。托管资源的回收工作是不需要人工干预的,CLR会在合适的时候调用GC(垃圾回收器)进行回收。
我们可以使用GC类提供的GC.Collect方法来使应用程序在一定程度上直接控制垃圾回收器,但是一般不要去手动干预GC。没有特殊理由,不要去调用GC.Collect(),让它自己决定什么时候去回收内存。还是人家的比较严谨。
非托管资源:
非托管资源是不由CLR管理,例如:Image Socket, StreamWriter, Timer, Tooltip, 文件句柄, GDI资源, 数据库连接等等资源(这里仅仅列举出几个常用的)。这些资源GC是不会自动回收的,需要手动释放。
垃圾回收算法 - 分代(Generation)算法
CLR托管堆支持3代:第0代,第1代,第2代。第0代的空间约为256KB,第1代约为2M,第2代约为10M。新构造的对象会被分配到第0代,
当第0代的空间满时,垃圾回收器启动回收,不可达对象(上图C、E)会被回收,存活的对象被归为第1代。
当第0代空间已满,第1代也开始有很多不可达对象以至空间将满时,这时两代垃圾都将被回收。存活下来的对象(可达对象),第0代升为第1代,第1代升为第2代。
垃圾回收的基本流程包含以下三个关键步骤:
① 标记
② 清除
③ 压缩
关于代龄(Generation)
当然,实际的垃圾回收过程可能比上面的要复杂,如果每次都扫描托管堆内的所有对象实例,这样做太耗费时间而且没有必要。分代(Generation)算法是CLR垃圾回收器采用的一种机制,它唯一的目的就是提升应用程序的性能。分代回收,速度显然快于回收整个堆。分代(Generation)算法的假设前提条件:
1、大量新创建的对象生命周期都比较短,而较老的对象生命周期会更长
2、对部分内存进行回收比基于全部内存的回收操作要快
3、新创建的对象之间关联程度通常较强。heap分配的对象是连续的,关联度较强有利于提高CPU cache的命中率
.NET将托管堆分成3个代龄区域: Gen 0、Gen 1、Gen 2:
第0代,新近分配在堆上的对象,从来没有被垃圾收集过。 任何一个新对象,当它第一次被分配在托管堆上时,就是第0代。
第1代,经历过一次垃圾回收后,依然保留在堆上的对象。
第2代,经历过两次或以上垃圾回收后,依然保留在堆上的对象。
当进行垃圾回收时,垃圾回收器将会首先检查所有的第0代对象,并对其中可回收的对象进行清理。如果清理后获取到了足够的内存空间,经历过垃圾回收后的对象将提升为第1代对象。
如果所有第0代对象都检查过,但是内存空间还不够用,那么将会检查第1代对象的可访问性,并进行垃圾回收。 此时,如果经历过垃圾回收的第1代对象仍保留在堆上,则会升级为第2代对象。 类似地,如果内存仍不够用,将会对第2代对象进行检查和垃圾回收。 如果第2代的部分对象在此次回收后仍保留在堆栈上,它依然是第2代对象,因为总共只定义了三代对象。 如果第2代对象在进行完垃圾回收后空间仍然不够用,则会抛出OutOfMemoryException异常。
大对象堆
垃圾回收的过程中还有一个很影响性能的地方,就是在压缩的过程中,因为要批量地挪动对象,以填充腾出来的空间,如果对象很大,那么要挪动的数据量就会很大。 除此以外,如果将大对象直接分配在第0代,那么第0代的空间很快就会被占满,从而迫使CLR执行一次垃圾回收,这样执行垃圾回收的次数就会变得很频繁。因此,第二个优化策略就是采用大对象堆(LOH,Large Object Heap),当对象的大小超过指定数值(85000字节)时,就会被分配在大对象堆上。 大对象堆有几个特点:
没有代级的概念,所有对象都被视为第2代。
不进行对象移动和空间压缩,因为移动大对象是相对耗时的操作。 因此,需要一个链表来维护空闲区域的位置。
对象不会被分配在末尾,而会在链表中寻找合适的位置,因此会存在碎片的问题。
对象析构
非托管资源回收.NET中提供释放非托管资源的方式主要是:Finalize() 和 Dispose()。
Dispose()常用的大多是Dispose模式,主要实现方式就是实现IDisposable接口
Finalize() 终结器(析构函数)磁盘文件、 TCP连接、 通信端口、 数据库连接等, 当对象被垃圾回收时, 只是被简单地覆盖掉, 并不会释放这些资源。
Dispose()和Finalize()区别Finalizer的执行时间是不确定的, 有时候, 我们期望客户端在对象使用完毕后立即释放资源,此时可以实现IDisposable()接口:
public interface IDisposable { void Dispose(); }
using
using(A a = new A()) { //使用A对象的方法 }
在作用域结束的时候,会自动调用A对象的Dispose方法
但是前提A对象必须实现了IDispose接口
否则无法使用using关键字
性能优化建议
尽量不要手动执行垃圾回收的方法:GC.Collect()
垃圾回收的运行成本较高(涉及到了对象块的移动、遍历找到不再被使用的对象、很多状态变量的设置以及Finalize方法的调用等等),对性能影响也较大,因此我们在编写程序时,应该避免不必要的内存分配,也尽量减少或避免使用GC.Collect()来执行垃圾回收,一般GC会在最适合的时间进行垃圾回收。
而且还需要注意的一点,在执行垃圾回收的时候,所有线程都是要被挂起的(如果回收的时候,代码还在执行,那对象状态就不稳定了,也没办法回收了)。
推荐Dispose代替Finalize
如果你了解GC内存管理以及Finalize的原理,可以同时使用Dispose和Finalize双保险,否则尽量使用Dispose。
选择合适的垃圾回收机制:工作站模式、服务器模式
关于GC的面试题
1. 简述一下一个引用对象的生命周期?
- new创建对象并分配内存
- 对象初始化
- 对象操作、使用
- 资源清理(非托管资源)
- GC垃圾回收
2. 创建下面对象实例,需要申请多少内存空间?
public class User { public int Age { get; set; } public string Name { get; set; } public string _Name = "123" + "abc"; public List<string> _Names; }
40字节内存空间,详细分析文章中给出了。
3. 什么是垃圾?
一个变量如果在其生存期内的某一时刻已经不再被引用,那么,这个对象就有可能成为垃圾。
4. GC是什么,简述一下GC的工作方式?
GC是垃圾回收(Garbage Collect)的缩写,是.NET核心机制的重要部分。她的基本工作原理就是遍历托管堆中的对象,标记哪些被使用对象(哪些没人使用的就是所谓的垃圾),然后把可达对象转移到一个连续的地址空间(也叫压缩),其余的所有没用的对象内存被回收掉。
5. GC进行垃圾回收时的主要流程是?
① 标记:先假设所有对象都是垃圾,根据应用程序根Root遍历堆上的每一个引用对象,生成可达对象图,对于还在使用的对象(可达对象)进行标记(其实就是在对象同步索引块中开启一个标示位)。
② 清除:针对所有不可达对象进行清除操作,针对普通对象直接回收内存,而对于实现了终结器的对象(实现了析构函数的对象)需要单独回收处理。清除之后,内存就会变得不连续了,就是步骤3的工作了。
③ 压缩:把剩下的对象转移到一个连续的内存,因为这些对象地址变了,还需要把那些Root跟指针的地址修改为移动后的新地址。
6. GC在哪些情况下会进行回收工作?
- 内存不足溢出时(0代对象充满时)
- Windwos报告内存不足时,CLR会强制执行垃圾回收
- CLR卸载AppDomian,GC回收所有
- 调用GC.Collect
- 其他情况,如主机拒绝分配内存,物理内存不足,超出短期存活代的存段门限
7. using() 语法是如何确保对象资源被释放的?如果内部出现异常依然会释放资源吗?
using() 只是一种语法形式,其本质还是try…finally的结构,可以保证始终会被执行。
8. 解释一下C#里的析构函数?为什么有些编程建议里不推荐使用析构函数呢?
C#里的析构函数其实就是终结器Finalize,因为长得像C++里的析构函数而已。
有些编程建议里不推荐使用析构函数要原因在于:第一是Finalize本身性能并不好;其次很多人搞不清楚Finalize的原理,可能会滥用,导致内存泄露,因此就干脆别用了
9. Finalize() 和 Dispose() 之间的区别?
Finalize() 和 Dispose()都是.NET中提供释放非托管资源的方式,他们的主要区别在于执行者和执行时间不同:
- finalize由垃圾回收器调用;dispose由对象调用。
- finalize无需担心因为没有调用finalize而使非托管资源得不到释放,而dispose必须手动调用。
- finalize不能保证立即释放非托管资源,Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间;而dispose一调用便释放非托管资源。
- 只有class类型才能重写finalize,而结构不能;类和结构都能实现IDispose。
另外一个重点区别就是终结器会导致对象复活一次,也就说会被GC回收两次才最终完成回收工作,这也是有些人不建议开发人员使用终结器的主要原因。
https://i.cnblogs.com/posts/edit;postId=8465963
Finalization Queue和Freachable Queue
这两个队列和.net对象所提供的Finalize方法有关。这两个队列并不用于存储真正的对象,而是存储一组指向对象的指针。当程序中使用了new操作符在Managed Heap上分配空间时,GC会对其进行分析,如果该对象含有Finalize方法则在Finalization Queue中添加一个指向该对象的指针。在GC被启动以后,经过Mark阶段分辨出哪些是垃圾。再在垃圾中搜索,如果发现垃圾中有被Finalization Queue中的指针所指向的对象,则将这个对象从垃圾中分离出来,并将指向它的指针移动到Freachable Queue中。这个过程被称为是对象的复生(Resurrection),本来死去的对象就这样被救活了。为什么要救活它呢?因为这个对象的Finalize方法还没有被执行,所以不能让它死去。Freachable Queue平时不做什么事,但是一旦里面被添加了指针之后,它就会去触发所指对象的Finalize方法执行,之后将这个指针从队列中剔除,这是对象就可以安静的死去了。.net framework的System.GC类提供了控制Finalize的两个方法,ReRegisterForFinalize和SuppressFinalize。前者是请求系统完成对象的Finalize方法,后者是请求系统不要完成对象的Finalize方法。ReRegisterForFinalize方法其实就是将指向对象的指针重新添加到Finalization Queue中。这就出现了一个很有趣的现象,因为在Finalization Queue中的对象可以复生,如果在对象的Finalize方法中调用ReRegisterForFinalize方法,这样就形成了一个在堆上永远不会死去的对象,像凤凰涅槃一样每次死的时候都可以复生。
10. Dispose和Finalize方法在何时被调用?
- Dispose一调用便释放非托管资源;
- Finalize不能保证立即释放非托管资源,Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间;
11. .NET中的托管堆中是否可能出现内存泄露的现象?
是的,可能会。比如:
- 不正确的使用静态字段,导致大量数据无法被GC释放;
- 没有正确执行Dispose(),非托管资源没有得到释放;
- 不正确的使用终结器Finalize(),导致无法正常释放资源;
- 其他不正确的引用,导致大量托管对象无法被GC释放;
12. 在托管堆上创建新对象有哪几种常见方式?
- new一个对象;
- 字符串赋值,如string s1=”abc”;
- 值类型装箱;
13.与GC相关的性能计数器
如果遇到了性能问题,在使用debug之前分析问题较为不错的一个工具就是perfmon。
如果您的网站遇到下面的几种情形,那还是先看看perfmon里GC相关的东西吧:
- cpu占用高,内存占用不高.
- cpu和内存占用都比较高
- cpu和内存占用都不高,但是网站响应很慢
14.手工进行回收
//对所有代进行垃圾回收。 GC.Collect(); //对指定的代进行垃圾回收。 GC.Collect(int generation); //强制在 System.GCCollectionMode 值所指定的时间对零代到指定代进行垃圾回收。 GC.Collect(int generation, GCCollectionMode mode);