C#垃圾回收学习总结

浅谈C#垃圾回收

http://www.cnblogs.com/cuiyiming/archive/2013/03/26/2981931.html


  理解C#垃圾回收机制我们首先说一下CLR(公共语言运行时,Common Language Runtime)它和Java虚拟机一样是一个运行时环境,核心功能包括:内存管理、程序集加载、安全性、异步处理和线程同步。

CTS(Common Type System)通用类型系统,它把.Net中的类型分为2大类,引用类型与值类型。.Net中所有类型都间接或直接派生至System.Object类型。所有的值类型都是System.ValueType的子类,而System.ValueType本身却是引用类型。

托管资源:
  由CLR管理的存在于托管堆上的称为托管资源,注意这里有2个关键点,第一是由CLR管理,第二存在于托管堆上。托管资源的回收工作是不需要人工干预的,CLR会在合适的时候调用GC(垃圾回收器)进行回收。

非托管资源:
  非托管资源是不由CLR管理,例如:Image Socket, StreamWriter, Timer, Tooltip, 文件句柄, GDI资源, 数据库连接等等资源(这里仅仅列举出几个常用的)。这些资源GC是不会自动回收的,需要手动释放。

通过上面的讲述总结一下,第一,GC(垃圾回收器)只回收托管资源,不回收非托管资源。第二,GC回收是要在合适的时候(CLR觉得应该进行回收的时候)才进行回收。那么非托管如何进行回收呢?下面就让我一一道来。

在.Net中释放非托管资源主要有2种方式,Dispose,Finalize

Dispose方法,对象要继承IDisposable接口,也就会自动调用Dispose方法。

Class Suifeng:System.IDisposable
{
    #region IDisposable 成员
 
     public void Dispose()
     {
         //
     }
 
     #endregion
}

Suifeng suiFeng= new Suifeng ();
suiFeng.Dispose();


也可以使用Using语句
(using Suifeng suiFeng= new Suifeng())
{
     //
}


Finalize()方法

MSDN上的定义是允许对象在“垃圾回收”回收之前尝试释放资源并执行其他清理操作。
它的本质就是析构函数

class Car
{
    ~Car()  // destructor
    {
        // cleanup statements...
    }
}


该析构函数隐式地对对象的基类调用 Finalize。 这样,前面的析构函数代码被隐式地转换为以下代码:

protected override void Finalize()
{
    try
    {
        // Cleanup statements...
    }
    finally
    {
        base.Finalize();
    }
}

在.NET中应该尽可能的少用析构函数释放资源,MSDN2上有这样一段话:
  实现 Finalize 方法或析构函数对性能可能会有负面影响,因此应避免不必要地使用它们。用 Finalize 方法回收对象使用的内存需要至少两次垃圾回收。当垃圾回收器执行回收时,它只回收没有终结器的不可访问对象的内存。这时,它不能回收具有终结器的不可


访问对象。它改为将这些对象的项从终止队列中移除并将它们放置在标为准备终止的对象列表中。该列表中的项指向托管堆中准备被调用其终止代码的对象。垃圾回收器为此列表中的对象调用 Finalize 方法,然后,将这些项从列表中移除。后来的垃圾回收将确定终止


的对象确实是垃圾,因为标为准备终止对象的列表中的项不再指向它们。在后来的垃圾回收中,实际上回收了对象的内存。


  所以有析构函数的对象,需要两次,第一次调用析构函数,第二次删除对象。而且在析构函数中包含大量的释放资源代码,会降低垃圾回收器的工作效率,影响性能。所以对于包含非托管资源的对象,最好及时的调用Dispose()方法来回收资源,而不是依赖垃圾回收


器。
   在一个包含非托管资源的类中,关于资源释放的标准做法是:
   继承IDisposable接口;
   实现Dispose()方法,在其中释放托管资源和非托管资源,并将对象本身从垃圾回收器中移除(垃圾回收器不在回收此资源);
   实现类析构函数,在其中释放非托管资源。
   请看MSDN上的源码   

Public class BaseResource:IDisposable
   {
      PrivateIntPtr handle; // 句柄,属于非托管资源
      PrivateComponet comp; // 组件,托管资源
      Privateboo isDisposed = false; // 是否已释放资源的标志
       
      PublicBaseResource
      {
      }
        
       //实现接口方法
       //由类的使用者,在外部显示调用,释放类资源
       Public void Dispose()
       {
           Dispose(true);// 释放托管和非托管资源
                           
          //将对象从垃圾回收器链表中移除,
         // 从而在垃圾回收器工作时,只释放托管资源,而不执行此对象的析构函数


            GC.SuppressFinalize(this);
         }
        
         //由垃圾回收器调用,释放非托管资源


       ~BaseResource()
        {
           Dispose(false);// 释放非托管资源
        }
        
     //参数为true表示释放所有资源,只能由使用者调用
    //参数为false表示释放非托管资源,只能由垃圾回收器自动调用
   //如果子类有自己的非托管资源,可以重载这个函数,添加自己的非托管资源的释放
  //但是要记住,重载此函数必须保证调用基类的版本,以保证基类的资源正常释放
    Protectedvirtual void Dispose(bool disposing)
    {
       If(!this.disposed)// 如果资源未释放 这个判断主要用了防止对象被多次释放
         {
            If(disposing)
            {
               Comp.Dispose();// 释放托管资源
             }
                           
           closeHandle(handle);// 释放非托管资源
           handle= IntPtr.Zero;
           }
          this.disposed= true; // 标识此对象已释放
      }
  }


参考了MSDN和网上的一些资料,第一次写博文请各位大侠多多指点!


========

c# -- 对象销毁和垃圾回收

http://www.cnblogs.com/yang_sy/p/3784151.html
有些对象需要显示地销毁代码来释放资源,比如打开的文件资源,锁,操作系统句柄和非托管对象。在.NET中,这就是所谓的对象销毁,它通过IDisposal接口来实现。不再使用的对象所占用的内存管理,必须在某个时候回收;这个被称为无用单元收集的功能由CLR执行



对象销毁和垃圾回收的区别在于:对象销毁通常是明确的策动;而垃圾回收完全是自动地。换句话说,程序员负责释放文件句柄,锁,以及操作系统资源;而CLR负责释放内存。

本章将讨论对象销毁和垃圾回收,还描述了C#处理销毁的一个备选方案--Finalizer及其模式。最后,我们讨论垃圾回收器和其他内存管理选项的复杂性。

对象销毁 垃圾回收
1)IDisposal接口
2) Finalizer 垃圾回收
对象销毁用于释放非托管资源 垃圾回收用于自动释放不再被引用的对象所占用的内存;并且垃圾回收什么时候执行时不可预计的
为了弥补垃圾回收执行时间的不确定性,可以在对象销毁时释放托管对象占用的内存  
 

IDisposal,Dispose和Close

image

.NET Framework定义了一个特定的接口,类型可以使用该接口实现对象的销毁。该接口的定义如下:

public interface IDisposable
{
void Dispose();
}
C#提供了鴘语法,可以便捷的调用实现了IDisposable的对象的Dispose方法。比如:

using (FileStream fs = new FileStream ("myFile.txt", FileMode.Open))
{
// ... Write to the file ...
}
编译后的代码与下面的代码是一样的:


复制代码
FileStream fs = new FileStream ("myFile.txt", FileMode.Open);
try
{
// ... Write to the file ...
}
finally
{
if (fs != null) ((IDisposable)fs).Dispose();
}
复制代码
finally语句确保了Dispose方法的调用,及时发生了异常,或者代码在try语句中提前返回。


在简单的场景中,创建自定义的可销毁的类型值需要实现IDisposable接口即可


sealed class Demo : IDisposable
{
public void Dispose()
{
// Perform cleanup / tear-down.
...
}
}


请注意,对于sealed类,上述模式非常适合。在本章后面,我们会介绍另外一种销毁对象的模式。对于非sealed类,我们强烈建议时候后面的那种销毁对象模式,否则在非sealed类的子类中,也希望实现销毁时,会发生非常诡异的问题。


对象销毁的标准语法


Framework在销毁对象的逻辑方面遵循一套规则,这些规则并不限用于.NET Framework或C#语言;这些规则的目的是定义一套便于使用的协议。这些协议如下:


一旦销毁,对象不可恢复。对象不能被再次激活,调用对象的方法或者属性抛出ObjectDisposedException异常
重复地调用对象的Disposal方法会导致错误
如果一个可销毁对象x包含,或包装,或处理另外一个可销毁对象y,那么x的Dispose方法自动调用x的Dispose方法,除非另有指令(不销毁y)
这些规则同样也适用于我们平常创建自定义类型,尽管它并不是强制性的。没有谁能阻止你编写一个不可销毁的方法;然而,这么做,你的同事也许会用高射炮攻击你。


对于第三条规则,一个容器对象自动销毁其子对象。最好的一个例子就是,windows容器对象比如Form对着Panel。一个容器对象可能包含多个子控件,那你也不需要显示地销毁每个字对象:关闭或销毁父容器会自动关闭其子对象。另外一个例子就是如果你在


DeflateStream包装了FileStream,那么销毁DeflateStream时,FileStream也会被销毁--除非你在构造器中指定了其他的指令。


Close和Stop


有一些类型除了Dispose方法之外,还定义了Close方法。Framework对于Close方法并没有保持完全一致性,但在几乎所有情况下,它可以:


要么在功能上与Dispose一致
或只是Dispose的一部分功能
对于后者一个典型的例子就是IDbConnecton类型,一个Closed的连接可以再次被打开;而一个Disposed的连接对象则不能。另外一个例子就是Windows程序使用ShowDialog的激活某个窗口对象:Close方法隐藏该窗口;而Dispose释放窗口所使用的资源。


有一些类定义Stop方法(比如Timer或HttpListener)。与Dipose方法一样,Stop方法可能会释放非托管资源;但是与Dispose方法不同的是,它允许重新启动。


何时销毁对象


销毁对象应该遵循的规则是“如有疑问,就销毁”。一个可以被销毁的对象--如果它可以说话--那么将会说这些内容:


“如果你结束对我的使用,那么请让我知道。如果只是简单地抛弃我,我可能会影响其他实例对象、应用程序域、计算机、网络、或者数据库”


如果对象包装了非托管资源句柄,那么经常会要求销毁,以释放句柄。例子包括Windows Form控件、文件流或网络流、网络sockets,GDI+画笔、GDI+刷子,和bitmaps。与之相反,如果一个类型是可销毁的,那么它会经常(但不总是)直接或间接地引用非托管句柄。这


是由于非托管句柄对操作系统资源,网络连接,以及数据库锁之外的世界提供了一个网关(出入口),这就意味着使用这些对象时,如果不正确的销毁,那么会对外面的世界代码麻烦。


但是,遇到下面三种情形时,不要销毁对象


通过静态成员或属性获取一个共享的对象
如果一个对象的Dispose方法与你的期望不一样
从设计的角度看,如果一个对象的Dispose方法不必要,且销毁对象给程序添加了复杂度
第一种情况很少见。多数情形都可以在System.Drawing命名空间下找到:通过静态成员或属性获取的GDI+对象(比如Brushed.Blue)就不能销毁,这是因为该实现在程序的整个生命周期中都会用到。而通过构造器得到的对象实例,比如new SolidBrush,就应该销毁,这


同样适用于通过静态方法获取的实例对象(比如Font.FromHdc)。


第二种情况就比较常见。下表以System.IO和System.Data命名空间下类型举例说明


类型 销毁功能 何时销毁
MemoryStream 防止对I/O继续操作 当你需要再次读读或写流
StreamReader,
StreamWriter 清空reader/writer,并关闭底层的流 当你希望底层流保持打开时(一旦完成,你必须改为调用StreamWriter的Flush方法)
IDbConnection 释放数据库连接,并清空连接字符串 如果你需要重新打开数据库连接,你需要调用Close方法而不是Dispose方法
DataContext
(LINQ to SQL) 防止继续使用 当你需要延迟评估连接到Context的查询
第三者情况包含了System.ComponentModel命名空间下的这几个类:WebClient, StringReader, StringWriter和BackgroundWorker。这些类型有一个共同点,它们之所以是可销毁的是源于它们的基类,而不是真正的需要进行必要的清理。如果你需要在一个方法中使用这


样的类型,那么在using语句中实例化它们就可以了。但是,如果实例对象需要持续一段较长的时间,并记录何时不再使用它们以销毁它们,就会给程序带来不惜要的复杂度。在这样的情况下,那么你就应该忽略销毁对象。


选择性地销毁对象


正因为IDisposable实现类可以使用using语句来实例化,因而这可能很容易导致该实现类的Dispose方法延伸至不必要的行为。比如:


public sealed class HouseManager : IDisposable
{
public void Dispose()
{
CheckTheMail();
}
...
}


想法是该类的使用者可以选择避免不必要的清理--简单地说就是不调用Dispose方法。但是,这就需要调用者知道HouseManager类Dispose方法的实现细节。及时是后续添加了必要的清理行为也破坏了规则。


public void Dispose()
{
CheckTheMail(); // Nonessential
LockTheHouse(); // Essential
}
在这种情况下,就应该使用选择性销毁模式


public sealed class HouseManager : IDisposable
{
public readonly bool CheckMailOnDispose;
public Demo (bool checkMailOnDispose)
{
CheckMailOnDispose = checkMailOnDispose;
}
public void Dispose()
{
if (CheckMailOnDispose) CheckTheMail();
LockTheHouse();
}
...
}



这样,任何情况下,调用者都可以调用Dispose--上述实现不仅简单,而且避免了特定的文档或通过反射查看Dispose的细节。这种模式在.net中也有实现。System.IO.Compression空间下的DeflateStream类中,它的构造器如下

public DeflateStream (Stream stream, CompressionMode mode, bool leaveOpen)
非必要的行为就是在销毁对象时关闭内在的流(第一个参数)。有时候,你希望内部流保持打开的同时并销毁DeflateStream以执行必要的销毁行为(清空bufferred数据)


这种模式看起来简单,然后直到Framework 4.5,它才从StreamReader和StreamWriter中脱离出来。结果却是丑陋的:StreamWriter必须暴露另外一个方法(Flush)以执行必要的清理,而不是调用Dispose方法(Framework 4.5在这两个类上公开一个构造器,以允许你保持


流处于打开状态)。System.Security.Cryptography命名空间下的CryptoStream类,也遭遇了同样的问题,当需要保持内部流处于打开时你要调用FlushFinalBlock销毁对象。


销毁对象时清除字段


在一般情况下,你不要在对象的Dispose方法中清除该对象的字段。然而,销毁对象时,应该取消该对象在生命周期内所有订阅的事件。退订这些事件避免了接收到非期望的通知--同时也避免了垃圾回收器继续对该对象保持监视。


设置一个字段用以指明对象是否销毁,以便在使用者在该对象销毁后访问该对象抛出一个ObjectDisposedException,这是非常值得做的。一个好的模式就是使用一个public的制度的属性:


public bool IsDisposed { get; private set; }
尽管技术上没有必要,但是在Dispose方法清除一个对象所拥有的事件句柄(把句柄设置为null)也是非常好的一种实践。这消除了在销毁对象期间这些事件被触发的可能性。


偶尔,一个对象拥有高度秘密,比如加密密钥。在这种情况下,那么在销毁对象时清除这样的字段就非常有意义(避免被非授权组件或恶意软件发现)。System.Security.Cryptography命令空间下的SymmetricAlgorithm类就属于这种情况,因此在销毁该对象时,调用


Array.Clear方法以清除加密密钥。


自动垃圾回收机制


无论一个对象是否需要Dispose方法以实现销毁对象的逻辑,在某个时刻,该对象在堆上所占用的内存空间必须释放。这一切都是由CLR通过GC自动处理. 你不需要自己释放托管内存。我们首先来看下面的代码


public void Test()
{
byte[] myArray = new byte[1000];
}
当Test方法执行时,在内存的堆上分配1000字节的一个数组;该数组被变量myArray引用,这个变量存储在变量栈上。当方法退出后,局部变量myArray就失去了存在的范畴,这也意味着没有引用指向内存堆上的数组。那么该孤立的数组,就非常适合通过垃圾回收机制进


行回收。


垃圾回收机制并不会在一个对象变成孤立的对象之后就立即执行。与大街上的垃圾收集不一样,.net垃圾回收是定期执行,尽享不是按照一个估计的计划。CLR决定何时进行垃圾回收,它取决于许多因素,比如,剩余内存,已经分配的内存,上一次垃圾回收的时间。这就


意味着,在一个对象被孤立后到期占用的内存被释放之间,有一个不确定的时间延迟。该延迟的范围可以从几纳秒到数天。


垃圾回收和内存占用
垃圾收集试图在执行垃圾回收的时间与程序的内存占用之间建立一个平衡。因此,程序可以占用比它们实际需要更多的内存,尤其特现在程序创建的大的临时数组。
你可以通过Windows任务管理器监视某一个进程内存的占用,或者通过编程的方式查询性能计数器来监视内存占用:
// These types are in System.Diagnostics:
string procName = Process.GetCurrentProcess().ProcessName;
using (PerformanceCounter pc = new PerformanceCounter
("Process", "Private Bytes", procName))
Console.WriteLine (pc.NextValue());
上面的代码查询内部工作组,返回你当前程序的内存占用。尤其是,该结果包含了CLR内部释放,以及把这些资源让给操作系统以供其他的进程使用。



根就是指保持对象依然处于活着的事物。如果一个对象不再直接或间接地被一个根引用,那么该对象就适合于垃圾回收。


一个跟可以是:


一个正在执行的方法的局部变量或参数(或者调用栈中任意方法的局部变量或参数)
一个静态变量
存贮在结束队列中的一个对象
正在执行的代码可能涉及到一个已经删除的对象,因此,如果一个实例方法正在执行,那么该实例方法的对象必然按照上述方式被引用。


请注意,一组相互引用的对象的循环被视作无根的引用。换一种方式,也就是说,对象不能通过下面的箭头指向(引用)而从根获取,这也就是引用无效,因此这些对象也将被垃圾回收器处理。


image


Finalizers


在一个对象从内存释放之前,如果对象包含finalizer,那么finalizer开始运行。一个finalizer的声明类似构造器函数,但是它使用~前缀符号


class Test
{
    ~Test()
    {
        // finalizer logic ...
    }
}


(尽管与构造器的声明相似,finalizer不能被声明为public或static,也不能有参数,还不能调用其基类)


Finalizer是可能的,因为垃圾收集工作在不同的时间段。首先,垃圾回收识别没有使用的对象以删除该对象。这些待删除的对象如果没有Finalizer那么就立即删除。而那些拥有finalizer的对象会被保持存活并存在放到一个特殊的队列中。


在这一点上,当你的程序在继续执行的时候,垃圾收集也是完整的。而Finalizer线程却在你程序运行时,自动启动并在另外一个线程中并发执行,收集拥有Finalizer的对象到特殊队列,然后执行它们的终止方法。在每个对象的finalizer方法执行之前,它依然非常活跃


--排序行为视作一个跟对象。而一档这些对象被移除队列,并且这些对象的fainalizer方法已经执行,那么这些对象就变成孤立的对象,会在下一阶段的垃圾回收过程中被回收。


Finalizer非常有用,但它们也有一些限制:


Finalizer减缓内存分配和收集(因为GC需要追踪那些Finalizer在运行)
Finalizer延长对象及其所引用对象的生命周期(这些对象只有在下一次垃圾回收运行过程中被真正地删除)
对于一组对象,Finalizer的调用顺序是不可预测的
你不能控制一个对象的finalizer何时被调用
如果一个对象的finalizer被阻塞,那么其他对象不能处置(Finalized)
如果程序没有卸载(unload)干净,那么finalizer会被忽略
总之,finalizer在一定程度上就好比律师--一旦有诉讼那么你确实需要他们,一般你不想使用他们,除非万不得已。如果你使用他们,那么你需要100%确保你了解他们会为你做什么。


下面是实施finalizer的一些准则:


确保finalizer快速执行
绝对不要在finalier中使用阻塞
不要引用其他可finalizable对象
不要抛出异常
 
在Finalizer中调用Dispose


一个流行的模式是使finalizer调用Dispose方法。这么做是有意义的,尤其是当清理工作不是紧急的,并且通过调用Dispose加速清理;那么这样的方式更多是一个优化,而不是一个必须。


下面的代码展示了该模式是如何实现的


class Test : IDisposable
{
public void Dispose() // NOT virtual
{
Dispose (true);
GC.SuppressFinalize (this); // Prevent finalizer from running.
}
protected virtual void Dispose (bool disposing)
{
if (disposing)
{
// Call Dispose() on other objects owned by this instance.
// You can reference other finalizable objects here.
// ...
}
// Release unmanaged resources owned by (just) this object.
// ...
}
˜Test()
{
Dispose (false);
}
}


Dispose方法被重载,并且接收一个bool类型参数。而没有参数的Dispose方法并没有被声明为virtual,只是在该方法内部调用了带参数的Dispose方法,且传递的参数的值为true。


带参数的Dispose方法包含了真正的处置对象的逻辑,并且它被声明为protected和virtual。这样就可以保证其子类可以添加自己的处置逻辑。参数disposing标记意味着它在Dispose方法中被正确的调用,而不是从finalizer的最后采取模式所调用。这也就表明,如果调


用Dispose时,其参数disposing的值如果为false,那么该方法,在一般情况下,都会通过finalizer引用其他对象(因为,这样的对象可能自己已经被finalized,因此处于不可预料的状态)。这里面涉及的规则非常多!当disposing参数是false时,在最后采取的模式中


,仍然会执行两个任务:


释放对操作系统资源的直接引用(这些引用可能是因为通过P/Invoke调用Win32 API而获取到)


删除由构造器创建的临时文件


为了使这个模式更强大,那么任何会抛出异常的代码都应包含在一个try/catch代码块中;而且任何异常,在理想状态下,都应该被记录。此外,这些记录应当今可能既简单又强大。


请注意,在无参数的Dispose方法中,我们调用了GC.SuppressFinalize方法,这会使得GC在运行时,阻止finalizer执行。从技术角度讲,这没有必要,因为Dispose方法必然会被重复调用。但是,这么做会改进性能,因为它允许对象(以及它所引用的对象)在单个循环


中被垃圾回收器回收。


复活


假设一个finalizer修改了一个活的对象,使其引用了一个“垂死”对象。那么当下一次垃圾回收发生时,CLR会查看之前垂死的对象是否确实没有任何引用指向它--从而确定是否对其执行垃圾回收。这是一个高级的场景,该场景被称作复活(resurrection)。


为了证实这点,假设我们希望创建一个类管理一个临时文件。当类的实例被回收后,我们希望finalizer删除临时文件。这看起来很简单


复制代码
public class TempFileRef
{
public readonly string FilePath;
public TempFileRef (string filePath) { FilePath = filePath; }


~TempFileRef() { File.Delete (FilePath); }
}


实际,上诉代码存在bug,File.Delete可能会抛出一个异常(引用缺少权限,或者文件处于使用中) 。这样的异常会导致拖垮整个程序(还会阻止其他finalizer执行)。我们可以通过一个空的catch代码块来“消化”这个异常,但是这样我们就不能获取任何可能发生的错误


。 调用其他的错误报告API也不是我们所期望的,因为这么做会加重finalizer线程的负担,并且会妨碍对其他对象进行垃圾回收。 我们期望显示finalization行为简单、可靠、并快速。


一个好的解决方法是在一个静态集合中记录错误信息:


public class TempFileRef
{
static ConcurrentQueue<TempFileRef> _failedDeletions
= new ConcurrentQueue<TempFileRef>();
public readonly string FilePath;
public Exception DeletionError { get; private set; }
public TempFileRef (string filePath) { FilePath = filePath; }
~TempFileRef()
{
try { File.Delete (FilePath); }
catch (Exception ex)
{
DeletionError = ex;
_failedDeletions.Enqueue (this); // Resurrection
}
}
}


把对象插入到静态队列_failedDeletions中,使得该对象处于引用状态,这就确保了它仍然保持活着的状态,直到该对象最终从队列中出列。


GC.ReRegisterForFinalize


一个复活对象的finalizer不会再次运行--除非你调用GC.ReRegisterForFinalize


在下面的例子中,我们试图在一个finalizer中删除一个临时文件。但是如果删除失败,我们就重新注册带对象,以使其在下一次垃圾回收执行过程中被回收。


public class TempFileRef
{
public readonly string FilePath;
int _deleteAttempt;
public TempFileRef (string filePath) { FilePath = filePath; }
~TempFileRef()
{
try { File.Delete (FilePath); }
catch
{
if (_deleteAttempt++ < 3) GC.ReRegisterForFinalize (this);
}
}
}


如果第三次尝试失败后,finalizer会静悄悄地放弃删除临时文件。我们可以结合上一个例子增强该行为--换句话说---那就是在第三次失败后,把该对象加入到_failedDeletions队列中。


垃圾回收工作原理


标准的CLR使用标记和紧凑的GC对存储托管堆上的对象执行自动内存管理。GC可被视作一个可被追踪的垃圾回收器,在这个回收器中,它(GC)不与任何对象接触;而是被间歇性地被唤醒,然后跟踪存储在托管堆对象图,以确定哪些对象可以被视为垃圾,进而对这些对象


执行垃圾回收。


当(通过new关键字)执行内存分配是,或当已经分配的内存达到了某一阀值,亦或当应用程序占用的内存减少时,GC启动一个垃圾收集。这个过程也可以通过手动调用System.GC.Collect方法启动。在一个垃圾回收过程中,所有线程都可能被冻结。


GC从根对象引用开始,查找贵根对象对应的整个对象图,然后把所有的对象标记为可访问的对象。一旦这个过程完成,所有被标记为不再使用的对象,将被垃圾回收器回收。


没有finalizer的不再使用的对象立即被处置;而拥有finalizer的不再使用对象将会在GC完成之后,在finalizer线程上排队以等待处理。这些对象(在finalizer线程上排队的对象)会在下一次垃圾回收过程中被回收(除非它们又复活了)。


而那些剩余的“活”对象(还需要使用的对象),被移动到堆叠开始位置(压缩),这样以腾出更多空间容纳更多对象。改压缩过程有两个目的:其一是避免了内存碎片,这样就使得在为新对象分配空间后,GC只需使用简单的策略即可,因为新的对象总是分配在堆的尾


部。其二就是避免了维护一个非常耗时的内存片段列表任务。


在执行完一次垃圾回收之后,为新对象分配内存空间时,如果没有足够的空间可以使用,操作系统不能确保更多的内存使用时,抛出OutOfMemoryException。


优化技术


GC引入了各种优化技术来减少垃圾回收的时间。


通用垃圾回收


最重要的优化就是垃圾回收时通用的。其优点是:尽管快速分配和处置大量对象,某些对象是长存内存,因此他们不需要被垃圾回收追踪。


基本上,GC把托管堆分为三类:Gen0是在堆上刚刚分配的对象;Gen1经过一次垃圾回收后仍然存活的对象;剩余的为Gen2。


CLR限制Gen0的大小(在32位CLR中,最大16MB,一般大小为数百KB到几MB)。当Gen0空间耗尽,GC便触发一个Gen0垃圾回收--该垃圾回收发生非常频繁。对于Gen1,GC也应用了一个相似的大小限制,因为Gen1垃圾回收也是相当频繁并且快速完成。Gen2包含了所有类型的


垃圾回收,然而,发生在Gen2的垃圾回收执行时间长,并且也不会经常发生。下图展示了一个完全垃圾回收:


image


如果真要列出一组大概的数字,那么Gen0垃圾回收执行耗费少于1毫秒,在一个应用程序中一般不会被注意到。而全垃圾回收,如果程序包含大的图形对象,则可能会耗费100毫秒。执行时间受诸多因素影响二次可能会有不同,尤其是Gen2的垃圾回收,它的尺寸是没有限


定的。


段时间存活的对象,如果使用GC会非常有效。比如下面示例代码中的StringBuilder,就会很快地被发生在Gen0上的垃圾回收所回收。


string Foo()
{
var sb1 = new StringBuilder ("test");
sb1.Append ("...");
var sb2 = new StringBuilder ("test");
sb2.Append (sb1.ToString());
return sb2.ToString();
}


大对象堆


GC为大对象(大小超过85,000字节)使用单独的堆。这就避免了大量消耗Gen0堆。因为在Gen0上没有大对象,那么就不会出现分配一组16MB的对象(这些对象由大对象组成)之后,马上触发垃圾回收。


大对象堆不适合于压缩,这是因为发生垃圾回收时,移动内存大块的代价非常高。如果这么做,会带来下面两个后果:


内存分配低效,这是因为GC不能总是把对象分配在堆的尾部,它还必须查看中间的空隙,那么这就要求维护一个空白内存块链表。
大对象堆适合于片段化。这意味着冻结一个对象,会在大对象堆上生成一个空洞,这个空洞很难在再被填充。比如,一个空洞留下了86000字节的空间,那么这个空间就只能被一个85000字节或86000自己的对象填充(除非与另外的一个空洞连接在一起,形成更大的空间)
大对象堆还是非通用的堆,大对象堆上的所有对象被视作Gen2


并发回收和后台回收


GC在执行垃圾回收时,必须释放(阻塞)你的程序所使用的线程。在这个期间包含了Gen0发生的时间和Gen1发生的时间。


由于执行Gen2回收可能占用较长的时间,因此GC会在你的程序运行时,堆Gen2回收进行特殊的尝试。该优化技术仅应用于工作站的CLR平台,一般应用于windows桌面系统(以及所有运行独立程序的Windows)。原因是由于阻塞线程进行垃圾回收所带来的延迟对于没有用户


接口的服务器应用程序一般不会带来问题。


这种对于工作站的优化历史上称之为并发回收。从CLR4.0kaishi ,它发生了革新并重命名为后台回收。后台回收移除了一个限制,由此,并发回收不再是并发的,如果Gen0部分已经执行完而Gen2回收还正在执行。这就意味着,从CLR4.0开始,持续分配内存的应用程序会


更加敏感。


GC通知(适用于服务端CLR)


从Framework 3.5 SP1开始,服务器版本的CLR在一个全GC将要发生时,向你发送通知。你可以在服务器池配置中配置该特性:在一个垃圾回收执行之前,把请求转向到另外一台服务器。然后你立即调查垃圾回收,并等待其完成,在垃圾回收执行完成之后,把请求转回到


当前服务器。


通过调用GC.RegisterForFullGCNotification,可以启用GC通知。然后,启动另外一个线程,该线程首先调用GC.WaitForFullGCApproach,当该方法返回GCNotificationStatus指明垃圾回收已经进入等待执行的队列,那么你就可以把请求转向到其他的服务器,然后手执


行一次手动垃圾回收(见下节)。然后,你调用GC.WaitForFullGCComplete方法,当该方法返回时,GC完成;那么该服务器就可以开始再次接收请求。然后在有需要的时候,你可以再次执行上述整个过程。


强制垃圾回收


通过调用GC.Collect方法,你可以随时手动强制执行一次垃圾回收。调用GC.Collect没有提供任何参数会执行一次完全垃圾回收。如果你提供一个整数类型的参数,那么执行对应的垃圾回收。比如GC.Collect(0)执行Gen0垃圾回收。


// Forces a collection of all generations from 0 through Generation.
//
public static void Collect(int generation) {
    Collect(generation, GCCollectionMode.Default)
}




// Garbage Collect all generations.
//
[System.Security.SecuritySafeCritical]  // auto-generated
public static void Collect() {
    //-1 says to GC all generations.
    _Collect(-1, (int)InternalGCCollectionMode.Blocking);
}


一般地,允许GC去决定何时执行垃圾回收可以得到最好的性能;这是因为强制垃圾回收会把Gen0的对象不必要地推送到Gen1(Gen1不必要地推送到Gen2),从而影响性能。这还会扰乱GC自身的调优能力--在程序运行时,GC动态地调整每种垃圾回收的临界值以最大限度地


提高性能。


但是,也有另外。最常见的可以执行手动垃圾回收的场景就是当一个应用程序进入休眠状态,比如执行日常工作的windows服务。这样的程序可能使用了System.Timters.Timer以每隔24小时触发一次行为。当该行为完成之后,在接着的24小时之内没有任何代码会执行,那


就意味着,在这段时间内,不会分配任何内存,因此GC就没有机会被激活。服务在执行时所消耗的任何内存,在接着的24小时都会被持续占用--甚至是空对象图。那么解决方法就是在日常的行为完成之后调用GC.Collect()方法进行垃圾回收。


为了回收由于finalizer延迟回收的对象,你可以添加一行额外的代码以调用WaitForPendingFinalizers,然后再调用一次垃圾回收


GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
另外一种调用GC.Collect方法的场景是当你测试一个有Finazlier的类时。
 
内存压力


.NET运行时基于一些列因素决定何时启动垃圾回收,其中一个因素就是机器内存的总负载。 如果程序使用了非托管内存,那么运行时会对其内存的使用情况持盲目地乐观的态度,这是因为CLR之关心托管内存。通过告诉CLR已经分配了特定量的非托管内存内存,来减轻


CLR的盲目性;调用CG.AddMemoryPresure方法可以完成该目的。如果取消该行为(当所占用的托管内存已经被释放),那么可以调用GC.RemoveMemoryPressure。


管理内存泄漏


在非托管语言中,比如C++,你必须记住当对象不再使用时,应手动地释放内存;否则,将导致内存泄漏。在托管世界中,内存泄漏这种错误时不可能发生的,这归功于CLR的自动垃圾回收。


尽管如此,大型的和复杂的.NET程序也会出现内存泄漏;只不错内存泄漏的方式比较温和,但具有相同的症状和结果:在程序的生命周期内,它消耗越来越多的内存,到最后导致程序重启。好消息是,托管内存泄漏通常容易诊断和预防。


托管内存泄漏是由不再使用的活对象引起,这些对象之所以存活是凭借不再使用引用或者被遗忘的引用。一种常见的例子就是事件处理器--它们堆目标对象保存了一个引用(除非目标是静态方法)。比如,下面的类:


复制代码
class Host
{
public event EventHandler Click;
}
class Client
{
Host _host;
public Client (Host host)
{
_host = host;
_host.Click += HostClicked;
}
void HostClicked (object sender, EventArgs e) { ... }
}
复制代码
下面的测试类包含1个方法实例化了1000个Client对象


复制代码
class Test
{
static Host _host = new Host();
public static void CreateClients()
{
Client[] clients = Enumerable.Range (0, 1000)
.Select (i => new Client (_host))
.ToArray();
// Do something with clients ...
}
}
复制代码
你可能会认为,当CeateClients方法结束后,这个1000个Client对象理解适用于垃圾回收。很不幸,每个Client对象都包含一个引用:_host对象,并且该对象的Click事件引用每个Client实例。 如果Click事件不触发,那么就不会引起注意,或者HostClicked方法不做任


何事情也不会引起注意。


解决这个问题的一种方式就是使Client类实现接口IDisposable,并且在dispose方法中,移除时间处理器


public void Dispose() { _host.Click -= HostClicked; }
Client实例的使用者,在使用完实例之后,调用Client类的dispose方法处置该实例


Array.ForEach (clients, c => c.Dispose());
下面的对比展示两种方式的差别


CLR Profiler
Index 实现IDisposable 未实现IDisposable
Time line image image
Heap statistics image image
GC Generatation Sizes image image
 
计时器


不要忘记timmers也会引起内存泄漏。根据计时器的种类,会引发两种不同的内存泄漏。首先我们来看System.Timers命名空间下的计时器。在下面的例子中,Foo类每秒调用一次tmr_Elapsed方法


复制代码
using System.Timers;
class Foo
{
Timer _timer;
Foo()
{
_timer = new System.Timers.Timer { Interval = 1000 };
_timer.Elapsed += tmr_Elapsed;
_timer.Start();
}
void tmr_Elapsed (object sender, ElapsedEventArgs e) { ... }
}
复制代码
很不幸,Foo的实例决定不会被回收。原因在于.NET Framework本身持有对计活动的时器的引用,从而导致.net framework会触发这些计时器的Elapsed事件。因此


.NET Framework将使_timer处于活动状态
通过tmr_Elapsed事件处理器,_timer将使Foo实现处于活动状态
当你意识到Timer实现了IDisposable接口之后,解决的方法就在也明显不过了。处置Timer实例以停止计时器,并确保.NET Framework不再引用该计时器对象。


class Foo : IDisposable
{
...
public void Dispose() { _timer.Dispose(); }
}
相对于我们上面讨论的内容,WPF和Windows窗体的计时器表现出完全相同的方式。


然而,System.Threading命名空间下的计时器确是一个特例。.NET Framework没有引用活动线程计时器;想法,却直接引用回调代理。这就意味着如果你忘记处置线程计时器,那么finalizer会自动触发并停止计时器然后处置该计时器。比如:


复制代码
static void Main()
{
var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000);
GC.Collect();
System.Threading.Thread.Sleep (10000); // Wait 10 seconds
}
static void TimerTick (object notUsed) { Console.WriteLine ("tick"); }
复制代码
如果上面的代码编译为发布模式,那么计时器会被回收,并且在它再次触发之前被处置(finalized)。同样地,我们可以在计时器结束后通过处置该计数器以修复这个问题


using (var tmr = new System.Threading.Timer (TimerTick, null, 1000, 1000))
{
GC.Collect();
System.Threading.Thread.Sleep (10000); // Wait 10 seconds
}
using语句会隐式地调用tmr.Dispose方法,以确保tmr变量确实处于“使用(活动状态)”;因此不会在代码块结束之前被当作是死对象。讽刺的是,调用Dispose方法实际上使对象存活的时间更长了。


诊断内存泄漏


避免托管内存泄漏的最简单方式就是在编写应用程序时就添加监控内存占用。你可以在程序中通过调用下面的代码来获取当前内存的使用情况


long memoryUsed = GC.GetTotalMemory (true);
如果你采取测试驱动开发,那么你可以使用单元测试判断是否按照期望释放了内存。入股这样的判断失败,那么接着你就应该检查你最近对程序所作的修改。


如果你已经有一个大型程序,并且该程序存在托管内存泄漏问题,那么你应该使用windgb.exe工具来帮助你解决问题。当然你还可以使用其他的图形化工具,比如CLR Profiler, SciTech的Memory Profiler,或者Red Gate的ANTS Memory Profiler。


弱引用


有时候,引用一个对GC而言是“隐形”的对象,并且对象保持活动状态,这非常有用。这既是弱引用,它由System.WeakReference类实现。使用WeakReference,使用其构造器函数并传入目标对象。


var sb = new StringBuilder ("this is a test");
var weak = new WeakReference (sb);
Console.WriteLine (weak.Target); // This is a test
如果目标对象仅仅由一个或多个弱引用所引用,那么GC会把其加入到垃圾回收队列中。如果目的对象被回收,那么WeakReference的Target属相则为NULL。


var weak = new WeakReference(new StringBuilder("weak"))
Console.WriteLine(weak.Target); // weak
GC.Collect();
Console.WriteLine(weak.Target == null); // (true)
为了避免目标对象在测试其为null和使用目标对象之间被回收,把目标对象分配给一个局部变量


var weak = new WeakReference (new StringBuilder ("weak"));
var sb = (StringBuilder) weak.Target;
if (sb != null) { /* Do something with sb */ }
一旦目标对象分配给一个局部变量,那么目的对象就有了一个强类型根对象,从而在局部变量使用期间不会被回收。


下面例子中的类通过弱引用追踪所有被实例化的Widget对象,从而使这些实例不会被回收
class Widget
{
static List<WeakReference> _allWidgets = new List<WeakReference>();
public readonly string Name;
public Widget (string name)
{
Name = name;
_allWidgets.Add (new WeakReference (this));
}
public static void ListAllWidgets()
{
foreach (WeakReference weak in _allWidgets)
{
Widget w = (Widget)weak.Target;
if (w != null) Console.WriteLine (w.Name);
}
}
}

这样一个系统的唯一缺点就是,静态列表会随着时间推移而增加,逐渐累积对应null对象的弱引用。因此,你需要自己实现一些清理策略。

弱引用和缓存

使用弱引用的目的之一是为了缓存大对象图。通过弱引用,使得耗费内存的数据可以进行简要的缓存而不是造成内存的大量占用。

_weakCache = new WeakReference (...); // _weakCache is a field
...
var cache = _weakCache.Target;
if (cache == null) { /* Re-create cache & assign it to _weakCache */ }
在实际上,该策略只会发挥一半的作用,这是因为你不能控制GC何时运行,并且也不能控制GC会会执行哪一类回收。尤其是,当你的缓存是在Gen0中,那么这类内存会在微妙级别类被回收。因此,至少,你需要使用两类缓存,通过它们,首先你拥有一个强类型,然后不

时地把该强类型转换成弱类型。


弱引用和事件


在前面的章节中,我们看到事件是如何引起内存泄漏。而且解决这种内存泄漏的最简单方法是避免时间订阅,或者对为订阅事件的对象实现Dispose方法。此外,弱引用也提供了另外一种解决方案。


假设一个带来对其目标持有一个弱引用。那么这样的一个代理并不会使其目标为活动状态,除非这些目标对象有独立的引用。当然,这并不会阻止一个被触发的代理,在目标对象进入回收队列之后但在GC开始对该目标对象执行回收前的时间段中,击中一个未被引用的目


标。为了该方法高效,你的代码必须非常稳定。下面的代码就是就是采用这种方式的具体实现:


复制代码
public class WeakDelegate<TDelegate> where TDelegate : class
{
class MethodTarget
{
public readonly WeakReference Reference;
public readonly MethodInfo Method;
public MethodTarget (Delegate d)
{
Reference = new WeakReference (d.Target);
Method = d.Method;
}
}
List<MethodTarget> _targets = new List<MethodTarget>();
public WeakDelegate()
{
if (!typeof (TDelegate).IsSubclassOf (typeof (Delegate)))
throw new InvalidOperationException
("TDelegate must be a delegate type");
}
public void Combine (TDelegate target)
{
if (target == null) return;
foreach (Delegate d in (target as Delegate).GetInvocationList())
_targets.Add (new MethodTarget (d));
}
public void Remove (TDelegate target)
{
if (target == null) return;
foreach (Delegate d in (target as Delegate).GetInvocationList())
{
MethodTarget mt = _targets.Find (w =>
d.Target.Equals (w.Reference.Target) &&
d.Method.MethodHandle.Equals (w.Method.MethodHandle));
if (mt != null) _targets.Remove (mt);
}
}
public TDelegate Target
{
get
{
var deadRefs = new List<MethodTarget>();
Delegate combinedTarget = null;
foreach (MethodTarget mt in _targets.ToArray())
{
WeakReference target = mt.Reference;
if (target != null && target.IsAlive)
{
var newDelegate = Delegate.CreateDelegate (
typeof (TDelegate), mt.Reference.Target, mt.Method);
combinedTarget = Delegate.Combine (combinedTarget, newDelegate);
}
else
deadRefs.Add (mt);
}
foreach (MethodTarget mt in deadRefs) // Remove dead references
_targets.Remove (mt); // from _targets.
return combinedTarget as TDelegate;
}
set
{
_targets.Clear();
Combine (value);
}
}
}
复制代码
上述代码演示了许多C#和CLR的有趣的地方。首先,我们在构造器中检查了TDelegate是一个代理类型。这是因为C#本身的限制--因为下面的语句不符合C#的语法


... where TDelegate : Delegate // Compiler doesn't allow this
由于必须要进行类型限制,所以我们在构造器中执行运行时检查。


在Combine方法和Remove方法中,我们执行了引用转换,通过as运算符(而没有使用更常见的转换符)把target对象转换成Delegate类型。这是由于C#不允许转换符使用类型参数--因为它不能分清这是一个自定义的转换还是一个引用抓换(下面的代码不能拖过编译)。


foreach(Delegate d in ((Delegate)target).GetInvocationList())
                _targets.Add(new MethodTarget(d));
当调用GetInvocationList,由于这些方法可能被一个多播代理调用,多播代理就是一个代理有多余一个的方法接收。


对于Target属性,我们使其为一个多播代理--通过一个弱引用包含所有的代理引用,从而使其目标对象保持活动。然后我们清楚剩余的死引用,这样可以避免_targets列表无限制的增长。下面的代码演示了如何使用我们上面创建的实现了事件的代理类:


复制代码
public class Foo
{
WeakDelegate<EventHandler> _click = new WeakDelegate<EventHandler>();
public event EventHandler Click
{
add { _click.Combine (value); } remove { _click.Remove (value); }
}
protected virtual void OnClick (EventArgs e)
{
EventHandler target = _click.Target;
if (target != null) target (this, e);
}
}
复制代码
请注意,在触发事件时,在检查和调用之前,我们把_click.Target对象赋值给一个临时变量。这就避免了目标对象被GC回收的可能性。


参考


http://msdn.microsoft.com/en-US/library/system.idisposable.aspx


========

关于C#中垃圾回收GC杂谈

http://blog.csdn.net/pan869823184/article/details/19299581


在初学阶段用.Net编写程序时,一直都未曾考虑过程序垃圾资源回收率的问题,那是因为老师老在课堂讲什么不用管,不用理会,一听到不用理会,好吧,从此写程序就肆无忌惮的了!程序卡死、内存暴涨、顺便偶尔来几个内存错误,一看到这个就头大了。现在想想,


课堂老师讲的那句话,却只听进了前半句。。。
闲聊无事,也不用再怕什么在职防止泄露啥啥机密、啥啥技术的、、、嘎嘎、、、、(下面的纯属个人观点,如有雷同、敬请绕道、、、)
在.Net里面垃圾收集的工作方式:


运行.NET应用程序时,程序创建出来的对象实例都会被CLR跟踪,CLR都是有记录哪些对象还会被用到(存在引用关系);哪些对象不会再被用到(不存在引用关系)。CLR会整理不会再被用到的对象,在恰当的时机,按一定的规则销毁部分对象,释放出这些对象所占用的


内存。


说到这里,那就引出了新的技术点:


CLR是怎么记录对象引用关系的?


CLR会把对象关系做成一个“树图”,这样标记他们的引用关系


CLR是怎么释放对象的内存的?


关键的技术是:CLR把没用的对象转移到一起去,使内存连续,新分配的对象就在这块连续的内存上创建,这样做是为了减少内存碎片。注意!CLR不会移动大对象


垃圾收集器按什么规则收集垃圾对象?


CLR按对象在内存中的存活的时间长短,来收集对象。时间最短的被分配到第0代,最长的被分配到第2代,一共就3代。


一般第0贷的对象都是较小的对象,第2代的对象都是较大的对象,第0代对象GC收集时间最短(毫秒级别),第2代的对象GC收集时间最长。当程序需要内存时(或者程序空闲的时),GC会先收集第0代的对象,


收集完之后发现释放的内存仍然不够用,GC就会去收集第1代,第2代对象。(一般情况是按这个顺序收集的)


如果GC跑过了,内存空间依然不够用,那么就抛出了OutOfMemoryException异常。


GC跑过几次之后,第0代的对象仍然存在,那么CLR会把这些对象移动到第1代,第1代的对象也是这样。


既然有了垃圾收集器,为什么还要Dispose方法和析构函数?


因为CLR的缘故,GC只能释放托管资源,不能释放非托管资源(数据库链接、文件流等)。


那么该如何释放非托管资源呢?


一般我们会选择为类实现IDispose接口,写一个Dispose方法。


让调用者手动调用这个类的Dispose方法(或者用using语句块来自动调用Dispose方法)


Dispose执行时,析构函数和垃圾收集器都还没有开始处理这个对象的释放工作


有时候,我们不想为一个类型实现Dispose方法,我们想让他自动的释放非托管资源。那么就要用到析构函数了。


析构函数是个很奇怪的函数,调用者无法调用对象的析构函数,析构函数是由GC调用的。


你无法预测析构函数何时会被调用,所以尽量不要在这里操作可能被回收的托管资源,析构函数只用来释放非托管资源


GC释放包含析构函数的对象,比较麻烦(需要干两次才能干掉她),


CLR会先让析构函数执行,再收集它占用的内存。


我们需要手动执行垃圾收集吗?什么场景下这么做?


GC什么时候执行垃圾收集是一个非常复杂的算法(策略)


大概可以描述成这样:


如果GC发现上一次收集了很多对象,释放了很大的内存,


那么它就会尽快执行第二次回收,


如果它频繁的回收,但释放的内存不多,


那么它就会减慢回收的频率。


所以,尽量不要调用GC.Collect()这样会破坏GC现有的执行策略。


除非你对你的应用程序内存使用情况非常了解,你知道何时会产生大量的垃圾,那么你可以手动干预垃圾收集器的工作 


我有一个大对象,我担心GC要过很久才会收集他,


[csharp] view plain copy print?在CODE上查看代码片派生到我的代码片
  
关于弱引用和垃圾收集之间的关系?


当一个大对象被使用后不存在引用关系时,GC就会自动回收它占用的内存。


当这个对象足够大的情况下,GC在回收它时,可能时间稍微会长点,当用户需要再次使用该对象时,我们可以从回收池中再次提取该对象,这里就涉及到弱引用,代码如下:




[csharp] view plain copy print?在CODE上查看代码片派生到我的代码片
var bss = new BsCtl(BrowserContainer);  
            var vbss = new WeakReference<BsCtl>(bss);  
            bss = null;  
            BsCtl ok;              
            vbss.TryGetTarget(out ok);  
            //如果没有进行垃圾收集OK不会为NULL  
            if (ok == null)  
            {  
                //如果已经进行了垃圾收集,就会执行这段代码  
                ok = new BsCtl(BrowserContainer);  
            }  




垃圾收集随时可以收集bss对象,


如果收集了,就会进入if语句块,如果没有收集,就不会进入if语句块,TryGetTarget(out ok)就成功把bss从垃圾堆里捞回来了。


注意:这里只说了短弱引用,没有提及长弱引用,我觉得长弱引用使用的场景较少。


垃圾收集器优点:


因为我没有很丰富的C/C++编程经验,如果想谈垃圾收集器的好处,那么势必要和C/C++这样的较低级的语言对比。所以一般性的回答都是减少内存使用不当的BUG,提升编程效率之类的问题。
========

垃圾收集器原理

http://www.tuicool.com/articles/Nbyqi2
原文  http://www.cnblogs.com/izhaogang/p/collector.html


在编程的过程中,你是否遇到过OutOfMemeryException的异常?程序在做性能测试时,应用服务器程序消耗的内存不断上升?在使用开源框架时,由于没有及时的Dispose而导致程序的异常发生?而造成这些异常的原因都是没有合理的释放内存导致的。我们不经要问,C#


框架下不是有GC(自动垃圾收集器)吗?那么为什么还会出现如此异常错误呢?GC到底何时执行,执行时又做了什么?GC对性能的影响?怎样合理的释放资源呢?下面我们来揭开垃圾收集器的神秘面纱。


1、垃圾收集平台的基本工作原理


1.1 基本原理分析


我们知道,C#是CLR(Common Language Runtime公共语言运行库)下的一种托管代码语言,它的类型和对象在应用计算机内存时,大体用到两种内存,一种叫堆栈,另一种叫托管堆。C#中主要分为值类型和引用类型,当声明一个值类型对象时,会在栈中分配适当大小的


内存,内存空间存储对象的值。其中维护一个栈指针,它包含栈中下一个可用内存空间的地址。当一个变量离开作用域时,栈指针向下移动并释放变量所占用的内存,所以它任然指向下一个可用地址;当声明一个引用类型对象时,引用变量也利用栈,但这时栈包含的只是


对另一个内存位置的引用,而不是实际的值。这个位置是托管堆中的一个地址,和栈一样,它也维护一个指针,包含堆中下一个可用内存空间的地址。我们来写一个简单的事例代码,看看它内部到底发生了什么?

namespace SourceDemo
{
	class Program
	{ 
		static void Main(string[] args)
		{
			int iTotal = 1;
			Order order = new Order();
		}
	}
	class Order
	{
	}
}


通过ILDASM.EXE工具查看对应的IL代码如下:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint					
  // Code size 10 (0xa)
  .maxstack  1  
  .locals init ([0] int32 iTotal,
	  [1] class SourceDemo.Order order)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  stloc.0
  IL_0003:  newobj     instance void SourceDemo.Order::.ctor()
  IL_0008:  stloc.1
  IL_0009:  ret
} // end of method Program::Main


可以看到,声明引用类型和值类型的区别在于引用类型有一个newObj创建对象的操作。那么newObj到底做了哪些操作呢?主要操作如下:


计算新建对象所需要的内存总数(包括基类的所有字段字节总数)。
在前面所得字节总数的基础上再加上对象开销所需的字节数。开销包括:类型对象指针和同步块的索引。
CLR检查保留区域是否有足够的空间来存放新建对象。


如果空间足够,调用类型的构造函数,将对象存放在NextObjPtr指向的内存地址中。
如果空间不够,就执行一次垃圾回收来清理托管堆,如果依然不够,则抛出OutOfMemeryException异常
最后,移动NextObjPtr指向托管堆下一个可用地址。可以看到,垃圾收集器通过检查托管堆上不再使用的对象来回收内存,那么垃圾收集器怎么确定对象是不再使用的对象呢?请接着往下看。


1.2 应用程序的根


每个应用程序都有一组根,一个根就是一个存储对象,其中包含一个指向引用类型的内存指针,它或者指向托管堆的对象,或者被设为null。如类字段、方法参数或者是局部变量都是根,注意只有引用类型才被认为是根,而值类型只是占用内存永远不会被认为为根。垃


圾收集器是怎么工作的呢?工作主要分为以下两阶段:


第一阶段,标记对象阶段。 
垃圾收集器开始执行的时候,首先假设托管堆中的对象都是可以收集的垃圾。它开始遍历线程的堆栈检查所有的根,如果发现根引用了一个对象那么就在该对象的同步块的索引字段上设置一位来标记它。同时检查该对象是否引用其他对象,如果引用则进行标记,通过递


归的方式进行标记,直到发现根及根引用的对象已经标记,垃圾收集器将继续收集下一个根。
第二阶段,压缩阶段。该阶段垃圾收集器线性的遍历堆以寻找包含未标记对象的连续区块。如果垃圾收集器找到了较小内存块,那么它忽略内存不计;如果找到了较大的连续内存块,那么垃圾收集器将把内存中非垃圾对象搬移到这些连续内存块中以压缩托管堆。


图:垃圾收集器执行前的托管堆。


对于以上描述,专业词汇较多不是蛮好理解。我们来举一个容易理解的例子:有一个执行清理房间的任务(任务方法),房间中有很多物品、柜子盒子及其里面的物品等都需要清理(对象清理),当我们执行这个任务时(调用方法),清理过程中我们可以标记物品,同


时可能存在这样的情况,我们在清理其中一个盒子的时候,发现盒子里面还有其他的盒子,如手机盒子里面还有个装充电器的盒子(手机里面又引用了手机充电器的对象),那么我们需要深度遍历清理标记所有的盒子,遍历完成后,我们会发现,有很多以前有用现在无


用的东西,如老式的手机充电器、数据线等;废旧的电池等(不在使用,不可达对象);这样我们会根据当时的情况将不再使用对象进行清理处理。而垃圾清理器大概就是做这样的工作,只是它处理的方式及细节更加复杂。


1.3 对象的代


当CLR试图寻找不可达对象的时候,它需要遍历托管堆上的对象。随着程序的运行,托管堆上的对象也越来越多,如果要对整个托管堆进行垃圾回收,那么会严重的影响性能。为了优化这个过程,CLR中使用了"代"的概念,托管堆上的每一个对象都被指定属于某个“代”


(generation)。


托管堆上的对象可以分为0、1、2三个代:


0代:新构建的对象,垃圾收集器还没对它们执行任何检查
1代:在一次垃圾收集清理没有被回收的对象
2代:在至少两次垃圾收集清理没有被回收的对象。
下面我们来看看CLR如何通过这种机制来优化垃圾收集机制的性能?


图:垃圾收集代策略执行过程


CLR初始化时,它会为每一代选择一个预算容量,假设为0代为256KB,1代为2M,2代为10M(实际可能不同),如果分配的新对象导致代容量超过预算容量,那么将执行垃圾收集清理操作。如上图所示:


1、垃圾回收前,托管堆中对象ABCDE都处于第0代;
2、假设ABCDE已占用256K内存,当需要创建新对象F时,开始执行垃圾回收,垃圾收集器判断CE为不可达对象,将对他们进行清理,完成后,对象ABD将变为1代对象;
3、现在需创建FGHIJ对象,它们将都处于0代;这个时候1代对象中的B可能不再被调用变为不可达对象。这里面会发现:当0代对象内存不超过256KB时,垃圾回收器不会对1代对象进行检查清理,因此1代中不可达对象B在垃圾清理后依旧会保留在内存中。那么什么时候B会


被清理呢?
4、创建新对象,发现1代空间操作预算容量2M,这个时候将引发垃圾收集,回收不可达对象BH,同时原有1代对象AD变为2代,0代对象FGIJ变为1代。
下面我们来通过事例代码验证上述的执行步骤:


internal sealed class GenObj {
    ~GenObj()
    {
Console.WriteLine("Finalize GenObj");
    }
}
class Program
{
    static void Main(string[] args)
    {
Console.WriteLine("Maxnum gen:" + GC.MaxGeneration);
//创建一个新的对象
object o = new GenObj();
//因为是新创建的对象,为0代
Console.WriteLine("Gen "+GC.GetGeneration(o));
//执行垃圾收集提升对象的代
GC.Collect();
Console.WriteLine("Gen " + GC.GetGeneration(o));
//这里强制回收
GC.Collect();
Console.WriteLine("Gen " + GC.GetGeneration(o));
GC.Collect();
Console.WriteLine("Gen " + GC.GetGeneration(o));
o = null;
Console.WriteLine("Collecting Gen 0");
GC.Collect(0);
GC.WaitForPendingFinalizers();
Console.WriteLine("Collecting Gen 0 1");
GC.Collect(1);
GC.WaitForPendingFinalizers();
Console.WriteLine("Collecting Gen 0 1 2");
GC.Collect(2);
GC.WaitForPendingFinalizers();
Console.ReadLine();
    }  
}
程序的返回结果为:
Maxnum gen: 2
Gen 0
Gen 1
Gen 2
Gen 2
Collecting Gen 0
Collecting Gen 0 1
Collecting Gen 0 1 2
Finalize GenObj
这里需要注意的时:
GC.Collect()  强制对所有代码进行即时回收
GC.Collect(int Generation) 强制对0代到指定代对象进行回收
因此:我们将代码第二处的GC.Collect()修改为:GC.Collect(0) 
程序的返回结果则变为:
Maxnum gen: 2
Gen 0
Gen 1
Gen 1     //注意这里变为1 而不是2
Gen 2
Collecting Gen 0
Collecting Gen 0 1
Collecting Gen 0 1 2
Finalize GenObj
由此可以看出,垃圾回收机制通过引入代的机制,由遍历整个托管堆对象变成遍历少量的对象来达到性能优化的目的。当然,不仅如此,它还有其他的策略来进行性能优化。


策略1:根据回收存货的比例高低来调整预算容量。如果垃圾收集器发现0代对象被收集以后存活下来的对象很少,它可能会决定将第0代的预算容量从256K减少到128K。已分配空间的减少意味着垃圾收集器执行的频率更高,但每次收集工作会减少,这样一来进程的工作集


会变小;如果发现0代对象被手机以后存货下来的对象很多,也就是说没有回收较多的内存,那么可能决定将预算容量从256K增加到512K,这样回收的频率降低,执行回收的内存较多。

策略2:大对象回收特殊机制。大对象是指任何占用内存等于或超过85000字节的对象,将会总被认为为2代对象。原因是:该堆中的对象的终结和内存释放和小对象相同,但是它永远不会被压缩。因为将85000字节的内存块搬移到堆中要浪费很多的CPU时间。


对于这样的场景,我们来举一个更易懂的例子,一个公司业绩下滑,需要通过裁员来减清负担,开始时决定全公司范围裁员,结果搞的人心惶惶,人人自危,极大的影响了员工士气和工作效益(就好比遍历整个堆栈导致性能不佳);然后公司管理层决定优化这个方案,

将裁员的人员定为刚进公司1年的新员工(0代),因为这样有一定的好处,不仅人少执行效率快,赔的钱少而且对业务影响也较小;经过这次风波后(资源清理),随着市场行情的提升,业绩越来越好,结果又持续招人(新对象创建),但是过了1年,由于XXX原因,效

益大幅下降(内存、性能等下降),又要开始裁员(引发系统清理),规则还是按照上一次的规则,只是之前上一年没有裁掉的新员工(0代),他们在今年不再是一年级的新员工(上升为1代),在裁员的时候,发现只是裁新员工(部分表现不佳的)还不够,那么需要


对去年新员工(1代)表现差的进行裁员(0代超过预算容量则开始检查1代)。这个例子可能不是很符合现实,但是它可以体现出代的思想。


1.4 小结及启发


通过上面的介绍,我们来回顾总结一下。我们可以学习到什么?我们大概能知道垃圾收集器是如何工作的,是如何高性能的工作的。我们不仅要会使用它,我们还需要知道它的原理是什么,这样当你遇到它,你就不会觉得它有多么的神秘,不仅如此,更重要的是,里面


有很多思想是我们可以借鉴的,因为这些思想都是行内权威人士智慧的结晶。通过了解,我们还可以举一反三学习到:


程序中根的标记递归算法,这不正是深度优先的算法吗?这个算法在很多场景都在使用,比如说搜索中的爬虫程序、图的遍历、最优最快路径等等应用场景都会用到;垃圾收集器通过引进“代”的概念来进行性能优化的策略原理。我们在项目的开发过程中,也有很多应

用场景都可以借鉴这种思路来进行性能优化。比如说:现在很多大并发场景如12306、秒杀、购物网站等使用的排队机制,它们可以智能的设置队列容量,这不正是很好的体现吗?在比如说多级缓存系统系统缓存策略等等,这些应用场景都可以借鉴该思想。

2、资源清理Finalize、Dispose、Using用法说明

2.1 Finalize使用及原理说明


终结(Finalization)是CLR提供的一种机制,他允许对象在垃圾回收其内存之前执行一些清理工作,回收它占用的资源(内存、本地资源等),当垃圾收集器判定一个对象为可收集垃圾时,它会通过该对象的Finalize方法来执行清理。C#中是通过在类名称前加一个波浪


线~来定义的,这也就是我们所说的析构函数。通过ILDASM.EXE工具查看上面跟部分的GenObj类,打开确实可以发现Finalze方法。那么我们来看看Finalize的工作原理是什么?


.method family hidebysig virtual instance void 
    Finalize() cil managed
{
// Code size 25 (0x19)


.maxstack 1

.try

{

IL_0000: nop

IL_0001: ldstr "Finalize GenObj"

IL_0006: call void [mscorlib]System.Console::WriteLine(string)

IL_000b: nop

IL_000c: nop
IL_000d: leave.s IL_0017

} // end .try

finally


{


IL_000f: ldarg.0


IL_0010: call instance void [mscorlib]System.Object::Finalize()


IL_0015: nop


IL_0016: endfinally


} // end handler


IL_0017: nop


IL_0018: ret


} // end of method GenObj::Finalize


可以看到方法体的代码在Try中生成,而base.Finalize的调用则在finally中。通常Finalize方法的实现时调用Win32的CloseHandle函数,该函数接受本地资源的句柄作为参数。如:FileStream类定义了一个文件句柄字段来标示本地资源,同时也定了一个Finalize方法,


该方法内部调用CloseHandler函数并为它传递文件句柄作为参数,确保托管堆的FileStream对象成为可收集垃圾之前,本地文件句柄可以得到关闭。C#中也提供给了相应的类来进行非托管资源的清理类:


CriticalFinalizerObject类型。
它位于命名空间System.Runtime.ConstrainedExecution。CLR赋予它三个很酷的特征:1、首次构造派生于它的类型的任何对象,CLR立即对继承的层次结构中的所有Finalize方法进行JIT编译,这样在内存较小的情况下,不会影响Finalize方法因为没有内存而无法执行,

从而导致资源泄露。2、CLR在调用了非派生自CriticalFinalizerObject该类的Finalize方法后再调用派生于CriticalFinalizerObject类型的对象的Finalize方法。这样可以确保拥有Finalize方法的托管资源类可以在Finalize方法中访问派生自CriticalFinalizerObject

的对象。3、应用程序域被非法中断时,可以确保CLR调用派生自CriticalFinalizerObject类型的Finalize方法来执行资源清理。

SafeHandle类型及其派生类型。
Microsoft意识到最常用的本地资源就是有Windows提供的资源,而且Windows资源都是由句柄操作。为了使便车简单,因此提供了SafeHandle类来提供资源句柄操作,它位于命名空间System.Runtime.InteropService.它本身派生于对于CriticalFinalizerObject类型,对


于这个类的用法,具体可以查阅相关资料。


我们来看看SafeHandle类都作了什么?


[SecurityCritical, __DynamicallyInvokable, SecurityPermissio(SecurityAction.InheritanceDemand, UnmanagedCode=true)]
public abstract class SafeHandle : CriticalFinalizerObject, IDisposable
{
// Fields
private bool _fullyInitialized;
private bool _ownsHandle;
private int _state;
[ForceTokenStabilization]
protected IntPtr handle;
// Methods
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle)
{
this.handle = invalidHandleValue;
this._state = 4;
this._ownsHandle = ownsHandle;
if (!ownsHandle)
{
GC.SuppressFinalize(this);
}
this._fullyInitialized = true;
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), SecurityCritical, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public void Close()
{
this.Dispose(true);
}
[MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), SecurityCritical, __DynamicallyInvokable]
public extern void DangerousAddRef(ref bool success);
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public IntPtr DangerousGetHandle()
{
return this.handle;
}
[MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
public extern void DangerousRelease();
[SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public void Dispose()
{
this.Dispose(true);
}
[SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
this.InternalDispose();
}
else
{
this.InternalFinalize();
}
}
[SecuritySafeCritical, __DynamicallyInvokable]
~SafeHandle()
{
this.Dispose(false);
}
[MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
private extern void InternalDispose();
[MethodImpl(MethodImplOptions.InternalCall), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
private extern void InternalFinalize();
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
protected abstract bool ReleaseHandle();
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
protected void SetHandle(IntPtr handle)
{
this.handle = handle;
}
[MethodImpl(MethodImplOptions.InternalCall), SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable]
public extern void SetHandleAsInvalid();
// Properties
[__DynamicallyInvokable]
public bool IsClosed
{
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries"), __DynamicallyInvokable]
get
{
return ((this._state & 1) == 1);
}
}
[__DynamicallyInvokable]
public abstract bool IsInvalid { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), __DynamicallyInvokable] get; }
}
首先可以看到,它继承于CriticalFinalizerObject对象,这样让它具备上面提到的三个特性;它同时实现了IDisposable接口。然后通过Dispose和close释放托管资源和非托管资源。其中提供了对本地资源句柄的操作,ReleaseHandle 如果在派生类中重写,执行释放句


柄所需的代码。更多细节可以查看, https://msdn.microsoft.com/zh-cn/library/system.runtime.interopservices.safehandle.aspx


哪些时间会导致Finalize方法的调用呢?


0代对象充满 该事件是目前导致垃圾回收执行最常见的一种方式。
代码显式调用GC.Collect()方法 Micrsoft强烈建议不要这样干,但某些时候执行还是有意义的。
Windows报告内存不足
CLR卸载应用程序域
CLR被关闭
由此可以看出Finalize方法的执行不能显式调用,因此它执行时间具备不确定性。

2.2 Dispose使用

Finalize方法非常有用,它可以确保托管对象在释放内存的同时不会泄露本地资源,但是它的问题在于我们不知道何时才会调用它。在使用本地资源的托管类型时,能够确定的释放或者是关闭对象都是很有用的。要提供确定释放或者关闭对象的能力,一个类型通常需要

实现一种释放模式(DisposePattern).通过前面的SafeHandler类可以显式关闭本地资源,这是由于它实现了IDisposable接口。我们来看一下MSDN给出的Dispose释放写法。


using System;


class BaseClass : IDisposable
{
//Flag: Has Dispose already been called?bool disposed = false;


// Public implementation of Dispose pattern callable by consumers.


public void Dispose()


{


Dispose(true);


//调用GC.SuppressFinalize(this)方法来阻止Finalize方法的调用


GC.SuppressFinalize(this);


}


// Protected implementation of Dispose pattern.
protected virtual void Dispose(bool disposing)
{
if (disposed)
return; 
if (disposing) {
// 释放托管资源
//
}
// 释放非托管资源
//设置true 表示对象正在被显式的执行资源清理而不是垃圾收集器执行终结操作
disposed = true;
}
}
通过Dispose来释放资源,其实只是清理SafeHandle对象包装的资源方式之一。SafeHandle包装的资源清理还可以通过编程人员显式的调用Close、Dispose方法来清理;或者是通过垃圾收集器调用对象的Finalize方法来释放。上面给出的SafeHandle类代码实现,正好可以


说明这一点。


2.3 Using使用


前面介绍了怎样显示的调用一个类型的Dispose或者是Close方法,如果决定显式调用,那么强烈建议把他们放在一个异常处理的finally代码块中,这样可以保证它们被执行。但是这样做书写的代码很是繁琐。为了解决这个问题,C#提供了一个using语句,它简化了上述

finally的操作,并且能够得到和上述一样的效果。

3、实例分析

3.1 Windows服务关闭问题


在很多时候,我们需要编写并开启一个Windows服务来执行需要循环执行的应用需求,在开启任务后,程序顺利执行;当关闭Windows服务时,发现服务关闭了,但是服务对应的资源线程还没有完全结束,要经过一段时间后,服务才会完全停止。这很有可能是因为,在服


务Stop()方法里面,没有完全释放程序调用的资源导致的。


3.2 Redis使用遇到的问题


记得以前在使用Redis过程中遇到了一个奇怪的问题。


先附上Redis帮助类:RedisManager.cs


internal class RedisManager
{
private static readonly PooledRedisClientManager _Manager;
static RedisManager()
{
_Manager = GetManager();
}
public static PooledRedisClientManager Manager
{
get
{
return _Manager;
}
}
#region Help Methods
private static PooledRedisClientManager GetManager()
{
var conn = ConfigurationManager.ConnectionStrings["Redis"].ConnectionString;
if (string.IsNullOrEmpty(conn))
{
throw new Exception("请配置ConnectionString Key 为Redis的连接串");
}
var manager = new PooledRedisClientManager(conn);
return manager;
}
#endregion
}
Dao.cs 数据操作基类


public abstract class Dao<TEntity> : IDisposable where TEntity : class
{
private IRedisClient _Client;
private IRedisTypedClient<TEntity> _Collection;
public Dao()
{
_Client = RedisManager.Manager.GetClient();
_Collection = _Client.As<TEntity>();
}
public void Save(TEntity entity)
{
_Collection.Store(entity);
}
public TEntity Get(object id)
{
return _Collection.GetById(id);
}
public void Delete(Object id)
{
_Collection.DeleteById(id);
}
#region Dispose
~Dao()
{
_Client.Dispose();
}
/****以下为修改BUG时新加****/
/*public void Close()
{
_Client.Dispose();
}
public void Dispose()
{
_Client.Dispose();
}*/
#endregion
}
单元测试用例:


[TestMethod]
public void TestAddAndGet()
{
for (var i = 0; i < 10; i++)
{
//Bug前代码
dao.Save(new TestEntity() { Id = "fdsafsa", FirstName = "Jack", SecondName = "Cui" });
Assert.AreEqual(dao.Get("fdsafsa").FirstName, "Jack");
//修复Bug代码
/*using (var dao = new TestDao())
{
dao.Save(new TestEntity() { Id = "fdsafsa", FirstName = "Jack", SecondName = "Cui" });
Assert.AreEqual(dao.Get("fdsafsa").FirstName, "Jack");
}*/
}
}
运行单元测试,单条保存测试用例通过。但是在应用程序大量数据操作测试的时候,发现写入Redis的数据有丢失的情况,但并不是每次都会丢失。并且系统没有操作失败的异常日志。这个时候就感觉特别奇怪,于是就在单元测试时想办法重现这个错误,当按上面测试用


例,循环10次保存数据测试用例通过,当循环100次时,发现异常重现了。没有异常抛出,测试用例也没有返回。然后设置断点调试,发现在运行第11次时,系统在_Client = RedisManager.Manager.GetClient()此处停住了。看了一下,代码中用到了PooledRedis 


ClientManager 客户端池对象,池对象有个特点是,有一个池的容量,当容量满的时候需要等待。而现在池中保存的就是RedisClient,是否是由于RedisClient达到了使用上限导致的。那么我们手动释放一下RedisClient是否解决这个错误,马上尝试了一下,通过使用


Using显式释放资源,发现确实解决了问题。问题是解决了,但是我们不经要问?系统中不是有析构函数吗?难道这是PooledRedisClientManager池的一个Bug吗?带着这样的疑问,让我们来查看一下问题到底出现在哪里?


1、析构函数只有在GC进行垃圾收集时才会被调用,而GC并不会马上执行,执行时间是不确定的。


2、那么这到底是不是PooledRedisClientManager池的一个Bug呢?


查看了一下对应的源码:


protected readonly int PoolSizeMultiplier = 10;
public IRedisClient GetClient()
{
lock (writeClients)
{
AssertValidReadWritePool();
RedisClient inActiveClient;
while ((inActiveClient = GetInActiveWriteClient()) == null)
{
if (PoolTimeout.HasValue)
{
// wait for a connection, cry out if made to wait too long
if (!Monitor.Wait(writeClients, PoolTimeout.Value))
throw new TimeoutException(PoolTimeoutError);
}
else
Monitor.Wait(writeClients, RecheckPoolAfterMs);
}
WritePoolIndex++;
inActiveClient.Active = true;
InitClient(inActiveClient);
return inActiveClient;
}
}
从源代码可以看出,在获取可用GetInActiveWriteClient()为null时,有一个循环调用,线程一直等待获取可以用的RedisClient.当池中无可用的RedisClient对象时,那么线程将一直等待。对象池有一个特征:获取池中对象 → 使用对象 → 归还对象 。那么是否是由


于使用完对象后没有归还对象呢?通过Using释放使用的对象切实可以起到归还的效果。于是再去挖掘程序中是否有Dispose或者是归还对象的操作, 发现该池定义了protected void Dispose(RedisClient redisClient)的方法,但是浏览源码切实没有发现任何地方显式


调用这个Dispose。


3.3 应用程序线程使用内存不断上升


记得之前同事遇到这样的一个BUG,系统上线后,监控发现该应用程序使用内存不断上升,这个不得了,这意外着随着时间的持续,系统会因为内存不足导致应用挂掉。于是通过获取线上的DUMP文件,通过分析,发现char[] 数组的对象特别多,那么在什么时候我们会使


用这么多的char[]呢?回顾一下,好像没有直接使用char[]的地方,但是我们知道,string对象经过编译后,它就是有char[]组成的,我们试着去找是否有StringBulider对象不停地加入数据,但是没有执行清理。结果真的发现有如此一个对象,这个对象据说是用来进行


测试调试使用的,上线的时候应该去掉,结果上线的时候忘记了。


参考资料


《框架设计 CLR Via C#》 Jeffrey Richter著


结束语:


在技术学习的过程中,很多时候我们知其然不知其所以然,因此在开发的过程中可能遇到不知其所以然而导致的问题,到最后也无法找到问题的根本原因。我们需要深入了解原理,并且通过原理举一反三,在其他类似的应用场景可以借鉴他们的思想。


写此文主要有三方面的目的:


1、将学过的东西通过文字的形式分享出来,一直被分享,从未进行分享。- -


2、有些时候,很多东西我们可能都理解,但是很难系统的书写出来,书写出来可以对已学知识和个人理解做一个记录和总结,进一步巩固已学知识。


3、试着将比较枯燥的概念和理论通过更通俗易懂的例子解释出来,同时能够将这些枯燥难解的理论和实际结合,让知识体现的更加具体一点。


个人感觉,文章还没有达到个人预期效果。其主要表现在如下两方面:其一,对于细节的理解可能说的不够透彻,没有找到通俗易懂的例子来进行解释;其二,对于项目中遇到的关于垃圾收集典型问题所举例子还不够丰富,没有真正体现出核心的价值。因此欢迎各位博


友能够分享个人经验进行补充,让对此方面知识还不是十分了解的同学更容易理解。

========

浅谈.NET垃圾回收机制

        垃圾收集器(GarbageCollection)是组成.Net平台一个很重要的部分,.NET垃圾回收机制降低了编程复杂度,使程序员不必分散精力去处理析构。不妨碍设计师进行系统抽象。减少了由于内存运用不当产生的Bug。成功的将内存管理工作从程序的编写时,脱离至


运行时的优点。


方法/步骤
1
 关于垃圾回收
 
      在.NET Framework中,内存中的资源(即所有二进制信息的集合)分为"托管资源"和"非托管资源".托管资源必须接受.NET Framework的CLR(通用语言运行时)的管理(诸如内存类型安全性检查),而非托管资源则不必接受.NET Framework的CLR管理. 需要手动清理垃圾(


显式释放)。
 
      托管资源在.NET Framework中又分别存放在两种地方: "堆栈"和"托管堆"(以下简称"堆");规则是,所有的值类型(包括引用和对象实例)和引用类型的引用都存放在"堆栈"中,而所有引用所代表的对象实例都保存在堆中。在C#中,释放托管资源是可以自动通过"垃圾回


收器"完成的(注意,"垃圾回收"机制是.NET Framework的特性,而不是C#的).
 
      在C++时代,我们需要自己来管理申请内存和释放内存. 于是有了new, delete关键字. 还有的一些内存申请和释放函数(malloc/free). C++程序必须很好地管理自己的内存, 不然就会造成内存泄漏(Memory leak). 在.net时代, 微软为开发人员提供了一个强有力的


机制--垃圾回收. 垃圾回收机制是CLR的一部分, 我们不用操心内存何时释放, 我们可以花更多精力关注应用程序的业务逻辑. CLR里面的垃圾回收机制用一定的算法判断某些内存程序不再使用,回收这些内存并交给我们的程序再使用.
2
 垃圾回收的功能
 
     1、用来管理托管资源和非托管资源所占用的内存分配和释放。
 
     2、寻找不再使用的对象,释放其占用的内存, 以及释放非托管资源所占用的内存。
 
     3、垃圾回收器释放内存之后, 出现了内存碎片, 垃圾回收器移动一些对象, 以得到整块的内存,同时所有的对象引用都将被调整为指向对象新的存储位置。
3
 回收内存的模式
 
     在.net中提供三种模式来回收内存资源:dispose模式,finalize方法,close方法。
 
     1、dispose提供了一种显示释放内存资源的方法。dispose调用方法是:要释放的资源对象.dispose().
 
     2、finalize方法是.net的内部的一个释放内存资源的方法。这个方法不对外公开,由垃圾回收器自己调用。
 
     3、close和dispose其实一样,只不过有的对象没有提供dispose的方法,只提供了close方法,而close其实在那个对象的类中,依然是调用了一个私有的dispose方法,而finalize其实也是调用一个不对外公开的dispose方法。
4
 回收一般过程
 
     1、垃圾回收时机:托管堆满了,内存分配即将不足时,0代内存分配满了,或其他情况,微软没有公开该部分算法。程序员可以手动调用GC.Collect(),但是会有警告,微软并不建议这么做。
 
     2、垃圾确认:通过根来寻找可达的对象(以后添加),并做标记,然后回收没有标记的对象。
 
     3、垃圾回收:内存回收,对于实现了Finalize方法的对象请参考最上面1的介绍。
 
     4、内存转移,合并。垃圾回收后使得内存不连续,零碎,.Net会将利用的内存合并为连续的块,然后更新对象的指针。
5
 注意的地方
 
     1、值类型(包括引用和对象实例)和引用类型的引用其实是不需要什么"垃圾回收器"来释放内存的,因为当它们出了作用域后会自动释放所占内存(因为它们都保存在"堆栈"中,学过数据结构可知这是一种先进后出的结构);
     2、只有引用类型的引用所指向的对象实例才保存在"堆"中,而堆因为是一个自由存储空间,所以它并没有像"堆栈"那样有生存期("堆栈"的元素弹出后就代 表生存期结束,也就代表释放了内存),并且非常要注意的是,"垃圾回收器"只对这块区域起作用; 
     3、"垃圾回收器"也许并不像许多人想象的一样会立即执行(当堆中的资源需要释放时),而是在引用类型的引用被删除和它在"堆"中的对象实例被删除中间有 个间隔,为什么呢? 因为"垃圾回收器"的调用是比较消耗系统资源的,因此不可能经常被调用!(当然,用户代码


可以用方法System.GC.Collect()来强制执行"垃圾回收器")
    4、有析构函数的对象需要垃圾收集器两次处理才能删除:第一次调用析构函数时,没有删除对象,第二次调用才真正删除对象。
    5、由于垃圾收集器的工作方式,无法确定C#对象的析构函数何时执行。
    6、可实现IDisposable接口的Dispose()来显示释放由对象使用的所有未托管资源。
    7、垃圾收集器在释放了它能释放的所有对象后,就会压缩其他对象,把他们都移动回heap的端部,再次形成一个连续的块。
========
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页