工作原理
在面向对象编程中,每个类型都代表一种可供程序使用的资源。要使用这些资源必须为代表资源的类型分配内存。访问一个资源所需要的步骤如下:
- 调用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):查看指定代发生了多少次垃圾回收
*可通过使用这两个方法把握代码块对进程工作集的影响,并了解执行代码时发生了多少次垃圾回收,如果数字太高则需要考虑优化代码