工作原理

在面向对象编程中,每个类型都代表一种可供程序应用的资源。要应用这些资源必须为代表资源的类型分配内存。拜访一个资源所须要的步骤如下:

  • 调用IL指令newobj,为代表资源的类型分配内存。在C#中应用new关键字,编译器会主动生成该指令
  • 初始化内存,设置资源的初始状态,使资源可用。类型的实例结构器负责初始状态
  • 拜访类型的成员来应用资源
  • 捣毁资源的状态以进行清理
  • 开释内存,由垃圾回收负责

这看似简略的步骤,却频频引发编程谬误,如应用了已被开释的内存和没有开释不再须要的内存。

在非托管编程中,这种bug会造成内存透露(节约内存)和对象损坏的问题,如何正确的进行内存治理便是一个很重要的问题,而垃圾回收便是专门负责这一性能的。

从托管堆分配内存

CLR要求所有资源都从托管堆调配,对象在应用程序不须要应用时便会被主动删除。

过程初始化时,CLR要保留一块间断的、最后并没有对应的物理存储空间地址空间,这个地址空间就是托管堆。托管堆保护着一个指向下一个对象在堆中调配的地位的指针NextObjPtr。刚开始的时候NextObjPtr设置为保留地址空间的基地址。

IL指令newobj用于创立一个对象,该指令会让CLR执行以下步骤:

  • 计算类型以及所有基类型的字段须要的字节数
  • 加上对象开销所须要的字节数,每个对象都具备的两个开销字段:类型对象指针和一个同步索引块。对于32位应用程序两个字段各占32位须要8字节,64位应用程序两个字段各占64位须要16字节
  • CLR查看保留区域是否可能提供调配对象所需的字节数,如果有必要就提交存储。如果托管堆有足够的空间,对象会被放入,对象将会放在NextObjPtr指针指向的地址中,并且为它调配的字节将会被清零。接着调用类型的实例结构器位this参数传递NextObjPtr,newobj指令将返回对象的地址。在地址返回前,NextObjPtr指针的值会加上对象占据的字节数从而失去一个新值,它指向下一个对象放入托管堆的地址

垃圾回收器工作原理

应用程序调用new操作符创建对象时,可能存在没有足够的内存空间来调配该对象的状况,托管堆将对象须要的字节数加到NextObjPtr指针中的地址上来检测这种状况,如果后果值超过了地址空间的开端,表明托管堆已满,此时必须执行一次垃圾回收。

垃圾回收算法

垃圾回收器查看应用程序中是否存在不再应用的对象,如果存在,它们应用的内存就能够回收,如果一次垃圾回收之后,堆中任然没有可用的内存,new操作符将会抛出一个OutOfMemoryException异样。

应用程序的root

每个应用程序都蕴含一组根,每个根都是一个存储地位,其中蕴含一个指向援用类型对象的一个指针。该指针要么援用托管堆中的一个对象,要么为null

*只有援用类型的变量才被认为是根,值类型的变量永远不认为是根

垃圾回收器如何晓得应用程序正在应用一个对象?
  • 标记阶段:垃圾回收器在开始执行时,总是假如堆中所有的对象都是垃圾。垃圾回收器沿着线程栈上行查看所有的根,如果发现一个根援用了一个对象,就在对象的同步索引字段上设置一个bit。例如应用程序的根间接援用了A,B,C对象,所有这些对象都被标记。在标记对象C的时候发现对象C援用了对象D的一个字段,造成对象D也被标记。垃圾回收器就是这样以递归的形式遍历所有可达对象。
  • 标记好根和它的援用对象后,垃圾回收器查看下一个根,并持续标记对象,如果试图标记一个先前曾经标记过的对象,就会沿着这个门路走上来,不会进行二次遍历
  • 查看好所有根之后,堆中将蕴含一组已标记和未标记的对象。已标记的对象是通过利用程序代码可达的对象,而未标记的对象是不可达的认为是垃圾,它们占用的内存能够回收。垃圾回收器将进入压缩阶段
  • 压缩阶段:垃圾回收器线性地遍历堆,寻找未标记对象的间断内存块,如果发现的内存块较小,垃圾回收器就将其疏忽。然而如果发现大的可间断的内存块,垃圾回收器就会把非垃圾对象挪动到这里以压缩堆。
  • 挪动内存中的对象之后,蕴含指向这些对象的指针的变量和CPU寄宿器都将有效。垃圾回收器必须从新拜访应用程序的所有根,并批改它们使其指向对象的新的内存地位。如果对象中的字段指向的是一个曾经挪动了地位的对象,垃圾回收器也要负责改过这些字段。堆内存压缩之后,NextObjPtr指针指向紧接着最初一个非垃圾对象之后的地位

*由此可见,垃圾回收将造成显著的性能损失

应用终结操作开释本地资源

大多数类型只须要内存便可失常工作,然而还有一些类型除了要应用类型还要应用本地资源,如FileStream类型。
终结是CLR提供的一种机制,容许对象在垃圾回收器回收其内存之前执行一些清理操作。这些类型都实现了一个Finalize办法,当垃圾回收器断定一个对象是垃圾时,会调用对象的Finalize办法。

Finalize办法的定义

class People{    //这是一个Finalize办法    ~People()    {        //这里的代码会进入Finalize办法    }}

通过ildasm检查程序集可确认Finalize办法已生成

CriticalFinalizerObject类型
  • 首次结构任何CriticalFinalizerObject派生类型的一个对象时,CLR立刻对继承层次结构中的所有Finalize办法进行JIT编译,在结构对象就编译这些办法可确保对象被断定为垃圾的时候本地资源能保障失去开释
  • CLR会先调用非CriticalFinalizerObject派生类性的Finalize办法,再调用CriticalFinalizerObject派生类型的Finalize办法。如此托管类资源便能够在它们的Finalize办法中胜利的拜访CriticalFinalizerObject派生类型的对象
  • 如果AppDomain被一个宿主应用程序强行中断,CLR也会调用CriticalFinalizerObject派生类型的Finalize办法。宿主应用程序不再信赖它外部运行的托管代码时也利用这个性能确保本地资源得以开释
引起Finalize办法调用的起因
  • 第0代满时,会主动触发垃圾回收,导致Finalize办法被调用的最常见的起因
  • 代码显示调用System.GC的静态方法Collect,不倡议这样操作
  • Windows提醒内存不足
  • CLR卸载AppDomain,卸载时CLR会认为AppDomain中不再存在任何根,因而会对所有代的对象执行垃圾回收
  • CLR敞开,一个过程失常终止时,CLR就会敞开。在敞开过程中,CLR会认为该过程中不再存在任何根,因而会调用托管堆中的所有对象的Finalize办法。此时因为整个过程都要终止,CLR不会尝试压缩或开释内存,将有Windows负责回收过程的所有内存
应用Finalize办法造成的性能影响
  • 可终结的对象要花更长的工夫分配内存,因为指向它们的指针必须放到终结列表中
  • 可终结对象在回收时必须进行一些额定的解决,导致程序的运行速度变慢
Finalize办法揭秘

应用程序创立新对象时,new操作符会从堆中分配内存,如果对象定义了Finalize办法,那么在该类的实例结构器被调用之前,会将指向该对象的一个指针放到一个终结列表。
终结列表是由垃圾回收器管制的一个外部数据结构,列表中的每一项都指向一个对象。在回收该对象之前应该调用它的Finalize办法。

*结构一个类型的实例时,如果该类型的Finalize办法是从Object继承的,就不认为这个对象是能够终结的。类型必须重写Object的Finalize办法,这个类型及其派生类的对象才被人为是能够终结的。

垃圾回收器开始时,会查找终结列表中指向垃圾对象的指针。找到一个指针后,该指针会从终结列表中移除,并追加到垃圾回收器的另一个外部数据结构freachable队列中,freachable队列中的每个指针都代表其Finalize办法曾经筹备好的一个对象。

一个非凡的高优先级CLR线程专门负责Finalize办法的调用,当freachable队列为空时,这个线程将会休眠。一旦队列不为空,该线程便会被唤醒,将每一项从freachable队列中移除,并调用每个对象的Finalize办法。

freachable队列

垃圾回收器会将不可达的对象视为垃圾,然而,当垃圾回收器将对象的援用从终结列表挪动到freachable队列后,对象将不再被认为是垃圾,其内存不可被回收。标记freachable对象时,这些对象的援用类型的字段所援用的对象也会被递归标记。所有这些对象都会在垃圾回收的过程中存活下来。这时,垃圾回收器完结对垃圾的标识(这个过程中会有某一些被认为是垃圾的对象又被从新认为不是垃圾)。而后垃圾回收器开始压缩可回收内存,非凡的CLR线程清空freachable队列,并执行每个对象的Finalize办法。垃圾回收器下一次调用时,会发现已终结的对象成为真正的垃圾,因为应用程序的根不再指向它,freachable队列也不再指向它。所以这些对象的内存会间接回收。
*整个过程中,可终结的对象须要执行两次垃圾回收能力开释它们占用的内存。

FInalize办法蕴含共享状态的代码

应该应用线程同步锁,在只有一个终结线程的状况下,可能存在多个CPU调配可终结的对象,但只有一个线程执行Finalize办法,会造成该线程可能跟不上调配速度,造成性能和伸缩性方面的问题。

using语句
static void Main(){    using (FileStream fs = new FileStream("Temp.txt", FileMode.Create))    {        fs.Write(new byte[] { 1, 2, 3 }, 0, 3);    }    File.Delete("Temp.txt");}

应用using语句的时候编译器会主动生成一个try块和一个Finally块。显然,在Finally块中,编译器会生成代码将变量转型成一个IDisposable并调用Dispose办法。所以using语句只能用于实现了IDisposable接口的类型。

手动监督和管制对象的生存期

GC Handle table
CLR为每个AppDomain都提供了一个GC句柄表,该表容许程序监督对象的生存期,或手动管制对象的生存期。在一个AppDomain创立之初,句柄表是空的。句柄表的每个记录项都蕴含两种信息:一个指向托管堆上一个对象的指针;一个flag标记,它指出想要监督或管制的对象。

应用GCHandle的Alloc办法管制或监督对象的生存期

public static GCHandle Alloc(object value, GCHandleType type);

GCHandleType枚举

  • Weak:容许监督对象的生存期。可检测垃圾回收在什么时候断定该对象在利用程序代码中行将不可达。此时对象的Finalize可能曾经执行也可能没有执行,对象可能依然存在内存中。
  • WeakTrackResurrection:容许监督对象的生存期。可检测垃圾回收在什么时候断定该对象在利用程序代码中行将不可达。此时对象的Finalize曾经执行,对象的内存已回收。
  • Normal:容许管制对象的生存期。通知垃圾回收器:行将应用应用程序中没有变量援用的对象,该对象必须保留在内存中。垃圾回收产生时,该对象的内存能够压缩(挪动)。如果不向Alloc传递任何GCHandleType标记,就默认应用GCHandleType.Normal标记。
  • Pinned:容许管制对象的生存期。通知垃圾回收器:行将应用应用程序中没有变量援用的对象,该对象必须保留在内存中。垃圾回收产生时,该对象的内存不能压缩(挪动)。须要将内存地址传给非托管代码时,这个标记就十分有用。非托管代码能够释怀的向托管代码的这个内存写入,晓得托管对象的地位不会因为垃圾回收而挪动。

调用Alloc办法时,扫描AppDomain的GC句柄表,查找一个可用的记录项来存储传给Alloc的对象地址,并将标记设置为GCHandleType实参传递的值。接着返回GCHandle实例,该实例是一个轻量级的值类型,其中蕴含一个实例字段,它援用了句柄表中的记录索引项。能够通过获取GCHandle实例,调用其Free办法开释GC句柄表中的记录项,使GCHandle实例有效。

垃圾回收器如何应用GC句柄表
  • 垃圾回收器标记所有可达对象,而后扫描GC句柄表。所有Normal和Pinned都被看成时根,同时标记这些对象(包含这些对象的字段援用的对象)
  • 垃圾回收器扫描GC句柄表,查找所有Weak记录项。如果一个Weak记录项援用了一个未标记的对象,指针标识的就是一个垃圾对象,记录项的指针更改为null
  • 垃圾回收器扫描中结列表,如果列表中的一个指针未援用标记的对象,指针标识的就是一个不可达对象,指针将从终结列表移入freachable队列。这时对象将会被标记,因为对象又变成了可达对象
  • 垃圾回收器扫描GC句柄表,查找所有WeakTrackResurrection记录项。如果一个WeakTrackResurrection记录项援用了一个未标记的对象(由freachable队列中的一个记录项指向的一个对象),指针标识的就是一个垃圾对象,该记录项的指针值更改为null
  • 垃圾回收器对内存进行压缩,其实就是内存碎片整顿的过程。某些状况下如果垃圾回收器判断内存碎片化不重大,就会决定不压缩内存。Pinned对象是不会压缩(挪动)的,所以垃圾回收器会将其它对象移到它的四周
托管代码的援用传给非托管代码

要应用Pinned标记,因为非托管代码要回调托管代码时,不能真正的将指向一个托管对象的指针传给非托管代码,因为如果产生垃圾回收,对象可能在内存中挪动,指针便有效了。为了失常工作,调用Alloc办法,向它传递对象援用和Pinned标记,而后将返回的GCHandle实例转型成为一个Intptr,再将Intptr传递给非托管代码。
非托管代码回调托管代码时,托管代码将Intptr转型成为GCHandle,查问Target属性取得托管对象的援用,非托管代码不再须要这个援用后能够调用GCHandle实例的Free办法,使将来的垃圾回收能开释这些对象。

对象复活

一个被视为垃圾的对象又从新被当做可达(非垃圾)对象的过程,成为对象复活。垃圾回收器将一个对象的援用放入freachable队列,对象就变成可达对象了。待对象的Finalize办法返回,不再有根指向对象,对象才真正死亡。

Finalize办法在执行时将对象指针放到一个动态字段中

class Program{    public static Object s_Obj = null;}class SomeType{    ~SomeType()    {        Program.s_Obj = this;    }}

上述代码展现了当SomeType的Finalize办法被调用时,该对象的援用会被放入到一个根,使对象得以复活,应用程序能够自在应用这个对象。但须要留神的是,这个对象已经被终结,所以应用它可能造成无奈预测的后果。如果SomeType的一些字段援用了其它对象,这些对象都会被复活,在这些对象中一部分对象的Finalize办法曾经被调用过了。

应用ReRegisterForFinalize创立死不了的对象

class SomeType{    ~SomeType()    {        Program.s_Obj = this;        GC.ReRegisterForFinalize(this);    }}

Finalize办法被调用时,让一个根援用该对象,从而让对象复活。而后调用ReRegisterForFinalize办法,将指定对象(this)的地址增加到终结列表开端,当垃圾回收器判断这个对象不可达时(s_Obj为null),会将对象的指针从终结列表挪动到freachable队列,造成对象的Finalize办法再被调用一次。复活一个对象会复活这个对象援用的所有对象,所有这些对象都须要调用ReRegisterForFinalize办法。

*不倡议对象有这种行为

CLR垃圾回收器采纳的一种机制,目标是晋升应用程序的性能。代的特点:

  • 对象越新,生存期越短
  • 对象越老,生存期越长
  • 回收堆的一部分速度快于回收整个堆

代的工作原理

  • 托管堆在初始化时不蕴含任何对象,增加到堆的对象成为第0代对象。第0代对象就是新结构的对象,垃圾回收器从未查看过这些对象。
  • CLR初始化时,会为第0代对象抉择一个估算容量,假设该容量为256kb,如果调配一个新对象超过预算容量,就会启动一次垃圾回收。不可达的对象将被回收,而在垃圾回收中存活的对象将会被认为时第1代对象。
  • 一次垃圾回收之后第0代就不蕴含任何对象了,此时新结构的对象会被调配到第0代中。如果此时新调配的对象超过预算容量,将会启动垃圾回收,因为存在第1代对象,所以垃圾回收器也会为第1代对象抉择一个估算容量,假如为2M。
  • 开启一次垃圾回收时,垃圾回收器还会查看第1代所占的内存。如果第1代的内存远少于2M,那么垃圾回收器只会查看第0代对象,疏忽第1代对象从而放慢垃圾回收的速度。
  • 疏忽第1代对象的益处在于:垃圾回收时不用遍历托管堆中的每个对象。如果一个对象援用了老一代的对象,垃圾回收器就能够疏忽老对象外部的所有援用。
  • 基于较老的对象生存周期较长的特点,垃圾回收器会认为查看第1代中的对象很有可能找不到多少垃圾,回收不了多少内存,因而对第1代进行垃圾回收很有可能浪费时间,会呈现如果第1代真的有垃圾而没有回收的状况。
  • 随着程序的运行,假如第1代的内存曾经超出2M,将会查看第0代和第1代的对象,两代都会被垃圾回收,之前可能残留在第1代中不可达的对象将会在这时被回收。
  • 垃圾回收后第0代中存活的对象变成第1代,而第1代中存活的对象将变成第2代(没有第三代)。并且为第2代抉择约为10M的估算容量,估算的大小是为了晋升性能,估算越大启动垃圾回收的频率就越低。

*如果垃圾回收在第0代后存活的对象很少,垃圾回收器可能会将第0代的估算从256kb升高到128kb。意味着垃圾回收更加的频繁,但垃圾回收器须要做的工作会缩小,从而减小过程的工作集。如果第0代所有对象都是垃圾,垃圾回收时就没必要压缩内存,只需让NextObjPtr指针指向第0代的起始地位即可,这样将变得更快。反之垃圾回收器也有可能增大估算,使垃圾回收的次数变少,但每次回收的内存要多得多。如果没有回收足够的内存,垃圾回收器便会执行一次残缺的回收,如果内存还是不够便会抛出OutOfMemoryException异样。

应用MemoryFailPoint预测需要大量内存的操作是否胜利
try{    //保留1G内存    using(MemoryFailPoint mfp = new MemoryFailPoint(1000))    {        //执行耗费大量内存的算法    }  //Dispose开释1G内存}catch (InsufficientMemoryException e){    //无奈保留所需的内存}
编程管制垃圾回收器

强制执行一次垃圾回收可调用以下办法

  • public static void Collect():对所有代执行一次齐全回收。
  • public static void Collect(int generation):容许回收指定的代,可传递0到GC.MaxGeneration之间的任何整数(GC.MaxGeneration最大为2,因为最多只有2代)。传递0回收第0代对象;传递1回收第0代和第1代对象;传递2回收第0,1,2代对象。
  • public static void Collect(int generation, GCCollectionMode mode):与第2个办法的差异在于多了一个GCCollectionMode参数

GCCollectionMode枚举

  • Default:等同于不传递任何符号
  • Forced:强制回收指定的代以及低于它的所有代
  • Optimized:只有在可能开释大量内存或者缩小碎片化的前提下,才执行回收。如果垃圾回收成果不佳,以后调用将不产生作用

###### GC.WaitForPendingFinalizers
挂起以后线程,直到解决freachable队列的线程清空该队列,实现对每个对象的Finalize办法的调用。

示例代码:

class MyFinalizeObject{    ~MyFinalizeObject()    {        Console.WriteLine("MyFinalizeObject 的 Finalize办法被执行");        GC.KeepAlive(this);    }}class Program{    static void Main()    {        Console.WriteLine("最大代数:{0}", GC.MaxGeneration);        MyFinalizeObject o = new MyFinalizeObject();        //查看对象o所属的代        Console.WriteLine("对象o以后代数:{0}", GC.GetGeneration(o));        //执行垃圾回收晋升对象的代        GC.Collect();        Console.WriteLine("1次垃圾回收后对象o以后代数:{0}", GC.GetGeneration(o));        GC.Collect();        Console.WriteLine("2次垃圾回收后对象o以后代数:{0}", GC.GetGeneration(o));        GC.Collect();        Console.WriteLine("3次垃圾回收后对象o以后代数:{0}", GC.GetGeneration(o));        //销毁对象的援用        o = null;        //回收第0代        Console.WriteLine("回收第0代对象");        GC.Collect(0);        GC.WaitForPendingFinalizers(); //对象o的Finalize办法未被调用        //回收第1代        Console.WriteLine("回收第1代对象");        GC.Collect(1);        GC.WaitForPendingFinalizers(); //对象o的Finalize办法未被调用        //回收第2代        Console.WriteLine("回收第2代对象");        GC.Collect(2);        GC.WaitForPendingFinalizers(); //对象o的Finalize办法被调用        Console.ReadKey();    }}

运行后果

线程劫持

CLR要开始一次垃圾回收时,会立刻挂起正在执行托管代码的所有线程。而后CLR查看每个线程的指令指针,判断线程执行到了哪里。接着指令指针和JIT编译器生成的表进行比拟,判断线程正在执行什么代码。
如果线程的指令指针恰好在一个表中标记好的便宜地位,就说该线程到达了一个平安点。线程能够在平安点平安的挂起,直到垃圾回收完结。反之则示意线程不在平安点,CLR则不能执行垃圾回收。在这种状况下CLR会劫持该线程。也就是说它会批改线程栈,使它返回的地址指向CLR外部实现的一个非凡函数。而后线程复原执行,当执行的办法返回后,非凡函数开始执行,它会将线程挂起。
然而,线程有时候会长工夫不能从以后办法返回。所以当线程复原执行后,CLR会用大概250ms的工夫尝试劫持线程,过了这个工夫,CLR会再次挂起线程,并查看它的指令指针。如果线程到达了平安点,那么就能够开始垃圾回收了。然而如果线程还未到达平安点,CLR就查看是否调用了另一个办法,如果是,CLR再一次批改线程栈,以便从最近执行的一个办法返回后劫持线程。而后CLR复原线程进行下一次劫持尝试。
所有线程都到达平安点或是被劫持后,垃圾回收能力开始,垃圾回收实现后,所有线程都会复原,应用程序持续运行,被劫持的线程返回最后调用它们的办法。

*在理论利用中,CLR次要通过劫持线程进而挂起线程,而不是依据JIT编译器生成的表来判断线程是否到达一个平安点。起因是JIT生成表须要大量的内存,而且会增大工作集,进而重大影响性能。

垃圾回收模式
  • CLR在启动时会抉择一种GC模式,在过程的生存期内这个模式不能扭转。两种根本的GC模式:
  • 工作站:客户端应用程序优化垃圾回收器。垃圾回收器假设机器上运行的其它应用程序对CPU资源的要求不高。工作站模式有两个子模式:并发回收器的工作站、无并发回收器的工作站。
  • 服务器:服务端应用程序优化垃圾回收器。垃圾回收器假设机器上没有运行其它应用程序,并假设机器的所有CPU都可用来执行垃圾回收。该GC模式造成托管堆分解成几个区域,每个CPU一个区域。开始一次垃圾回收时,垃圾回收器在每个CPU上都会运行一个线程,每个线程和其它线程并发回收它本人的区域。这个性能要求应用程序在多CPU计算机上运行,使线程真正的同时工作,从而晋升性能。

工作站GC模式能够应用并发或非并发的形式运行,在并发形式中,垃圾回收器有一个额定的后盾线程,能在利用程序运行时并发地回收对象。一个线程因为调配一个对象导致第0代超出预算,垃圾回收器会先挂起所有线程,再判断要回收哪些代。如果垃圾回收器须要回收的是第0代或第1代,那么将没什么不同。如果要回收第2代,那么就会增大第0代的估算,以便在第0代中调配新对象。

设置GC模式
应用GCSettings类的LatencyMode属性管制对垃圾回收模式

GCLatencyMode枚举

  • Batch(服务器GC模式的默认值):在工作站GC模式中,这个提早模式敞开并发GC。在服务器模式中,这是惟一无效的提早模式。
  • Interactive(工作站GC模式的默认值):在工作站GC模式中,这个提早模式会关上并发GC。在服务器GC中,这个提早模式是有效的。
  • LowLatency:在工作站GC模式中,在短期的,工夫敏感的操作中应用这个提早模式;在这种状况下对第2代的回收可能造成凌乱。在服务器GC中,这个提早模式是有效的。补充阐明:个别状况下这个模式用来执行一次短期的、工夫敏感的操作,再将模式改为Batch或Interactive。这个模式期间垃圾回收器会尽量避免回收第2代对象,如果调GC.Collect或者Windows通知CLR零碎内存低第2代依然会被回收。在这个模式应用程序抛出OutOfMemoryException的几率会增大,所以尽可能短地处在这个模式中,防止调配太多地对象或大对象。

大对象
任何85000字节或更大的对象都主动视为大对象。大对象从一个非凡的大对象堆中调配。因为在堆中下移85000字节的内存块会节约太多的CPU工夫,所以大对象永远不会被压缩。然而也不能假设大对象永远不挪动,因为在将来的某个时刻,可能大对象曾经不再是85000字节了。

*大对象总是被认为是第2代的一部分,所以只能为须要长时间存活的资源创立大对象。如果调配短时间存活的大对象,将导致第2代更加频繁的回收,造成性能侵害。

证实大对象总是在第2代中被调配

static void Main(){    object o = new byte[85000];    //显示2而不是0    Console.WriteLine(GC.GetGeneration(o));}
监督垃圾回收
  • public static long GetTotalMemory(bool forceFullCollection):查看托管堆中的对象以后应用了多少内存
  • public static int CollectionCount(int generation):查看指定代产生了多少次垃圾回收

*可通过应用这两个办法把握代码块对过程工作集的影响,并理解执行代码时产生了多少次垃圾回收,如果数字太高则须要思考优化代码