asp.net 之 GC (垃圾回收机制)

时间:2024-01-17 14:44:38

今天抽时间好好整理了下GC相关知识,看了CSDN和博客园的几篇文章,有了一定的简单了解,决定根据个人理解整合一份随笔写下来,望诸位指教。

一:基础问题

1.首先需要知道了解什么是GC?

GC如其名,就是垃圾收集,当然这里仅就内存而言。

2.GC工作的原理

Garbage Collector(垃圾收集器,在不至于混淆的情况下也成为GC)以应用程序的root为基础,遍历应用程序在Heap(堆)上动态分配的所有对象,通过识别它们是否被引用来确定哪些对象是已经死亡的、哪些仍需要被使用。已经不再被应用程序的root或者别的对象所引用的对象就是已经死亡的对象,即所谓的垃圾,需要被回收。

Heap分为3个代龄区域,相应的GC有3种方式: # Gen 0 collections, # Gen 1 collections, #Gen 2 collections。如果Gen 0 heap内存达到阀值,则触发0代GC,0代GC后Gen 0中幸存的对象进入Gen1。如果Gen 1的内存达到阀值,则进行1代GC,1代GC将Gen 0 heap和Gen 1 heap一起进行回收,幸存的对象进入Gen2。

一次垃圾收集简单的说,是按照下面的流程来走的:

1) 挂起那些正在执行.net 调用的线程(比如分配一个对象或修改堆上的对象),执行原生调用的线程在他们返回到托管代码的时候挂起。

2) 决定哪些对象在当前代中是可以被垃圾收集的。通过询问JIT、EE stack walker、handle table和finalize queue来完成哪些对象还在使用中。

3) 删除所有标记删除的对象或在堆还没有合并的时候,添加一个空白到*列表中(free list)

4) 合并移动残留下来的对象到堆的后端(代价最为昂贵)

5) 重新启动所有线程

3.代的概念

2代GC将Gen 0 heap、Gen 1 heap和Gen 2 heap一起回收,Gen 0和Gen 1比较小,这两个代龄加起来总是保持在16M左右;Gen2的大小由应用程序确定,可能达到几G,因此0代和1代GC的成本非常低,2代GC称为full GC,通常成本很高。粗略的计算0代和1代GC应当能在几毫秒到几十毫秒之间完成,Gen 2 heap比较大时,full GC可能需要花费几秒时间。大致上来讲.NET应用运行期间,2代、1代和0代GC的频率应当大致为1:10:100。

二:托管代码、非托管代码

提到GC,与托管代码和非托管代码密不可分。下面对这两者进行一下详细的说明。

1).托管代码:无需也无法人为干预内存回收工作的代码。会自动调用GC进行垃圾回收,我们日常所写的研发程序代码大多数都是分托管代码,没有终结器(Finalize)。

像简单的int,string,float,DateTime等等,.net中超过80%的资源都是托管资源。

说到托管代码的自动垃圾回收机制,有以下几个知识点需要知晓。

首先,什么时候发生垃圾回收?

①.发生垃圾回收的时间有以下情况:

(1)第0代满。

(2)代码显式调用GC.Collect方法 ——System.GC是一个表示垃圾收集器的.NET基类, Collect()方法则调用垃圾收集器。但是,这种方式适用的场合很少,(难道销毁一个对象就让垃圾回收检查一便内存?)

(3)Windows报告内存不足   —— CLR注册了 Win32     CreateMemoryResourceNotification和QueryMemoryResourceNotification监视系统总体内存使用情况,如果收到Window报告内存不足的通知,强行执行GC。

(4)CLR卸载AppDomain

(5)CLR关闭,程序关闭

②关于堆和栈

.NET中的所有类型都是(直接或间接)从System.Object类型派生的。

 CTS中的类型被分成两大类——引用类型(reference type,又叫托管类型[managed type]),分配在内存堆上;值类型(value type),分配在堆栈上。

值类型在栈里,先进后出,值类型变量的生命有先后顺序,这个确保了值类型变量在退出作用域以前会释放资源。比引用类型更简单和高效。堆栈是从高地址往低地址分配内存。

 引用类型分配在托管堆(Managed Heap)上,声明一个变量在栈上保存,当使用new创建对象时,会把对象的地址存储在这个变量里。托管堆相反,从低地址往高地址分配内存。

2).非托管资源:与托管代码形成对立面,这部分资源由垃圾回收器可以跟踪封装非托管资源的对象的生存期,可以人为的干涉内存回收工作,但是不会自动调用GC自行进行垃圾回收,不过还好.net Framework提供了Finalize()方法(终结器),默认情况下,Finalize 方法不执行任何操作。如果您要让垃圾回收器在回收对象的内存之前对对象执行清理操作,您必须在类中重写 Finalize 方法,它允许在垃圾回收器回收该类资源时,适当的清理非托管资源。

在编程中,并不建议进行override方法Finalize(),因为,实现 Finalize 方法或析构函数对性能可能会有负面影响。理由如下:用 Finalize 方法回收对象使用的

内存需要至少两次垃圾回收,当垃圾回收器回收时,它只回收没有终结器(Finalize方法)的不可访问的内存,这时他不能回收具有终结器(Finalize方法)的不可以访问的内存。它改将这些对象的项从终止队列中移除并将他们放置在标记为“准备终止”的对象列表中(即可以理解为去掉了终结器),该列表中的项指向托管堆中准备被调用其终止代码的对象,等到下次垃圾回收器进行回收操作时会将其回收。

与非托管资源相关的几个知识点如下:

①. 以下是几种常见的非托管资源:ApplicationContext,Context,Cursor,FileStream,Font,Icon,Image,Matrix,Object,OdbcDataReader,

OleDBDataReader,Pen,Regex,Socket,StreamWriter,Timer,Tooltip 等等,这些东西相比大家都并不陌生,只是可能在用的时候并未注意过而已。

②.NET Framework 提供 Object.Finalize 方法,它允许对象在垃圾回收器回收该对象使用的内存时适当清理其非托管资源。默认情况下,Finalize 方法不执行任何操作。如果您要让垃圾回收器在回收对象的内存之前对对象执行清理操作,您必须在类中重写 Finalize 方法。

大家由此可以发现在实际的编程中根本无法override方法Finalize(),在C#中,可以通过析构函数自动生成 Finalize 方法和对基类的 Finalize 方法的调用。

例如:

//析构函数
~MyClass()
{
  // Perform some cleanup operations here.
}

//该代码隐式翻译为下面的代码。
protected override void Finalize()
{
  try
  {
    // Perform some cleanup operations here.
  }
  finally
  {
    base.Finalize();
  }
}

③.析构函数
前面介绍了构造函数可以指定必须在创建类的实例时进行的某些操作,在垃圾收集器删除对象时,也可以调用析构函数。由于执行这个操作,所以析构函数初看起来似乎是放置

释放未托管资源、执行一般清理操作的代码的最佳地方。但是,事情并不是如此简单。由于垃圾回首器的运行规则决定了,不能在析构函数中放置需要在某一时刻运行的代码,如果对象占用了宝贵而重要的资源,应尽可能快地释放这些资源,此时就不能等待垃圾收集器来释放了.

对象的Finalizer被执行的时间是在对象不再被引用后的某个不确定的时间。

Finalizer的使用有性能上的代价。需要Finalization的对象不会立即被清除,而需要先执行Finalizer.Finalizer,不是在GC执行的线程被调用。GC把每一个需要执行Finalizer的对象放到一个队列中去,然后启动另一个线程来执行所有这些Finalizer,而GC线程继续去删除其他待回收的对象。在下一个GC周期,这些执行完Finalizer的对象的内存才会被回收。(前文已有提及)

三:关于IDisposable接口

由于GC并不是实时性的,这会造成系统性能上的瓶颈和不确定性。所以有了IDisposable接口,IDisposable接口定义了Dispose方法,这个方法用来供程序员显式调用以释放非托 管资源。使用using语句可以简化资源管理。

Dispose()的执行代码显式释放由对象直接使用的所有未托管资源,并在所有实现IDisposable接口的封装对象上调用Dispose()。这样,Dispose()方法在释放未托管资源时提供了精确的控制。
假定有一个类ResourceGobbler,它使用某些外部资源,且执行IDisposable接口。如果要实例化这个类的实例,使用它,然后释放它,就可以使用下面的代码:

ResourceGobbler theInstance = new ResourceGobbler();
// 这里是theInstance 对象的使用过程
theInstance.Dispose();
如果在处理过程中出现异常,这段代码就没有释放theInstance使用的资源,所以应使用try块,编写下面的代码:
ResourceGobbler theInstance = null;
try
{
theInstance = new ResourceGobbler();
// 这里是theInstance 对象的使用过程
}
finally
{
if (theInstance != null) theInstance.Dispose();
}

即使在处理过程中出现了异常,这个版本也可以确保总是在theInstance上调用Dispose(),总是释放由theInstance使用的资源。但是,如果总是要重复这样的结构,代码就很容易被混淆。C#提供了一种语法,可以确保在引用超出作用域时,在对象上自动调用Dispose()(但不是Close())。该语法使用了using关键字来完成这一工作—— 但目前,在完全不同的环境下,它与命名空间没有关系。下面的代码生成与try块相对应的IL代码:

using (ResourceGobbler theInstance = new ResourceGobbler())
{
// 这里是theInstance 对象的使用过程
}

using语句的后面是一对圆括号,其中是引用变量的声明和实例化,该语句使变量放在随附的复合语句中。另外,在变量超出作用域时,即使出现异常,也会自动调用其Dispose()方法。如果已经使用try块来捕获其他异常,就会比较清晰,如果避免使用using语句,仅在已有的try块的finally子句中调用Dispose(),还可以避免进行额外的缩进。
注意:
对于某些类来说,使用Close()要比Dispose()更富有逻辑性,例如,在处理文件或数据库连接时,就是这样。在这些情况下,常常实现IDisposable接口,再执行一个独立的Close()方法,来调用Dispose()。这种方法在类的使用上比较清晰,还支持C#提供的using语句。

释放未托管资源的两种方式:
●         利用运行库强制执行的析构函数,但析构函数的执行是不确定的,而且,由于垃圾收集器的工作方式,它会给运行库增加不可接受的系统开销。
●         IDisposable接口提供了一种机制,允许类的用户控制释放资源的时间,但需要确保执行Dispose()。

一般情况下,最好的方法是执行这两种机制,获得这两种机制的优点,克服其缺点。假定大多数程序员都能正确调用Dispose(),实现IDisposable接口,同时把析构函数作为一种安全的机制,以防没有调用Dispose()。下面是一个双重实现的例子:

publicclass BaseResource : IDisposable
{
// 指向外部非托管资源
private IntPtr handle;
// 此类使用的其它托管资源.
private Component Components;
// 跟踪是否调用.Dispose方法,标识位,控制垃圾收集器的行为
privatebool disposed =false;
// 构造函数
public BaseResource()
{
// Insert appropriate constructor code here.
}
// 实现接口IDisposable.
// 不能声明为虚方法virtual.
// 子类不能重写这个方法.
publicvoid Dispose()
{
Dispose(true);
// 离开终结队列Finalization queue
// 设置对象的阻止终结器代码
//
GC.SuppressFinalize(this);
}
// Dispose(bool disposing) 执行分两种不同的情况.
// 如果disposing 等于 true, 方法已经被调用
// 或者间接被用户代码调用. 托管和非托管的代码都能被释放
// 如果disposing 等于false, 方法已经被终结器 finalizer 从内部调用过,
//你就不能在引用其他对象,只有非托管资源可以被释放。
protectedvirtualvoid Dispose(bool disposing)
{
// 检查Dispose 是否被调用过.
if (!this.disposed)
{
// 如果等于true, 释放所有托管和非托管资源
if (disposing)
{
// 释放托管资源.
Components.Dispose();
}
// 释放非托管资源,如果disposing为 false,
// 只会执行下面的代码.
CloseHandle(handle);
handle = IntPtr.Zero;
// 注意这里是非线程安全的.
// 在托管资源释放以后可以启动其它线程销毁对象,
// 但是在disposed标记设置为true前
// 如果线程安全是必须的,客户端必须实现。
}
disposed =true;
}
// 使用interop 调用方法
// 清除非托管资源.
[System.Runtime.InteropServices.DllImport("Kernel32")]
privateexternstatic Boolean CloseHandle(IntPtr handle);
// 使用C# 析构函数来实现终结器代码
// 这个只在Dispose方法没被调用的前提下,才能调用执行。
// 如果你给基类终结的机会.
// 不要给子类提供析构函数.
~BaseResource()
{
// 不要重复创建清理的代码.
// 基于可靠性和可维护性考虑,调用Dispose(false) 是最佳的方式
Dispose(false);
}
// 允许你多次调用Dispose方法,
// 但是会抛出异常如果对象已经释放。
// 不论你什么时间处理对象都会核查对象的是否释放,
// check to see if it has been disposed.
publicvoid DoSomething()
{
if (this.disposed)
{
thrownew ObjectDisposedException();
}
}
// 不要设置方法为virtual.
// 继承类不允许重写这个方法
publicvoid Close()
{
// 无参数调用Dispose参数.
Dispose();
}
publicstaticvoid Main()
{
// Insert code here to create
// and use a BaseResource object.
}
}
}

当你用Dispose方法释放未托管对象的时候,应该调用GC.SuppressFinalize。如果对象正在终结队列(finalization queue), GC.SuppressFinalize会阻止GC调用Finalize方法。因为Finalize方法的调用会牺牲部分性能。如果你的Dispose方法已经对委托管资源作了清理,就没必要让GC再调用对象的Finalize方法(MSDN)。

另外Close与Dispose这两种方法的区别在于,调用完了对象的Close方法后,此对象有可能被重新进行使用;而Dispose方法来说,此对象所占有的资源需要被标记为无用了,也就是此对象被销毁了,不能再被使用。

:GC模式与适合的应用程序

一共有三种模式的GC,分别对应优化不同类型的应用程序。

①Server GC

这种类型的GC是针对服务器端高吞吐量和高扩展性进行优化的,那情况是一种长时间的加载和请求不停地分配和重新分配,并维持在较高水准的情况。

这种server GC 使用每个处理器一个堆、一个GC线程,并尽量的保持堆之间的平衡。在垃圾收集的时候,GC线程工作在各自的线程中,这样就最小化了锁资源,就保证了在这种应用条件下最有效的工作。

这种类型的GC只有在多处理器的机器上可见,如果你在单处理器上的设置这种模式,那你将得到实际运行的模式是非并发的workstation版本(Non Concurrent)。现在的双核也是这种模式,intel的超线程技术实现的cpu并不是真实的多cpu,因此它不会使用这种模式。

Asp.net 在多cpu的机器上默认使用这种模式,如果你想使用server GC模式,你可以在应用程序级别上做如下设置:

<configuration>
 <runtime>
   <gcServer enabled="true" />
 </runtime>
< /configuration>

②Workstation GC – Concurrent

这种被用来作为winform应用程序和windows services 服务程序的默认设置。

这种模式是对交互的应用程序,这种程序要求应用程序不能暂停,即时一个相对很短暂的时间也是不行的。因为暂停进程会让用户界面闪烁或者当点击按钮的时候感觉应用程序没有响应。

这种实现方式是当进行Gen 2 收集的时候,将cpu和内存的使用量作为更短的停顿时间。

③Workstation GC – Non Concurrent

这种模式是模仿Server GC,只是收集是发生在引起GC的进程上,这种模式推荐为那种运行在单个cpu上的服务类型的应用程序。可以修改应用程序级上的配置来把 concurrency 关闭。

<configuration>
 <runtime>
   <gcConcurrent enabled="false" />
 </runtime>
</configuration>

 

Concurrent WS

Non-Concurrent WS

Server GC

Design Goal

Balance throughput and responsiveness for client apps with UI

Maximize throughput on single-proc machines

Maximize throughput on MP machines for server apps that create multiple threads to handle the same types of requests

Number of heaps

1

1

1 per processor (HT aware)

GC threads

The thread which performs the allocation that triggers the GC

The thread which performs the allocation that triggers the GC

1 dedicated GC thread per processor

EE Suspension

EE is suspended much shorter but several times during a GC

EE is suspended during a GC

EE is suspended during a GC

Config setting

<gcConcurrent enabled="true">

<gcConcurrent enabled="false">

<gcServer enabled="true">

On a single proc

   

WS GC + non-concurrent

垃圾收集(garbage collection ,GC)的代价是什么,如何保证代价最小?

可以使用一些不同的计数器来衡量一个应用程序的GC的消耗。记住,所有这些计数器都是在收集结束之后更新的,这意味着你使用中会发现过了很长的一个不活动期,那些数据才可靠的。

① NET CLR Memory"% time in GC

这个计数器衡量GC花费的cpu时间的数量,计算方式是: GC时间/自上次GC后的cpu时间

② .NET CLR Memory"# Induced GC

这个是自有人调用GC.Collect()以来的垃圾收集的次数。完美情况下,应该是0,因为含有收集行为表示你花费了很多时间在GC上,而且因为GC不断的调整自己来适应收集模式,而手动的收集会使优化性能降低。

③ .NET CLR Memory"# Gen X collections

这个计数器显示了给定代的收集数量。因为Gen 2 的收集代价比Gen 1 和 Gen 0 要高很多,相对于 Gen1 , Gen 0 你想要更少的 Gen 2 的收集次数,Gen 2 : Gen 1 :Gen 0=1:10:100是比较理想的。

原文参考:

http://blog.csdn.net/directionofear/article/details/8034133

http://www.cnblogs.com/brusegao/archive/2009/03/23/1419816.html

http://blog.csdn.net/jackluangle/article/details/6607086

http://kb.cnblogs.com/page/106720/