面试出现频率:经常出现,但通常不会问的十分深入。通常来说,看完我这篇文章就足够应付面试了。面试时主要考察垃圾回收的基本概念,标记-压缩算法,以及对于微软的垃圾回收模板的理解。知道什么时候需要继承IDisposible接口,解构函数是做什么用的,什么时候需要自己写一个解构函数。

重要程度:10/10

参考书籍:CLR via C#,其对垃圾回收讲解的十分详细,有些内容甚至过于高深。熟悉垃圾回收可以使你的程序更加健壮,性能更好。

4.1 托管堆的构造

垃圾回收的主要操作对象是托管堆,托管堆包括GC堆和加载堆。

GC堆里面为了提高内存管理效率等因素,分成多个部分,其中两个主要部分为:

  1. 0/1/2代:越大的代的堆空间越大。
  2. 大对象堆(Large Object Heap),大于85000字节的大对象会分配到这个区域,这个区域的主要特点就是:不会轻易被回收;就是回收了也不会被压缩(因为对象太大,移动复制的成本太高)。大对象堆是第二代GC堆的一部分。

加载堆不受GC管辖。加载堆上的主要对象有类型对象和它们的静态字段,字符串驻留池等。几个非托管资源的例子:StreamWriter,数据库连接对象等。

4.2 关于垃圾

  • 垃圾是不会再被用到的资源。具体的情况则包括超出该变量的有效范围(离开了对应的大括号的区域变量),将变量指定为null,重新指向其他物件(而原先指向的物件已无法被取得),重新初始化等,这时原先变量占有的空间都会被CLR视为垃圾而等待回收。
  • 托管代码/资源/物件是会被CLR管理的代码(CLR会对它们进行内存管理,垃圾回收,线程管理等),反之则是非托管代码。
  • C#的值类型(如果它属于托管代码)存储在栈中。使用完(离开其作用域)就立刻销毁。
  • C#的引用类型(如果它属于托管代码)存储在栈和堆中。使用完(离开其作用域)栈上的资料立刻销毁,而堆上(栈上所引用的资料指向堆上的一块空间)的资料不立刻销毁。销毁时间根据其世代而定。

4.3 简述GC的垃圾回收策略

  • GC将整个托管堆分成0代,1代和2代三个区域。更高的世代的区域更大。所有的引用对象一开始都是在第0代分配地址。进行垃圾回收时,大部分情况都是只对某个特定代进行操作。这样分配基于下面几个假设:

    • 越老的对象生存期越长(即还可能继续生存很长一段时间)
    • 回收堆的一部分快于回收整个堆
  • 当程序调用new操作符创建对象时,会计算类型(及其所有基类型)的字段需要的字节数。如果托管堆已经没有足够的空间来创建新对象了(第0代满),就触发一次垃圾回收。
  • 整个回收将会遍历0,1,2三代区域,并先标记,后压缩,标记了的所有0代垃圾被销毁,幸存者移到第一代堆中。标记了的所有1代垃圾被销毁,幸存者被移到第2代堆中。所有第二代堆的垃圾将会被销毁。幸存者仍然在第2代堆中。
  • GC使用的垃圾回收算法是先标记(垃圾),之后压缩,将垃圾清理,释放,将幸存者升代,使得垃圾释放空出来的位置变得连续。类似于磁盘空间的碎片整理。连续的空间便于管理和建立新的对象。
  • 具体一点说,每个应用程序都包含一组根,每个根都是一个存储位置,其中包含指向引用类型对象的一个指针。该指针要么引用堆中的一个对象,要么为null。
  • GC开始执行时,假设堆上所有的对象都是垃圾。在标记阶段,GC沿着线程栈开始遍历,检查每个根是否为null。对于那些有引用对象的根,则不认为它们是垃圾。
  • 可以通过呼叫GC.Collect来主动触发一次垃圾回收(甚至可以指定某代),但通常这是没必要的。

4.4 何时需要继承IDisposible接口?

你可以继承IDisposible接口,然后在Dispose方法中销毁任何资源,包括非托管资源。但如果你忘记了调用它,那么你的非托管资源将没有任何机会得到释放。只有当你的类型含有非托管资源,或者实现了IDisposible的托管资源时,你才需要继承IDisposible接口,实现一个Dispose。 如果你只面对一堆托管资源,并且它们都没有实现IDisposible时,你不需要做任何事。

4.5 什么是Finalize方法?

  • 只要对象继承自Object,它就拥有Finalize方法。在创建这个对象时,会在Finalization Queue(终结列表,由垃圾回收器控制的一个内部数据结构)为其加入一个指针。拥有Finalize方法的对象被称为可终结的。
  • Finalize方法又被称为终结器。复写Finalize方法称为实现终结器。只有你需要释放非托管资源时才需要这么做。
  • 复写Finalize方法的唯一方法是实现一个解构函数。解构函数的实现只有一个意义,就是保证非托管资源得到回收,作为Dispose这道关口后面的最终总闸,因为解构函数是肯定会被执行到的。
  • 垃圾清理时,会标记所有的垃圾,并探查终结列表,并将其中为垃圾的对象移除出终结列表,加到Freachable Queue之中(这无形当中会给对象续命一轮GC,因为此时对象被Freachable Queue引用,不再是没有被任何其他对象引用的垃圾)。
  • 一个特殊的高优先级的线程专门负责调用Finalize方法。这可以避免潜在的线程同步问题。Freachable队列为空时,该线程睡眠。一旦Freachable队列有记录出现,该线程就会被唤醒,将每一项都从Freachable队列中移出,并调用每一项对象的Finalize方法,该方法会销毁对象。
  • 当GC隐式的处理垃圾回收时,第一轮GC会将所有的拥有Finalize方法的垃圾移动到Freachable Queue之中,并不调用Finalize方法(所以对象还活着)。下一轮GC才遍历上面那轮GC中,放到Freachable Queue的对象,并使用Finalize方法销毁那些引用类型对象。所以如果对象拥有Finalize方法,它的寿命会无形之中延长一轮GC(称为对象的复生),并且它的Finalize方法调用的时间是不可知的。在必要的时候,你可以实现IDisposible接口利用Dispose来主动销毁资源,并在Dispose()成功地执行之后呼叫GC.SuppressFinalize(this); 这可以告诉GC不需再去呼叫这个物件的Finalize方法(因为Dispose执行过了之后Finalize不需要执行了),这样GC就不会把对象从终结列表移动到freachable队列,可以回避系统的续命行为。
  • 因为终结器会导致续命,所以请留心,记得呼叫Dispose,并呼叫GC.SuppressFinalize(this),这可以让终结器没有机会上场,对象就被销毁了。

4.6 什么是解构函数?何时需要写一个解构函数?

  • 解构函数是Finalize方法的override。它将会被隐式的转换为一个带有try-finally的Finalize方法,覆盖它的父对象的Finalize,并在finally中呼叫base.Finalize。(此处的base指System.Object)
  • 解构函数不能有参数和方法修饰符。除非你主动触发垃圾回收,它的执行时间是不可知的。
  • 虽然仅由托管资源组成的类型也可能会因为用户忘了呼叫Dispose而暂时存留在堆中,这并不会造成太大的问题,因为GC最终会回收它。而如果类型中有非托管资源,你需要实现解构函数。如果你没有实现解构函数,又忘了呼叫Dispose,则当GC回收这个类型时(通过Finalize),将只会回收托管资源(非托管资源没有Finalize方法),非托管资源将会一直存留在堆中。

4.7 如何回收托管资源?

如果类型没有非托管资源,此时,因为所有托管资源肯定都有Finalize方法,我们不需要实现解构函数。特别的,对于实现了IDisposible的类型,我们只需要简单的调用Dispose来释放资源即可(这会调用那个类型的Dispose方法,如果类型是属于微软的,则微软已经给你实现好了)。有些类型的Dispose方法的名称为Close。

如果你的托管资源包含了一些实现了IDisposible接口的成员时,你要继承IDisposible接口,并在Dispose方法中将这些成员回收。或者,你在使用成员时,使用using关键字。using关键字本质上是一个try - finally块,所以即使你在using块中发生了异常,也不用担心,对象仍然会在finally块中被dispose。(曾经有面试官问过我这个问题)

4.8 如何回收非托管资源?

如果你只是临时使用非托管资源,那么将其包含在using中就可以了,例如使用StreamWriter。

假设你的类型中含有非托管资源属性/字段,此时,你要继承IDisposible接口,实现Dispose方法,并写一个解构函数。你可以follow微软的垃圾回收模板,步骤如下:

  1. 写一个私有的方法,在私有的方法中,释放托管资源(如果该资源拥有Dispose方法则可以通过呼叫它的Dispose方法完成)和非托管资源。
  2. 实现Dispose方法,呼叫私有方法,之后呼叫SuppressFinalize。
  3. 实现一个解构函数(这会覆盖原有的Finalize方法)在其中呼叫私有方法。这是为了防止用户忘了呼叫Dispose方法而最终没有回收这个非托管资源。原有的Finalize方法并不会理会非托管资源。在解构函数中你不需要呼叫SuppressFinalize因为这已经是Finalize方法了,续命已经发生了。
    public sealed class WindowStationHandle : IDisposable
{
// 非托管资源
public IntPtr Handle { get; set; } public WindowStationHandle(IntPtr handle)
{
this.Handle = handle;
} public WindowStationHandle()
: this(IntPtr.Zero)
{
} public bool IsInvalid
{
get { return (this.Handle == IntPtr.Zero); }
} // 私有方法
private void CloseHandle()
{
if (this.IsInvalid)
{
return;
} if (!NativeMethods.CloseWindowStation(this.Handle))
{
Trace.WriteLine("CloseWindowStation: " + new Win32Exception().Message);
} // 释放非托管资源
this.Handle = IntPtr.Zero;
} public void Dispose()
{
//实现Dispose方法,呼叫私有方法,之后呼叫SuppressFinalize
this.CloseHandle();
GC.SuppressFinalize(this);
} ~WindowStationHandle()
{
//实现一个解构函数(这会覆盖原有的Finalize方法)在其中呼叫私有方法。
//这是为了防止用户忘了呼叫Dispose方法而最终没有回收这个非托管资源。
//原有的Finalize方法并不会理会非托管资源。
this.CloseHandle();
}
}

4.10 垃圾回收策略

结合上面4.4,4.8和4.9,就构成了常规的垃圾回收策略:

  1. 类中没有非托管资源,且没有对象实现IDisposible: 什么也不用做。
  2. 类中没有非托管资源,且有对象实现IDisposible: 特别注意这些对象,确保调用了它们的Dispose方法(显式或者隐式的)。你可以实现IDisposible,然后实现Dispose方法,在其中释放资源。
  3. 类中有非托管资源: 跟从微软模板,实现一个私有函数释放托管和非托管资源,实现IDisposible,然后实现Dispose方法,并在其中调用私有函数,然后呼叫GC.SuppressFinalize(第一道闸)。实现一个解构函数,并在其中调用私有函数(第二道闸)。如果你的第一道闸完美无缺,第二道闸是没有机会上场的。

4.11 扩展阅读:

大对象堆陷阱:http://www.cnblogs.com/brucebi/archive/2013/04/16/3024136.html

.NET面试题系列[5] - 垃圾回收:概念与策略的更多相关文章

  1. 李洪强iOS经典面试题37-解释垃圾回收的原理

    李洪强iOS经典面试题37-解释垃圾回收的原理   问题 我们知道,Android 手机通常使用 Java 来开发,而 Java 是使用垃圾回收这种内存管理方式. 那么,ARC 和垃圾回收对比,有什么 ...

  2. 李洪强iOS经典面试题31-解释垃圾回收的原理

    李洪强iOS经典面试题31-解释垃圾回收的原理 问题 我们知道,Android 手机通常使用 Java 来开发,而 Java 是使用垃圾回收这种内存管理方式. 那么,ARC 和垃圾回收对比,有什么优点 ...

  3. 【JVM】JVM系列之垃圾回收(二)

    一.为什么需要垃圾回收 如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收.除非内存无限大,我们可以任性的分配而不回收,但是事实并非如此.所以,垃圾回收是必须的. 二. ...

  4. 【Java面试题】49 垃圾回收的优点和原理。并考虑2种回收机制。

    1.Java语言最显著的特点就是引入了垃圾回收机制,它使java程序员在编写程序时不再考虑内存管理的问题. 2.由于有这个垃圾回收机制,java中的对象不再有“作用域”的概念,只有引用的对象才有“作用 ...

  5. jvm 垃圾回收概念和算法

    1.概念 GC 中的垃圾,特指存在于内存中.不会再被使用的对象.垃圾回收有很多种算法,如引用计数法.复制算法.分代.分区的思想. 2.算法 1.引用计数法:对象被其他所引用时计数器加 1,而当引用失效 ...

  6. 面试题-Java基础-垃圾回收

    1.Java中垃圾回收有什么目的?什么时候进行垃圾回收? 垃圾回收的目的是识别并且丢弃应用不再使用的对象来释放和重用资源. 2.System.gc()和Runtime.gc()会做什么事情? 这两个方 ...

  7. jvm系列三垃圾回收

    三.垃圾回收 1.如何判断对象可以回收 引用计数法 弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放 可达性分析算法 JVM中的垃圾回收器通过可达性分析来探索所有存活的对象 扫描堆中的 ...

  8. .NET面试题系列[8] - 泛型

    “可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用.“ - Jon Skeet .NET面试题系列目录 .NET面试题系列[1] - .NET框架基础知识(1) .NET面试题系列[2] ...

  9. .NET面试题系列[0] - 写在前面

    .NET面试题系列目录 .NET面试题系列[1] - .NET框架基础知识(1) .NET面试题系列[2] - .NET框架基础知识(2) .NET面试题系列[3] - C# 基础知识(1) .NET ...

随机推荐

  1. 先说IEnumerable,我们每天用的foreach你真的懂它吗?

    我们先思考几个问题: 为什么在foreach中不能修改item的值? 要实现foreach需要满足什么条件? 为什么Linq to Object中要返回IEnumerable? 接下来,先开始我们的正 ...

  2. ABP框架 - Swagger UI 集成

    文档目录 本节内容: 简介 Asp.net Core 安装 安装Nuget包 配置 测试 Asp.net 5.x 安装 安装Nuget包 配置 测试 简介 来自它的网页:“...使用一个Swagger ...

  3. 在.Net中实现自己的简易AOP

    RealProxy基本代理类 RealProxy类提供代理的基本功能.这个类中有一个GetTransparentProxy方法,此方法返回当前代理实例的透明代理.这是我们AOP实现的主要依赖. 新建一 ...

  4. C# 条形码操作【源码下载】

    本篇介绍通过C#生成和读取一维码.二维码的操作. 目录 1. 介绍:介绍条形码.条形码的分类以及ZXing.Net类库. 2. 一维码操作:包含对一维码的生成.读取操作. 3. 二维码操作:包含对二维 ...

  5. CoreCRM 开发实录——Travis-CI 实现 .NET Core 程度在 macOS 上的构建和测试 [无水干货]

    上一篇文章我提到:为了使用"国货",我把 Linux 上的构建和测试委托给了 DaoCloud,而 Travis-CI 不能放着不用啊.还好,这货支持 macOS 系统.所以就把 ...

  6. 微软新神器-Power BI横空出世,一个简单易用,还用得起的BI产品,你还在等什么???

    在当前互联网,由于大数据研究热潮,以及数据挖掘,机器学习等技术的改进,各种数据可视化图表层出不穷,如何让大数据生动呈现,也成了一个具有挑战性的可能,随之也出现了大量的商业化软件.今天就给大家介绍一款逆 ...

  7. html5 canvas常用api总结(二)--绘图API

    canvas可以绘制出很多奇妙的样式和美丽的效果,通过几个简单的api就可以在画布上呈现出千变万化的效果,还可以制作网页游戏,接下来就总结一下和绘图有关的API. 绘画的时候canvas相当于画布,而 ...

  8. JavaWeb——Listener

    一.基本概念 JavaWeb里面的listener是通过观察者设计模式进行实现的.对于观察者模式,这里不做过多介绍,大概讲一下什么意思. 观察者模式又叫发布订阅模式或者监听器模式.在该模式中有两个角色 ...

  9. C#反序列化XML异常:在 XML文档(0, 0)中有一个错误“缺少根元素”

    Q: 在反序列化 Xml 字符串为 Xml 对象时,抛出如下异常. 即在 XML文档(0, 0)中有一个错误:缺少根元素. A: 首先看下代码: StringBuilder sb = new Stri ...

  10. git提交项目到已存在的远程分支

    今天想提交项目到github的远程分支上,那个远程分支是之前就创建好的,而我的本地关联分支还没创建.   之前从未用github提交到远程分支过,弄了半个钟,看了几篇博文,终于折腾出来.现在把步骤整理 ...