垃圾回收

在托管进程中存在两种内存堆,本机堆托管堆
本机堆(Native Heap)是由VirtualAlloc这个windows api分配的,是由操作系统和CLR使用的,用于非托管代码所需的内存。
托管堆(Managed Heap)是为所有.NET托管对象分配内存,也被称为GC堆,因为其中的对象都受垃圾回收器管理。
托管堆又分为两种,小对象堆和大对象堆(LOH),两者各自拥有自己的多个内存段。
小对象堆进一步划分为3带,0,1,2代,0和1代都同在一个内存段内,2可能跨越多个内存段,包含0代和1代的内存段被称为“暂时段”。
image.png
小对象堆中分配内存的生存期:如果对象小于85000个字节,CLR都会把他分配到小对象堆中的第0代,紧挨着依次分配,如果没有空闲空间,分配器就会扩大第0代内存堆,如果扩大内存堆超过了内存段的边界则触发垃圾回收。
对象总是诞生于第0代内存堆中,如果对象保持存活,发生垃圾回收时,GC就会把它提升一代,这样1代2代会变大。对象的位置没有移动,但各内存堆得边界发生了变化。
如果对象到达第2代内存堆,它就会一直流在那直到终结,第0代回收不会处理第2代的内存堆,直到完全垃圾回收。

如果第0代堆即将占满一个内存堆,且垃圾回收也无法整理碎片获得空间,那么GC就会重新分配一个内存段,新的内存段用于容纳第0代和第1代堆,老的内存段会变成第2代内存段,老的0代会放入新的1代,老的1代会升级为2代(提升很方便,不会复制数据)。

如果第2代继续扩大,就可能跨越多个内存段,无论存在多少个内存段,第0代和第1代总是位于同一个内存段中。

LOH遵循另外一套回收规则,大于85000个字节的对象会自动在LOH中分配内存,没有代之分,处于性能考虑,在垃圾回收期间,不会对LOH进行碎片整理,.NET4.5之后可以手动触发碎片整理。

垃圾回收是针对某一代及其以下几代内存堆进行的,回收2代,则1代和0代包括LOH都会进行回收。

垃圾回收包含4个阶段:

  1. 挂起:在垃圾回收之前,所有托管线程都会被强行中止。
  2. 标记:从GC根对象开始,垃圾回收器沿着所有对象的引用进行遍历标记
  3. 碎片整理:将对象重新紧挨着存放并更新引用,减少碎片,此动作是按需进行,无法控制,LOH不会自动整理碎片,可以手动通知GC来一次。
  4. 恢复:托管线程恢复运行。

在标记阶段并不会遍历内存中的所有对象,而是回收哪一代就访问遍历哪一段,比如回收第一代,就遍历1代和0代。

注意:高代内存堆中的对象很可能是低代内存堆对象的根对象,这样就会导致垃圾回收器遍历到一部分高代内存堆的对象,不过即使这样开销也要小于高代内存堆的完全垃圾回收

重点结论:

  • 垃圾回收过程耗时几乎完全取决于所涉及代的内存堆中的对象数量,和你在程序中分配的对象数量没关系,即使你分配了一个包含100万个对象的树,只要在下一次垃圾回收前把根对象的引用解除掉,这100万个对象就不会增加垃圾回收的耗时。
  • 垃圾回收的频率取决于所涉及代的内存堆中已被占用内存的大小。只要已分配的内存超过了某个阈值,就会发生该代的垃圾回收。

垃圾回收参数配置

1、垃圾回收可以配置为工作站模式或者服务器模式

默认采用工作站模式,所有的GC都运行于触发垃圾回收的线程中。工作站模式适用于简单应用,单处理器(只能能采用工作站模式),服务器需要运行多种应用的场景。
服务器模式下,GC会为每一个逻辑处理器或核心创建各自专用的线程,还会为每一个处理创建各自独立的内存堆,这样在分配内存和回收内存时都可以并行进行,提高了效率。
可以在app.config中将垃圾回收配置为服务器模式

  1. <configuration>
  2. <runtime>
  3. <gcServer enabled="true" /> //垃圾回收器采用服务器模式
  4. </runtime>
  5. </configuration>

2、后台垃圾回收(background gc)

后台垃圾回收只会影响第2代内存堆得垃圾回收行为,第0代和第1代仍然会采用前台线程垃圾回收,也就是会阻塞所有应用程序的线程。
后台垃圾回收由一个专门的第2代堆垃圾回收线程完成。对于服务器模式,每个逻辑处理器都拥有一个额外的后台GC线程。所以如果启用服务器模式和后台垃圾回收模式,那么每个处理器就拥有了2个后台GC线程。当然拥有多个线程并不会为进程带来负担。
后台垃圾回收线程和应用程序线程是并行的,所以第1代和0代的阻塞式垃圾回收时,后台垃圾回收线程和应用程序线程都会被暂停,等待阻塞式垃圾回收完成。
如果使用工作站模式,则后台垃圾回收默认是开启的,从.NET4.5开始,服务器模式默认开启后台垃圾回收,但可以关闭。

  1. <configuration>
  2. <runtime>
  3. <gcConcurrent enabled="false" /> //gc并行
  4. </runtime>
  5. </configuration>

3、低延迟模式(Low Latency Mode)

如果需要在一段时间内确保较高的性能,可以通知GC不要执行开销很大的第2代垃圾回收。

  1. //仅适用于工作站模式,禁止第2代垃圾回收
  2. GCSettings.LatencyMode = LowLatency;
  3. //适用于工作站模式和服务器模式,禁止第2代完全垃圾回收,
  4. //但允许第2代后台垃圾回收,必须启用后台垃圾回收,参数才有效。
  5. GCSettings.LatencyMode = SustainedLowLatency;

因为不会进行垃圾碎片整理,所以这两种模式都会显著的增加托管堆的大小,所以如果进行需要大量内存,应该避免使用低延迟模式。

在即将进入低延迟模式前,最好是强制执行一次完全垃圾回收,

  1. GC.Collect(2,GCCollectionMode.Forced); //第2代完全垃圾回收

当代码离开低延迟模式后,马上再做一次完全垃圾回收。

不要将低延迟模式作为默认模式,低延迟模式确实是用于那些必须长时间不被中断的应用程序,但不是100%时间都需要,比如股票开市时需要低延迟模式,但是休市时间里就要关闭掉低延迟模式。

当满足一下条件时,才能开启低延迟模式(慎用此模式)

  • 完全回收垃圾的持续时间过长,是程序正常运行时绝对不能接受的
  • 应用程序的内存占用量远低于可用内存数
  • 无论是关闭低延迟模式期间,程序重启,还是手动执行完全垃圾期间,应用程序都应该保持存活状态。

减少内存分配量

减少内存分配量,也就减轻了垃圾回收器的运行压力,同时也减少了内存碎片整理和CPU占用率。
减少内存分配量思考:

  • 是否真的需要这个对象
  • 对象中有没有成员可以去掉
  • 数组能否减小一点
  • 基元类型能否减小一点(int64改int32)
  • 有些对象很少用到,仅在必要时在分配
  • 有些类能否转变为结构struct,这样就可以放在堆栈中
  • 分配的内存很多,是否只用了一小段
  • 能否用其他途径获取数据

首要规则

针对垃圾回收器,存在一条基本的高性能编码规则,其实垃圾回收器明显就是按照这条规则进行设计的:
只对第0代内存堆中的对象进行垃圾回收
第0代的垃圾回收时瞬时的,也就是说,对象的生存周期要尽可能的短。如果做不到这一条,就要尽可能的让对象提升到第2代内存堆中,这样就会保留在那里,不会被回收。这意味着需要一直保持对一个长久对象的引用。也意味着需要把可重用的对象进行池化(Pooling),特别是LOH中的所有对象。
内存堆得代数越高,垃圾回收的低价就越大,应该确保大多数垃圾回收都发生在第0代和第1代,第2代回收应尽可能的少。
也要避免大部分的第1代回收,从第0代提升到第1代后,会被适时的提升到第2代,第1代可以说是低0代和第2代之间的缓冲区。

缩短对象的生存期

对象的作用域越小,在垃圾回收时就越没有机会被提升到下一代,在使用对象时,应该确保对象尽快的离开作用域,如果某个对象的引用时一个长时间存活的对象的成员,有时就得需要显式的设置为null值了。

减少对象树的深度

GC会沿着对象引用遍历,如果对象树很深,则会让垃圾回收很长时间才会执行完成。新版本CLR会采用新的算法来减轻这种情况,但还要尽量避免很深的对象树。

减少对象间的引用

和对象树的深度游关联,尽量减少对象间引用的复杂度。如果垃圾回收引起的暂停时间很长,那往往意味着有大型、复杂的对象间引用关系存在。

避免对象固定

对象固定是为了能够安全的将托管内存的引用传递给本机代码,最常见的引用就是数组和字符串。
对象固定会把内存地址固定下来,垃圾回收器就无法移动这类对象,这样就会给垃圾回收造成影响,增加了内存碎片的可能性。
位于第2代或者LOH中的固定对象一般问题不大,因为移动这些对象的可能性不大。

避免使用终结方法

若非必要永远不要实现终结方法(Finalizer)。终结方法是一段由垃圾回收器引发调用的代码,用于清理非托管资源。终结方法由一个独立的线程调用,排成队列依次运行,而且只有在一次垃圾回收完成后,对象被垃圾回收器标记为已销毁,才能进行调用,也就意味着,如果实现终结方法,则对象一定会滞留在内存中,即便在垃圾回收时应该被销毁时。终结方法会影响垃圾回收的整体效率,而且清理对象的过程会占用CPU资源。
如果实现了终结方法,那就必须同时实现IDisposable接口以启用显式清理,还要再Dispose中调用GC.SupperessFinalize(this)来把对象从终结队列中移除。

  1. class Foo:IDisposable
  2. {
  3. ~Foo(){ //析构函数,终结函数
  4. }
  5. public void Dispose(){
  6. Dispose(true);
  7. GC.SuppressFinalize(this);
  8. }
  9. protected virtual void Dispose(bool disposing){
  10. if(disposing){
  11. this.managedResource.Dispose();
  12. }
  13. //清理非托管资源
  14. UnsafeClose(this.Handle);
  15. //如果基类是IDisposable
  16. //务必调用
  17. //base.Dispose(disposing);
  18. }
  19. }

避免分配大对象

大对象的界限被设定为85000个字节,超过就是大对象,会被分配到LOH内存堆中。
尽量避免使用大对象,因为LOH的垃圾回收开销更大,而且会因为碎片导致内存用量不断增大,LOH不会自动进行碎片整理。

避免缓冲区复制

任何时候都要避免复制数据,内存复制最大的影响是垃圾回收,如果有复制缓冲区的需求,尽量把数据复制到另一个池化的或已存在的缓冲区中,避免发生新的内存分配。
如果需要表示整个缓冲区的一段,请使用ArraySegment类,可用来代表底层byte[]类型缓冲区的一部分区域,此ArraySegment可以传给API,而与原来的流没有关系,甚至可以绑定到新的MemoryStream对象上,这些过程都不会发生数据复制

  1. var memoryStream = new MemoryStream();
  2. var segment = new ArraySegment<byte>(memoryStream.GetBuffer(),100,1024);
  3. ...
  4. var blockStream = new MemoryStream(segment.Array,segment.Offset,segment.Count);

对长期存活对象和大型对象进行池化

减少LOH的碎片整理

有一种方法可以提高这种可能性,就是保证LOH的每次分配都是统一尺寸,或者至少也是集中标准尺寸的组合。

某些场合可以强制执行完全回收

绝大部分情况下,除了GC正常的调度计划外,不应该在强制执行完全垃圾回收,那样会干扰垃圾回收器的调优活动,可能会导致整体性能下降。
值得进行强制完全回收的场合:

  • 采用了低延迟模式,这种模式下内存堆会不断增长,需要时适当的时候来一次完全回收。
  • 偶尔会创建大量对象,并会存活很长时间,这是最好尽快将这些对象提升到第2代内存堆中,如果这些对象覆盖了即将成为垃圾的其他对象,通过一次完全垃圾回收就可以很快的销毁这些垃圾对象。
  • 处于要对LOH进行碎片整理的时候。
    1. //调用GC.Collect()方法,参数为需要回收的代数,即可执行完全垃圾回收,还可以附带参数
    2. GCCollectionMode.Default; //立即进行强制完全回收
    3. GCCollectionMode.Forced; //由垃圾回收器立即启动完全回收
    4. GCCollectionMode.Optimized;//允许由垃圾回收器决定是否立即执行完全回收
    5. //两个语句效果一样
    6. GC.Collect(2);
    7. GC.Collect(2,GCCollectionMode.Forced);

必要时对LOH进行碎片整理

可以指挥GC在下一次完全垃圾回收时进行一次碎片整理

  1. GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapComparctionMode.CompactOnce;

在垃圾回收之前获得通知

如果你的应用程序绝对不能收到第2代垃圾回收的破坏,那么可以让GC在即将执行完全垃圾回收时通知你,这样你就有机会暂停程序或进入更合适的状态
只有在尽可能的完成其他优化之后,再考虑这一招。
仅当一下条件成立时才能从垃圾回收通知中收益:

  • 完全垃圾回收的开销过大
  • 你可以完全停止程序的运行
  • 你可以迅速停止程序运行
  • 第2代垃圾回收很少发生,因此执行一次还是划算的

用弱引用作为缓存

弱引用指向的对象允许被垃圾回收器清理,相反,强引用会完全阻止所指对象被垃圾回收。有些对象开销内存很大,原本希望它长期存活,但在内存吃紧的情况下也愿意释放出来,弱引用的最大好处就是用来缓存这种对象。
弱引用带有一个IsAlive属性,但只能判断是否已经消亡,不能判断是否存活,可能在查看后,就被垃圾回收器销毁了。所以要想用的话,只能把弱引用复制给强引用,在进行判断。

  1. WeakReference weakRef = new WeakReference(myExpensiveObject);
  2. ...
  3. //创建强引用
  4. //现在就不会被GC考虑了
  5. var myObject = weakRef.Target;
  6. if(myObject != null){
  7. //doSomethings...
  8. }

WeakReference的一种上佳用途就是构建对象缓冲区,有些对象一开始由强引用创建,经过相当长的时间后就会失去作用,然后可被降级为弱引用来保存,最终可能会被销毁。

JIT编译

.NET的代码以微软中间语言(IL)的程序集(Assembly)发布。