绘图(Graphing)
让我们从GC的角度来看这个问题。如果我们的目标是“移除垃圾”,那么我们需要一个计划,使之更有效。显然,我们需要判断什么是垃圾,什么不是(对于有敛癖(pack-rats)的人,这可能就有点痛苦了。)。为了决定什么该被保留,我们首先假定,任何没有被使用的即使垃圾(墙角成堆的旧报纸、阁楼上的舢板、衣柜里所有的东西,等等)。假设我们有2个朋友,Joseph Ivan Thomas (JIT) 和Cindy Lorraine Richmond (CLR)。Joe和Cindy记录哪些是正在使用的,并且将他们需要的做成一个列表给我们。我们将这些原始的列表称之为”根”列表(“root
”list)因为我们将它作为起始点.我们将保留一份主要的列表,用来描述哪些使我们要保留在屋内的。为了使我们的列表能正常工作的,都将被添加到图标中(如果我们保留了电视,就不应该丢掉电视遥控器,因此,遥控器也将被添加到列表里面。如果我们保留了电脑,那么键盘和显示器也将列入”保留”列表)。
这也是GC如何决定该保留哪些东西。它从JIT(即时编译器)和CLR(公共语言运行时)获取一份需要保留的”根”对象引用(还记得Joe和Cindy吧?) ,然后通过递归搜索对象引用以建立一份我们需要保留的图标。
根列表包括:
- 全局的/静态的指针。保证我们的对象没有被垃圾回收的一个方法是保持一个静态变量对其引用。
- 栈上的指针。我们不会丢弃执行应用程序进程所需的东西。
- CPU注册指针。位于托管堆的被CPU的内存地址所指向的任何东西都应该保留。
在上图中,位于托管堆的对象1,3和5被根所引用,其中,1和5是直接引用,3是递归搜索时找到的。类比我们上面的例子,对象1是电视,3就是遥控器。所有的对象都用图形表示出来后,我们将进入下一步,压缩。
压缩
现在我们已经画出哪些对象是我们要保留的,我们可以仅移动要保留的对象来压缩。
幸运的是,在我们的屋里,在我们把其他东西放进来,不需要进行空间清理。既然对象2是不需要的,我们将对象3往下移,同时修复对象1的指针。
下一步,我们将对象5往下移:
既然一切都已清理完毕,我们只需写个便利贴放到压缩的堆上,让Claire知道,新的对象应该放在哪里。
知道GC的本质将有助有我们理解移动对象是非常费力的。正如你所看到的,如果我们能够缩小我们不得不盈动的对象,那将是非常有意义的,我们将改善GC的整个处理过程,因为这将减少拷贝。
托管堆外的对象如何处理呢?
当一个人负责处理垃圾的时候,打扫屋里的时候,我们就会碰到一个问题,如何处理车里面的东西呢?既然要清理,我们就需要清理所有的东西。如果轻便电脑在屋里,但是电池在车上,怎么办呢?
有一些情形,GC需要执行代码用于清除非托管的资源,如,文件、数据库连接、网络连接等,处理这种情形的一种可行方案是通过终止化器(finalizer).
class Sample
{
~Sample()
{
// FINALIZER: CLEAN UP HERE
}
}
当对象被创建的时候,所有的对象连同一个终止化器将一起被添加到终止化列表里。我们假设对象1,4和5有终止化器并且被加到终止化列表。我们来看一下当对象2和4已经不再被程序引用并且将被垃圾回收时,会发生怎样的情况。
对象2将被视为正常情况的处理。对于对象4,GC发现其位于终止化列表,对象4将被移动,同时它的终结器将被添加到特殊的队列里:这个队列叫做终止化可达队列(freachable)
CLR中有一个专属的线程用来处理终止化可达队列中的项目。当对象4的终止化器被这个线程执行后,它将被移出终止化可达队列,当且仅当这个时候,对象4才准备被回收。
因此,对象4将一直存在,直到下一轮的GC操作。
由于添加一个终结器到我们的类里面将为GC增加额外的工作,这将给垃圾回收和我们的程序的性能带来昂贵的和不利的影响。只有当你非常确定的时候,才使用终结器。
一个好的实践是,确保清楚非托管的资源。你可以预见到的,最好是明确的关闭连接,并尽可能用IDisposable接口来清除而不是用终结器。
IDisposable
实现了IDisposable接口的类将在Dispose()方法(该接口的唯一签名)中进行资源清理。因此,如果我们有一个ResourceUser,不采用如下的终结器:
public class ResourceUser
{
~ResourceUser() // THIS IS A FINALIZER
{
// DO CLEANUP HERE
}
}
我们可以使用一种更好的方式IDisposable,来实现相同的功能:
public class ResourceUser : IDisposable
{
#region IDisposable Members
public void Dispose()
{
// CLEAN UP HERE!!!
}
#endregion
}
IDisposable 被集成到了using关键字中。在using块的最后,将调用在using中声明的对象的Dispose()方法。该对象在using代码块之后不应再被使用,因为它应该是不存在的,等待垃圾回收的。
public static void DoSomething()
{
ResourceUser rec = new ResourceUser();
using (rec)
{
// DO SOMETHING
} // DISPOSE CALLED HERE
// DON'T ACCESS rec HERE
}
我更喜欢将对象的声明放到using里面,因为它更容易理解,并且rec在using代码块外将不再可用。这种在写成一行的模式更符合IDisposable的意图,但他不是必须的。
public static void DoSomething()
{
using (ResourceUser rec = new ResourceUser())
{
// DO SOMETHING
} // DISPOSE CALLED HERE
}
通过对实现了IDisposable的类使用using(),我们可以执行资源清理工作,而不用强迫GC终结我们的对象付出额外的负担。
静态变量:小心!
class Counter
{
private static int s_Number = 0;
public static int GetNextNumber()
{
int newNumber = s_Number;
// DO SOME STUFF
s_Number = newNumber + 1;
return newNumber;
}
}
如果有2个线程同时调用了GetNextNumber()方法,并且都在s_Number增加之前给newNumber赋了相同的值,那么他们将返回相同的结果。有一种方法可以保证在同一时间里,只有一个线程可以进入到一个代码块里。作为最佳实践,你应该锁住尽可能小的代码块,因为其他线程必须排队等候lock住的代码。而这将是低效的。
class Counter
{
private static int s_Number = 0;
public static int GetNextNumber()
{
lock (typeof(Counter))
{
int newNumber = s_Number;
// DO SOME STUFF
newNumber += 1;
s_Number = newNumber;
return newNumber;
}
}
}
静态变量:小心!…第二种情形
下一个我们需要注意的事情是被静态变量所引用的对象.记住,任何被”根”所引用的对象将不会被清除。这是我能想到的最丑恶的代码:
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();
}
}
对于Olympics类来说,Runner集合是静态的,不仅是集合中的对象不会被垃圾回收(它们被根间接引用),也许你也注意到了,每次我们执行GetStats的时候,stream就对文件开放,由于我们没有关闭,并且GC也不会释放,这段代码将会发生一场灾难。想象下,我们有100,000个runners。那么我们将以许多无法回收的对象,并且每个对象都有开放的资源而结束。天啊,糟糕的性能。
单例
保留东西轻量的一个有效途径是总是在内存中保存类的一个实例。一种简单的方式是使用GOF的单例模式。单例应该谨慎使用,因为他们是真正的”全局变量”,并且在多线程中,将导致许多头疼和”奇怪”的行为,因为不同的线程都可能改变对象的状态。如果我们使用单例模式(或其他全局变量),我们应该能够证明这是正当的用法。(换句话说,如果没有好的理由,请不要使用它。)
public Earth{
private static Earth _instance = new Earth();
private Earth() { }
public static Earth GetInstance() { return _instance; }
}
我们有一个私有的构造函数,因此只有在Earth里面可以调用构造函数来构建Earth,我们还有一个Earth的静态实例和一个GetInstance方法,这种特别的实现方式是线程安全的,因为CLR保证静态变量的创建是线程安全的。这是我在C#里面法相的单例的最优雅的实现方式。
总结:
总结包装一下,提升GC性能,我们可以做的事情有:
- 清除。不要留下打开的资源。确保关闭所有的连接并且尽可能的清楚所有的非托管对象。作为使用非托管对象的一个通常规则,尽可能晚的实例化,并尽早的清除。
- 不要过度引用。使用引用对象要有理由。记住,如果我们的对象还存活,所有它引用的对象将不会被回收(如此循环下去)。当完成对类的某个引用,我们可以将其设成null来移除引用。我更倾向的一种严格的方法是,将不再需要的引用设置成自定义的轻量型的NullObject以防止空引用异常。GC开始工作的时候,留下越少的引用,操作处理将越没有压力。
- 慎重使用终结器。GC过程中,终结器开销是很大的,只有当我们可以证明这是合法的,才应该使用终结器。如果我们可以使用IDisposable代替终结器,那将更加有效,因为我们的对象将在一轮GC中被清除,而不需要第二轮。
- 保持对象和子对象一起。对于GC来说,相对于每次处理堆上的碎片来说,移动大块的内存时相对容易的。因此,当我们声明一个包含多个组合对象的对象时,应将这些组合对象尽可能在一个地方实例化。
The End.