zoukankan      html  css  js  c++  java
  • .NET面试题系列[5]

     面试出现频率:经常出现,但通常不会问的十分深入。通常来说,看完我这篇文章就足够应付面试了。面试时主要考察垃圾回收的基本概念,标记-压缩算法,以及对于微软的垃圾回收模板的理解。知道什么时候需要继承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

  • 相关阅读:
    PAT 解题报告 1009. Product of Polynomials (25)
    PAT 解题报告 1007. Maximum Subsequence Sum (25)
    PAT 解题报告 1003. Emergency (25)
    PAT 解题报告 1004. Counting Leaves (30)
    【转】DataSource高级应用
    tomcat下jndi配置
    java中DriverManager跟DataSource获取getConnection有什么不同?
    理解JDBC和JNDI
    JDBC
    Dive into python 实例学python (2) —— 自省,apihelper
  • 原文地址:https://www.cnblogs.com/zhangxiaolei521/p/5761938.html
Copyright © 2011-2022 走看看