前言
虽然在.Net Framework 中我们不必考虑内在管理和垃圾回收(GC),但是为了优化应用程序性能我们始终需要了解内存管理和垃圾回收(GC)。另外,了解内存管理可以帮助我们理解在每一个程序中定义的每一个变量是怎样工作的。
这一节我们将介绍垃圾回收机制GC以及一些提搞程序性能的技巧。
绘图Graphing
让我们站在GC的角度研究一下。如果我们负责“扔垃圾”,我们需要制定一个有效的“扔垃圾”计划。显然,我们需要判断哪些是垃圾,哪些不是。
为了决定哪些需要保留,我们假设任何没有正在被使用的东西都是垃圾(如角落里堆积的破旧纸张,阁楼里一箱箱没有用的过时产品,柜子里不用的衣服)。想像一下我们跟两个好朋友生活在一起:JIT 和CLR。JIT和CLR不断的跟踪他们正在使用的东西,并给我们一个他们需要保留的东西列表。这个初始列表我们叫它“根(root)”列表。因为我们用它做起点。我们将保持一个主列表去绘制一张图,图中分布着所有我们在房子中需要保留东西。任何与主列表中有关联的东西也被画入图中。如,我们保留电视就不要扔掉电视遥控器,所以电视遥控器也会被画入图中。我们保留电脑就不能扔掉显示器键盘鼠标,同样也把它们画入图中。
这就是GC怎么决定去保留对象的。GC会保留从JIT和CLR那收到的一个根(root)对象引用列表,然后递归搜索对象引用并决定什么需要保留。
这个根的构成如下:
- 全局/静态 指针。通过以静态变量的方式保持对象的引用,来确保对象不会被GC回收。
- 栈里的指针。为了程序的执行,我们不想扔掉那些程序线程始终需要的对象。
- CPU寄存器指针。托管堆里任何被CPU内存地址指向的对象都需要被保留。
在上面的图中,托管堆中的对象1,5被根Roots引用,3被1引用。对象1,5是被直接引用,3是通过递归查询找到。如果关联到我们之前的假设,对象1是我们的电视,对象3则是电视遥控器。当所有对象画完后,我们开始进行下一阶段:垃圾清理。
GC垃圾清理Compacting
现在我们有了一张需要保留对象的关系图,接下来进行GC的清理。
图中对象2和4被认定为垃圾将被清理。清理对象2,复制( memcpy )对象3到2的位置。
由于对象3的地址变了,GC需要修复指针(红色箭头)。然后清理对象4,复制( memcpy )对象5到原来3的位置(译外话:GC原则:堆中对象之间是没有间隙的,以后会有文章专门介绍GC原理)。
最后清理完毕,新对象将被放到对象5的上面(译外话:GC对一直管理一个指针指向新对象将被放置的地址,如黄色箭头,以后会有文章专门介绍)。
了解GC原理可以帮助我们理解GC清理(复制 memcpy ,指针修复等)是怎么消耗掉很多资源的。很明显,减少托管堆里对象的移动(复制 memcpy )可以提高GC清理的效率。
托管堆之外的终止化队列Finalization Queue和终止化-可达队列Freachable Queue
有些情况下,GC需要执行特定代码去清理非托管资源,如文件操作,数据库连接,网络连接等。一种可行性方案是使用析构函数(终结器):
class Sample
-
{
-
~Sample()
-
{
-
// FINALIZER: CLEAN UP HERE 终结器:在这里清理
-
}
-
}
译外话:析构函数会被内部转换成终结器override Finializer()
有终结器的对象在创建时,同时在Finalization Queue里创建指向它们的指针(更正原文说的把对象放到Finalization Queue里):
上图对象1,4,5实现了终结器,因此在Finalization Queue里创建指向它们的指针。让我们看一下,当对象2和4没有被程序引用要被GC清理时会发生什么情况。
对象2会被以常规模式清理掉(见文章开始部分)。GC发现对象4有终结器,则会把Finalization Queue里指向它的指针移到Freachable Queue中,如下图:
但是对象4并不被清理掉。有一个专门处理Freachable Queue的线程,当它处理完对象4在Freachable Queue里的指针后,会把它移除。
这时对象4可以被清理了。当下次GC清理时会把它移除掉。换句话说, 至少执行 两次GC清理才能把对象4清理掉,显然会影响程序性能。
创建终结器,意味着创建了更多的工作给GC,也就会消耗更多资源影响程序性能。因此,当你使用终结器时一定要确保你确实需要使用它。
更好的方法是使用 IDisposable接口。
public class ResourceUser : IDisposable
-
{
-
#region IDisposable Members
-
public void Dispose()
-
{
-
// 在这里清理!!!
-
}
-
#endregion
-
}
实现 IDisposable接口的对象可以使用using关键字:
using (ResourceUser rec = new ResourceUser())
{
// 具体实现。。。
} // {}代码块结束时,会调用DISPOSE方法
变量rec的作用域是大括号内,大括号外不可访问。
静态变量
-
class Olympics
-
{
-
public static Collection<Runner> TryoutRunners;
-
}
-
class Runner
-
{
-
private string _fileName;
-
private FileStream _fStream;
-
public void GetStats()
-
{
-
FileInfo fInfo = new FileInfo(_fileName);
-
_fStream = _fileName.OpenRead();
-
}
-
}
如果你初始化了TryoutRunners,那么它将永远不会被GC清理,因为有静态指针一直指向初始化的对象。一旦调用了Runner里GetStats()方法,因为GetStats()里面没有文件关闭操作,它将永远被打开也不会被GC清理。我们可以看到程序的崩溃即将来临。
总结
一些良好的操作可以提高程序的性能:
- 清理。不要打开资源而不关闭它。关闭所有你打开的连接。尽可能快的清理所有非托管资源。一般规则:使用非托管对象,初始化越晚越好,清理越早越好。
- 不要过度引用。合理使用引用对象。如果某一个对象还存在没有被GC清理,所有它引用的对象都将不会被GC清理,如此递归下去。。。当我们完成使用一个引用对象时,把它设为NULL(视你的情况而定,注意不要产生空引用异常)。当引用少了,GC开始创建清理关系图graphing时过程就简单一些了,进而提高程序性能。
- 谨慎使用终结器Finalizaer或析构函数。能使用IDisposible代替就使用IDisposible。
- 保持对象及其成员的紧凑。如果声明一个对象并且它由多个子对象组成,尽可能的把它们放在一起初始化,好让它们所在的内存空间紧凑。GC复制这样的一大块内存比复制分散的内存碎片要容易。
译外话:
我会在以后的文章里更详细的介绍GC垃圾回收机制,包括GC划分的0代generation 0,1代generation 1,2代generation 2。有时只有一篇文章或一种图解还是会让人迷惑,所以下一篇介绍GC垃圾回收的内容更详细,图解也有不同。
翻译:http://www.c-sharpcorner.com/UploadFile/rmcochran/csharp_memory_401282006141834PM/csharp_memory_4.aspx