GC总结

来源:互联网 发布:mac markdown 编辑:程序博客网 时间:2024/06/03 13:21

内存基础知识

下面的列表总结了重要的 CLR内存概念。

·        每个进程都有其自己单独的虚拟地址空间。同一台计算机上的所有进程共享相同的物理内存,如果有页文件,则也共享页文件。

·        默认情况下,32 位计算机上的每个进程都具有 2 GB 的用户模式虚拟地址空间。

·                    作为一名应用程序开发人员,您只能使用虚拟地址空间,请勿直接操控物理内存。垃圾回收器为您分配和释放托管堆上的虚拟内存。如果您编写的是本机代码,请使用Win32 函数处理虚拟地址空间。 这些函数为您分配和释放本机堆上的虚拟内存。

·        虚拟内存有三种状态:

o   可用。 该内存块没有引用关系,可用于分配。

o   保留。 内存块可供您使用,并且不能用于任何其他分配请求。但是,在该内存块提交之前,您无法将数据存储到其中。

o   提交。 内存块已指派给物理存储。

·        可能会存在虚拟地址空间碎片。就是说地址空间中存在一些被称为孔的可用块。 当请求虚拟内存分配时,虚拟内存管理器必须找到满足该分配请求的足够大的单个可用块。 即使您具有 2 GB 的可用空间,2 GB 的分配请求也有可能会不成功,除非所有这些空间必须位于单个的地址块中。

·        如果用完保留的虚拟地址空间或提交的物理空间,则可能会用尽内存。

进程初始化期间,CLR保留两个区段(segment)的虚拟地址空间:一个区段是普通堆,另一个区段是大对象堆。每个区段的大小是不同的。对于客户端应用程序,每个区段约为16MB;对于服务器应用程序,每个区段大约为64MB。还有一些其他因素会影响区段的大小,例如是32位还是64位操作系统上运行,以及机器安装的CPU数量,在CPU较多的机器上,区段会小一些。随着区段装满非垃圾对象,CLR会分配更多的区段,这个操作一直继续,直到整个进程都满了为止。

垃圾回收是在第0代满的时候发生的。使用代(generation)的机制的唯一目的就是提高性能。基本思路是,第0代是最近分配的对象,从未被垃圾回收算法检查过。在一次垃圾回收中存活下来的对象被提升到另一代。如经过一次垃圾回收,第0代被提升为第1代;第1代被提升为第2代。

GC会检查托管堆中是否有应用程序不再使用的对象。如果有,他们使用的内存就可以被回收,如果回收完毕后,仍然没有可用内存,那么new操作符会抛出一个OutOfMemoryException。

垃圾回收

有5种事件导致垃圾回收:

1.     第0代满

2.     显式调用GC.Collect

3.     Windows报告内存不足

4.     CLR卸载AppDomain

5.     CLR关闭(进程关闭)

CLR使用一个高优先级,专用的线程来调用Finalize方法。对于前四种情况,如果一个Finalize方法进入了无限循环,那么这个特殊的线程就会被阻塞,其他Finalize方法就得不到调用。因为应用程序永远都不能回这些可终结对象的内存,只要程序还在运行,内存就会一直泄露。

对于第五种情况,每个Finalize方法大约有2秒的时间返回。如果2秒之内没有返回,CLR将直接杀死该进程。另外调用所有Finalize方法的时间超过了40秒,CLR也会杀死进程。这些值在将来有可能会发生改变。

即使实例的构造函数抛出了异常,实例的Finalize方法也会被调用,所以不应在Finalize方法中假定对象处于一致的状态。

对于一个可终结对象,在其构造函数被调用之前,此对象将被放入终结列表(finalization list)中。该列表是一个由GC控制的数据结构,它包含了所有可被终结的对象。在垃圾回收开始后,GC扫描终结列表,将垃圾对象从列表中移除(因为非垃圾对象会被标记,所以垃圾对象就是那些没有被标记的对象),然后将其添加到Freachable队列中,该队列是CLR的另一个内部数据结构。其中的每一个对象的Finalize方法都已经准备好被调用。当Freachable队列为空时,负责调用Finalize方法的线程会睡眠。当队列进入记录项,线程被唤醒,每一项都会被移除并调用Finalize方法。因此在Finalize方法中不应该对执行线程做任何假设。

虽然System.Object类型定义了Finalize方法,但是CLR知道忽略它。

当对象被添加到Freachable队列时,这个对象(及其引用的对象)复活了,因为对象现在是可达的了。现在GC不再认为对象是垃圾,所以GC不会回收这些对象的内存。此时特殊的线程清空Freachable队列并调用Finalize方法。当下一次垃圾回收的时候,会发现被终结的对象是垃圾对象,将会进行内存回收。整个过程中,需要两次垃圾回收才能收回这些对象占有的内存。实际情况下可终结对象会被提升代,所以需要不止两次回收才能收回这些对象占有的内存。

调用GC.SupressFinalize方法,将会打开当前对象的一个标志位,次标志位被打开后,CLR就不会把对象添加到Freachable队列。

大对象堆

任何大于或等于85000字节的对象都被视为大对象,大对象从大对象堆中分配。大对象和小对象一样终结和释放,但是大对象永不压缩。但是此行为在4.5.1中可以更改。大对象总是属于第二代,所以大对象很难被回收,所以尽量不要分配临时大对象。

在4.5.1中,使用如下代码压缩大对象堆,每次压缩完毕,GCSettings.LargeObjectHeapCompactionMode被重置为默认值。还要注意的是,后台垃圾回收器永远不会压缩大对象堆。

GCSettings.LargeObjectHeapCompactionMode =GCLargeObjectHeapCompactionMode.CompactOnce;

GC.Collect(); 

预测大量内存操作是否成功

实现一个算法时,可能需要事先知道该算法需要大量内存。如果直接执行算法,有可能抛出OutOfMemoryException,这种情况下,之前的工作算是白做了。.NetFramework提供System.Runtime.MemoryFailPoint类,它允许在消耗内存的算法开始之前检测是否有充足内存。

创建 MemoryFailPoint 对象并指定下操作 (MB)需要使用MB 的内存位数。 如果没有足够的内存不可用, 将引发InsufficientMemoryException 异常。 MemoryFailPoint 运行在 16 MB 粒度。 所有值小于 16MB 视为 16 MB,同时,其他值视为 16 MB 的下一个更大的多个。

以下代码保留了1G内存用于执行算法,

try

{

    using (MemoryFailPoint mfp =newMemoryFailPoint(1000))

    {

        //Do Anything needs 1G memory

    }

}

catch (InsufficientMemoryException)

{

    Console.WriteLine("InsufficientMemory");

}

如果MemoryFailPoint类的构造器没有抛出异常,表明逻辑上已经保留了请求的内存。但是请求的物理内存尚未分配。这表示,算法只是更有可能获得所需的内存并允许成功,并不表示一定会分配到所需的物理内存。此类只是帮助你写更健壮的程序。

线程劫持

当GC开始内测回收的时候,所有线程都必须挂起,因为这些线程不能再访问内存中的对象。这是因为GC线程要压缩内存,压缩过程中,对象的引用可能变得无效,所以只能在GC完成后,其他线程才能继续执行。

GC开始的时候,CLR会对其他线程进行劫持,使其进入一个特殊的函数,而这个函数将会进入一个临界区。所以如果在GC线程正在垃圾回收的时候,使用WinDbg查看进程中所有其他线程的堆栈,那么回发现,所有这些线程都进入了同一个方法,并且都试图获取同一个临界区。

并不是任何时刻都可以劫持线程,必须等待线程进入一个安全点。如果CLR创建了一个新对象,但是还没有将对象赋值给变量,此时劫持线程就不可行的,或者说线程不在一个安全点上。若是此时劫持线程并进行垃圾回收,那么新对象将被垃圾回收,而变量指向一个无效地址。

垃圾回收模式

垃圾回收模式包括工作站模式和服务器模式,使用如下配置节来配置模式,默认情况下使用工作站模式。

<runtime>

   <gcServer enabled="true"/>

 </runtime>

还可以在运行时获取垃圾回收模式是否为服务器模式,如下:

GCSettings.IsServerGC{get;}

该模式不能在运行时修改。

GC延迟模式

有四种延迟模式可供选择

·        Batch

·        Interactive

·        LowLatency

·        SustainedLowLatency

该模式可以在运行时获取或者设置,如下:

GCSettings.LatencyMode= GCLatencyMode.SustainedLowLatency;

这里面有一些版本问题,第四种模式只被.Net Framework4.0支持,而且只能用于工作站模式,但是到了.Net Framework 4.5,也可以用于服务器模式。

.Net Framework提供了配置节用于配置延迟模式,如下:

<gcConcurrentenabled="true"/>

要注意的是,这是个bool值,而且名称也不是gcLatencyMode。所以不能完全对应到四种延迟模式,具体的对应关系如下:

 

gcServer=true

gcServer=false

gcConcurrent =true

·         .Net4.5之前:Batch

·         .Net 4.5之后:SustainedLowLatency

 

·         .Net4.0之前:Interactive

·         .Net 4.0之后:SustainedLowLatency

 

gcConcurrent =false

Batch

Batch

对于服务器模式Interactive和LowLatency模式是无效的,但是在运行却能看到LatencyMode是Interactive。这也许是.Net的Bug。

总结一下,工作站GC支持所有四种模式,服务器GC支持Batch和SustainedLowLatency模式。

对于四种延迟模式的具体作用,解释如下:

·        Batch

Batch模式是全阻塞模式,一旦GC开始运行,所有非GC线程都会被挂起,直到垃圾回收结束。

·        Interactive

此模式是工作站GC的默认模式(在.Net 4.0之前)。GC会使用多个线程进行并发垃圾回收,但是此并发只针对第二代对象。第零代和第一代对象永远使用全阻塞模式回收。具体的回收场景是,某个对象的创建引发了垃圾回收,GC使用创建此对象的线程回收第0,1代垃圾,同时有一个专用线程用于在后台回收第二代垃圾。

·        LowLatency

此模式用于对时间敏感的进程。GC会全力避免回收第二代,所以应该在短时间内应用此模式,并在不需要此模式的时候设置回原来的模式。

·        SustainedLowLatency

作用域Interactive基本相同,但是可以应用在服务器GC模式。

区别是:Interactive模式的专用线程在GC的过程中,不允许发起另外一个GC过程,而且只能在内存段中剩余的空间中分配内存。

SustainedLowLatency模式允许在后台GC运行中启动另一次针对第0和1代的GC过程,甚至允许创建另一个新段来进行内存分配。

工作站和服务器垃圾回收比较

以下是工作站垃圾回收的线程处理和性能注意事项:

·        回收发生在触发垃圾回收的用户线程上,并保留相同优先级。因为用户线程通常以普通优先级运行,所以垃圾回收器(在普通优先级线程上运行)必须与其他线程竞争 CPU 时间。

·        不会挂起运行本机代码的线程。(针对后台回收且只限于标记过程)

·        工作站垃圾回收始终用在只有一个处理器的计算机上,而不管 <gcServer> 设置如何。 如果您指定服务器垃圾回收,则CLR 会使用工作站垃圾回收,并禁用并发。

以下是服务器垃圾回收的线程处理和性能注意事项:

·        回收发生在以 THREAD_PRIORITY_HIGHEST 优先级运行的多个专用线程上。

·        为每个 CPU(逻辑CPU)提供一个用于执行垃圾回收的专用线程和一个堆,并将同时回收这些堆。 每个堆都包含一个小对象堆和一个大对象堆,并且所有的堆都可由用户代码访问。 不同堆上的对象可以相互引用。

·        因为多个垃圾回收线程一起工作,所以对于相同大小的堆,服务器垃圾回收比工作站垃圾回收更快一些。

·        服务器垃圾回收通常具有更大的段。

·        服务器垃圾回收会占用大量资源。例如,如果在一台具有 4 个处理器的计算机上运行了12 个进程,则在它们都使用服务器垃圾回收的情况下,将有 48 个专用垃圾回收线程。在高内存加载的情况下,如果所有进程开始执行垃圾回收,则垃圾回收器将要计划 48 个线程。如果运行应用程序的数百个实例,请考虑使用工作站垃圾回收并禁用并发垃圾回收。这可以减少上下文切换,从而提高性能。如果启用后台回收,启用的线程将会更多。

如果要手动回收内存,那么就可以指定是否使用后台回收功能,如下:

public static void Collect(int generation,GCCollectionMode mode, bool blocking);

blocking参数为false,则表示使用后台回收功能。

如果要分配超过2G内存的对象,需要使用如下配置:

<gcAllowVeryLargeObjects  enabled="true|false" />

 

0 0