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