我所理解的Remoting(2):远程对象生命周期的管理[上篇]
1.CLR的垃圾回收机制
在.NET中提到对象的生命周期,我们会不由自主地想到CLR的垃圾回收。在运行一个.NET程序过程中,我们通过某种方式,比如通过new操作符,通过反序列化,通过反射机制,创建一个对象,CLR在为这个对象在托管堆中开辟一块内存空间。随着程序的运行,创建的对象越来越多,托管堆中的可用的内存越来越少,必须有一种机制来判断被分配在托管堆中的对象那些已经不被使用,以及进行对这些对象占用的内存进行回收。这种机制被称为CLR自动内存管理,也就是我们常说的垃圾回收。为了说清楚远程对象的生命周期管理,我们 得首先了解本地对象的生命周期。
首先我们来说说CLR如何判断分配在托管堆中的对象那些是可以被垃圾回收的。我想我们应该可以想象得到,在程序运行的某个时刻,以下一些对象是会在后续的运行中会时候使用到的:一个类的静态字段,一个全局变量,调用方法堆栈中存储的方法参数和创建的局部变量,CPU 寄存器。我们一般把这些对象称为根(root),所有的根和被它直接引用或间接引用的对象,我们都认为是不应该被回收的。在CLR的内部维持着一个特殊的数据结构,这个表的每个条目对应一个跟,当CLR加载的时候,该表被创建。随着程序的不断运行,新的根会被加进来,已经不是跟的会被删除掉,所用这个表可以看作根的实时的反应。当垃圾回收器进行工作的时候(一般是第0代对象所对应的托管堆充满的时候,当然你也可以手动地调用GC.Collect方法启动垃圾回收。),它首先在托管堆上扫描,如果发现该的内存快所对应的对象是一个根,它会在该对象同步快索引(synchronous block index)字段做一个特殊的标记,表明它当前还不可以被垃圾回收,接着他会一递归的方式标记所有 被它直接或者间接引用的对象。所以当扫描完毕之后,那些被标记的对象就是在当前不可以被当成垃圾回收的对象,这些对象一般称为可达对象(reachable object,因为可以通过根来找到),反之,除此以外的对象则就是垃圾对象了。
标记可达对象只是垃圾回收的第一步,第二步才是对这些未被标记的垃圾对象进行回收。在开始之前我们必须能够分别两种不同的对象,一种称为可终结(Finalizable)对象和非可终结对象。我们知道,在很多情况下,我们的对象会引用一些非托管的资源,比如一个文件的句柄,一个数据库连结,或者一个Socket连结等等。在我们回收这些对象的时候,如果没有这些非托管的资源进行相应的终结操作的话,很有可能造成内存的泄漏。在.NET中,我们通常把这些终结操作写在一个特殊的方法中,叫做Finalize。在C#中我们一般定于在~ClassName()的形式,并且沿用C++ 的说法,称它为析构函数(我不推荐这么称呼,Finalize方法和C++的析构函数是不同的)。如果你有兴趣查看C#编译之后生成的IL代码,你会发现定义成~ClassName()的方法的名称就是Finalize。我们把相应的类定义了Finalize方法的对象叫做可终结的对象。说到可终结对象,这里我们又需要引入一个新的垃圾回收器维护的数据结构,这是的链表,用于保存未被回收的可终结对象,一般称为终结链表。
接下来我们看看垃圾回收器如何进行垃圾回收的。垃圾回收器开始再一次扫描托管堆,对于在第一步作了标记的对象,不予理睬。对于未作标记的对象,首先判断是否是可终结对象(看看在终结链表中是否有相应的对象),如果不是,直接回收掉。否则垃圾回收器还要先判断是否已经进行了终结操作,如果没有则会把它从终结链表中移除,把对象放入另一个称为终结可达对列(freachable queue——f代表finalizable,注意这里又引进了一个新的数据结构)。如果已经进行了终结操作,则直接进行回收就好了。
对于放入终结可达对列对象,我们必须在对它进行垃圾回收之前收前进行终结操作。从广义来讲终结可达对列中的对象也是一个根,所以被放入终结可达对列中的对象是不应该被垃圾回收的,由于终结操用涉及到线程同步的问题,所有的终结操作都在一个具有较高优先级的线程上进行。这个线程在终结可达对列为空的时候处于休眠的状态,一旦垃圾回收器把一个可终结对象放入这个终结可达对列的时候,这个特殊的线程立即被唤醒,调用该对象的Finalize方法并把它从终结可达对列中移除。等再一次进行回收的时候,对于这些经过终结操作对象已经成为垃圾对象——不会有任何的根,包括终结可达对列引用它,这个时候垃圾回收器可以对它进行回收了。
从垃圾的整个过程来看,如果我们重写了Finalize方法使之成为一个可终结类型,这种对象实际上要经过两次垃圾回收才会被真正地回收调——其中一次放入终结可达对列,另一次才真正被回收调。所以,我们在定义某个类型的时候,如果没有必要重写Finalize方法就千万不要重写它,不然会加重内存的压力,降低应用的性能。
2.基于租约(Lease)的生命周期管理机制
在前面我们简单地讲述了CLR垃圾回收机制。按照这种机制,如果我们要阻止一个对象被垃圾回收,我们必须让它被某个根直接或间接引用,而这个引用它的对象一般应该和该对象处于同一个Application Domain之中。所以这种机制不适合我们分布式环境。在一个分布式应用来说,服务器段对象通过Marshaling从Sever端的Application Domain传到Client所在的Application Domain。无论采用哪种Marshal方式,By Value 或者By Reference,Marshal本身是不会为远程对象在Server端的Application Domain创建一个引用的。如果这样的话,对于垃圾回收器来说,远程对象便成了一个垃圾对象,在进行垃圾回收的时候,必然会被回收掉。如果Client端调用一个已经被回收的远程对象,对于Client Activated Object,会抛出异常,如果对于Singleton模式的Server Activated Object,Remoting Framework会重新创建一个新的对象,原有的状态将不复存在。所以我们必须有一种机制来控制远程对象的生命周期。这就是我们现在讲的基于租约(Lease)的生命周期管理。
在正式讲述之前,我首先我讲一个现实生活中的一个困难不是很恰当的例子,一个租房的例子。
去年9月份,我从苏州来到上海,通过中介租了一间房子,很巧的是这个中介就是我的房东,并且我是和房东合租的。当时觉得不是太满意,所以签合同的时候只签了半年。在租期还没有到期的时候,我有权向房东提出续租,租期可以使半年,也可以使一年。如果到期了,我没有提出续租,房东肯定会主动和我联系,询问我时候有续租的打算,如果我提出续租,考虑到我们房东和和睦相处的关系,我们可以在不需要签订新的合同的情况下让我续租。否则我走人,我的房间被转租出去。如果房东在找我的时候,可能我出差了,手机又换号了,联系不到我,这时候,他肯定会打联系我的女朋友,他们也很熟,如果我的女朋友说要续租,房东便会在没有获得我答复的情况下,给我续租。但是现在租期已经到期了,我也没有提出续租,房东也没有叫我续租,但是我还是每个月给他交房东,虽然合同已经在法律的意义上失效了,但是我和清楚,我交了下个月的房租,我的房间到下个月底使用权归我。这就是我租房的故事(呵呵)。大家注意这样故事中的几个实体,合同,房东兼中介,房间,我(合同上的承租者),我女朋友。
现在我们再来讲Remoting关于Lease的对象生命周期的管理机制。当远程对象被激活和Marshal的时候,处于Server端Application Domain的Lease Manager会为该远程对象创建一个Lease。就相当于中介和我签了一份租房合同,中介相当于Lease Manager,我(承租者)相当于客户端,而房间就是这个远程对象,Lease则代表我们签订的租房合同。就像合同会写明租期一样,Lease也会有一个初始的时间(InitialLeaseTime)代表远程对象的初始生命周期。就像我可以在租期到期之前可以自动提出延长租期一样,Client可以通过这个Lease来延长对应远程对象的生命周期。不过和租房的例子不同的是,Server端也可以具有相同的权利。
就像我可以通过交房租来延长一个月的租期一样,远程对象可以通过来自Client端的调用来延长这个Lease,这个时间由属性RenewOnCallTime来表示。不过有一点值得注意的是,就像我在租期到了的那个月之前交房租这个行为不会延长租期(始终是6个月),只有我在第6个月月底交房租才会把实际的租期延长到7月个。在Remoting中,只有在RenewOnCallTime大于Lease剩下的时间的时候,这个RenewOnCallTime才会起作用。
就像我可以让房东在我不在的时候,找我的女朋友来代表我一样,在Remoting中,Client可注册一个Sponsor,这个Sponsor有权代表Client延长租房期限,当然Client有权利取消这个注册的Sponsor,就像有一天我和女朋友分手了,她就没有这样的权利了。随着时间的推移,当Lease的过期了,Lease Manager会首先通过远程调用(可以把这种情况看成一种Callback),从Client获得Client为相应远程对象注册的Sponsor,找到了之后,通过这个Sponsor设置的时间来延长远程对象的生命周期。但是,我们已经说了,Lease Manager获得Sponsor是一种远程调用,可能他们处在不同的Application Domain,不同的Process,不同的Machine,甚至处于Internet的两端。这个调用是否成功和调用的时间是不确定的,所以这里必须给定一个特定的时间来规定,在某一段限定的时段内,如果不能获得相应的Sponsor则认为该Sponsor不可得。否则始终这个调用下去也不是个事儿。就像房东在房租到期一个月之内还找不到我和我女朋友,关系再好也必须把房间转租出去了。对于Lease来说,这个时间通过SponsorshopTimeout属性表示。
这里还有一个重要的时间,那就是Lease Manager每个多少时间扫描Lease——LeaseManagerPollTime。
上面的所有的时间都是可以定制的,我们现在看看,如何定制这些时间。
1. 通过编程的方式:通过设置System.Runtime.Remoting.Lifetime. LifetimeServices静态属性。
LifetimeServices.LeaseTime = TimeSpan.FromMinutes(2);
LifetimeServices.RenewOnCallTime = TimeSpan.FromMinutes(2);
LifetimeServices.SponsorshipTimeout = TimeSpan.FromMinutes(2);
2. 通过Configuration的方式
leaseManagerPollTime="7S" />
3. 定制单个MarshalByRefObject 对象的Lease 时间:Override 继承自MarshalByRefObject 的InitializeLifetimeService方法。
{
ILease lease = (ILease)base.InitializeLifetimeService();
if (lease.CurrentState == LeaseState.Initial)
{
lease.InitialLeaseTime = TimeSpan.FromSeconds(1);
lease.RenewOnCallTime = TimeSpan.FromSeconds(1);
lease.SponsorshipTimeout = TimeSpan.FromSeconds(1);
}
return lease;
}
通过上面的讲述,我的应该对Remoting对象生命基于Lease和Sponsorship的生命周期的管理有一个感性的认识。实际上Sponsorship远非这么简单,对Sponsorship的深入探讨,有兴趣话可以关注:[原创]我所理解的Remoting (2) :远程对象的生命周期管理-Part II .
Reference:Jeffery Richiter 《CLR via C#》
相关内容:
[原创]我所理解的Remoting(1):Marshaling & Activation - Part I
[原创]我所理解的Remoting(1):Marshaling & Activation - Part II
[原创]我所理解的Remoting(2):远程对象生命周期的管理—Part I
[原创]我所理解的Remoting (2) :远程对象的生命周期管理-Part II
[原创]我所理解的Remoting(3):创建CAO Service Factory使接口和实现相互分离
[原创].NET Remoting: 如何通过Remoting实现双向通信(Bidirectional Communication)