RMI运行时说明

RMI运行时环境在客户端和服务端都扮演了重要的角色.在这种架构中,stub达到了三种目的:
1.它是序列化的,通过网络可以从服务端向客户端发送.也可以包含数据使其可以稳定地向服务端发送消息.
2.它是服务端的代理,客户端可以把stub当作是服务器.
3.它可以池化socket.每次方法调用的时候,stub就向RMI运行时请求一个特别服务的连接.这使得RMI可以在多个请求之间重用sockets(即可以共享socket).

在多个请求之间重用socket,可能会导致下面的问题:

如果一个socket被多个客户端的stubs重用,它们每一个都向不同的服务器发送了消息,那么这些消息之间应该有某种意义上的区别.那么应该如何来区分呢?

在RMI中,这种区分是通过使用java.rmi.server包下的ObjID类来进行的.

ObjID在javadoc中是这样定义的:

ObjID被唯一用来标识远程对象.每一个标识符都包含一个对象数(类型为long)和一个地址空间标识符(类型为UID,对于特定主机是唯一的).

对象标识符是在远程对象被导出时分配的.如果java.rmi.server.randomIDs属性设置为true的话,通过无参构造函数创建的ObjID对象的64位对象树将包含一个加密的强随机数.

因此,ObjID的实例可以唯一地标识服务器JVM中的特定服务.同时,ObjID类实现了序列化接口,因此,在每个远程方法调用时,其实例都可以被序列化.

在服务端,RMI运行时使用ObjID的反序列化来找出远程方法调用将要调用的skeleton.

RMI是怎样解决引导问题的?

大部分服务器在过去的使用中都是通过随机分配ObjID的实例来处理的.在其实例中有三种保留的标识符:ACTIVATOR_ID(),DGC_ID和REGISTRY_ID.

这三种IDs分别对应RMI提供的三种服务:激活构架,分布式垃圾收集和RMI注册表.

为了能够完全解决引导问题,RMI注册表需要预知的一个唯一标识符.

当一个客户端第一次试图连接RMI注册表的时候,除非它知道与服务器相关的对象标识符,否则此客户端就不能向服务器端发送消息.

也就是说,因为服务器端的RMI运行时需要一个ObjID的实例来决定使用哪个合适的服务器来响应客户端的请求,客户端为了能连接注册表,它必须要知道运行在服务器JVM中注册表的ObjID.

为了解决这个问题,RMI的设计者们定义了ObjID的保留实例-如果一个注册表运行在一个虚拟机中,它就使用REGISTRY_ID.否则,不允许任何服务器使用REGISTRY_ID.

这解决了RMI引导问题.Naming的静态方法接受一个主机和端口;构建stub时仅需的附加信息是stub连接服务器的Object ID.

由于注册表总是使用REGISTRY_ID,注册表的stub在连接注册表之前总是能在客户端被完全地构建.

这种策略也意味着,一个JVM只能导出一个注册表,这是因为不能在多个服务器间共享ObjID.

分布式垃圾收集

另一个固定的标识符是DGC_ID.这是内置于RMI中的关于分布式垃圾收集的对象标识符.

垃圾收集的基本想法是定义一系列的可到达对象,并丢弃那么不可达对象.可达对象是递归定义的:

1.每个活动线程当前都处在一个方法中或处在未知对象的实例中.那么线程所在的实例就是可到达的.

2.每个线程能够立即找到被方法级变量和实例字段引用的对象,这些对象也是可到达的.

3.从这些立即可访问的对象(此对象引用了其他对象),能够访问到其他可到达的对象.

诸如此类,一般来说,如果一个线程,从它当前位置开始,能够最终找到某个对象,那么此对象就是可达的.

在单个JVM中垃圾收集机制工作得很好.但在分布式系统中,stub问题将会出现.如果一个客户端拥有一个服务器端的引用 ,那么服务器应该是可到达的.

由于所有的stub真正拥有的是一个ObjId的实例,这意味着RMI运行时必须保持所有活动服务器的引用.为了垃圾回收,客户端的RMI运行时必须以某种方法让服务端运行时知道stub何时不再被使用.

最明显的方式是使用分布式引用计数.也就是,强制客户端stub发送两条额外的信息给服务器.

当stub被实例化发送一条消息,以便让服务器知道有一个活动的客户端(计数器加1).当stub释放的时候,再发送一条消息,以通知服务器客户端已经使用完了服务端(计数器减1).

在这种方案中,最脆弱的是第一步.下面的每一个问题都可能增加计数的困难度:

1.客户端垃圾收集算法不能保证立即回收stub.如果客户端的内存充裕,且正忙于处理具有高优先级的任务,那么垃圾收集暂时可能不会发生.在这段时期之内,客户端将隐含地强制服务端保持那些没有必要的资源.

2.客户端可能会崩溃(crash).假设一个客户端崩溃了的话,那么第二条消息将无法发送,这样就会导致服务端引用计数无法变为0,且RMI运行时将永远保持服务对象为活动状态.

3.可能会出现网络问题.即使客户端是好的,且能发送消息,但网络问题也可能出现.在这种情况下, RMI运行时决不将服务端引用减为0,这样就导致了服务端对象一直处于活动状态.


在这三个问题中,第一个问题是不可能解决的.Java语言规范明确的表明本地垃圾收集是不可依赖的.垃圾收集将来可能发生,但没有一种方式来强制它在某一时间段内发生.

在垃圾收集运行之前,没有一种方法知道一个stub变成了不可引用,因此任何一个分布式引用计数架构将不得不接受第一问题的现实.

第二和第三个问题可以通过将所有分布式引用作为临时引用来消除.其基本思想被称为租赁.基本算法如下:

1.客户端调用服务器并请求一段时间的租期.

2.服务端响应,并同意一段时间的租期(不一定与客户端请求的租期相同)

3.在这段时间之内,分布式引用计数将包含客户端(即增加计数1)

4.当租期期满的时候,如果客户端请求没有延长期限的话,则分布式应用计数将会自动减少(即减少计数1).

只要stub没有被垃圾回收的话,客户端就会自动尝试续期租赁.通过这种算法,可以灵巧地解决第二,第三个问题.

如果客户端崩溃了的话,即客户端就不再运行了,那么客户端就不能再续期租赁,结果就会导致服务器最终将其回收掉.

相似地,如果因网络问题阻止了客户端程序连接服务器,客户端也不会续期到租赁,因此服务端最终也会对其进行垃圾回收.

默认地租期为10分钟,可以通过java.rmi.dgc.leaseValue属性进行设置,时间单位为毫秒.

真实的分布式垃圾收集器

分布式垃圾收集器是一个RMI服务器程序,它实现java.rmi.dgc.DGC接口(此接口继承自java.rmi.Remote接口).此接口只包含两个方法声明:

public void clean(ObjID[] ids,long sequenceNum,VMID vmid,boolean strong)

public Lease dirty(ObjID[] ids,long sequenceNum,Lease lease)

clean()方法是由客户端在不需要服务端引用的时候,由它的运行时调用的.严格来讲,调用clean()方法是没有必要的-客户端运行时可能不续期租赁而达到相同的目的.

但是,租赁应该看作是服务器清理机制的最后一道防线(通过它可以在适当的位置减少网络和客户端失败造成的损害).

客户端运行时可以通过调用dirty()方法来获得一个租赁.不需要直接传递一个VMID(虚拟机ID,即一个JVM的唯一标识符)给dirty()方法,因Lease的实例已经传递了VMID(通过其构造函数可以看出).

注意:对于一个特定的JVM来说,它只能有一个分布式垃圾回收器,其对象标识符为DGC_ID.

Unreferecnced接口

分布式垃圾回收器负责维持租赁.在服务器端,当特定服务器的所有未解决租赁过期的时候,分布式垃圾回收器确保RMI运行时不再保留服务端引用.

因此服务端就可以被回收.在此处理过程中,如果服务端(远程对象)实现了Unreferenced接口,那么服务端在没有多个引用该对象的客户机时接收通知.Unreferenced接口只包含一个方法:

public void unreferenced()

当服务器需要立即释放资源而不是等待垃圾回收发生时,此接口非常重要.同时,它也是一个持久化代码的便利钩子-因为服务端知道不再有远程方法调用,所以它能将状态安全地存储到一个持久化介质(如,一个关系型数据库).

RMI日志

除了分布式垃圾回收之外,RMI运行时也包含广泛的日志,这些日志可以让你跟踪应用程序的行为.RMI中有三种不同的日志类型:标准日志,专门日志(包含5种类型),调试日志

标准日志

标准日志用于在服务端记录方法调用和异常信息.标准日志的使用是很容易的,可以通过设置java.rmi.server.logCalls系统属性(值为boolean类型)来启用或关闭.此属性可通过命令行或程序进行设置.如:

命令行:

Java -Djava.rmi.server.logCalls=true


程序:

System.getProperties().put("java.rmi.server.logCalls","true");


一旦你启用了日志,你可以配置日志的输出目的地.可以通使用java.rmi.server.RemoteServer类的静态方法进行设置:

public static PrintStream getLog()
用于获取当前的日志的流

public static void setLog(OutputStream out)
设置日志的输出流.

缺省情况下,日志系统使用System.err.也就是说,如果你没有设置标准日志输出目的地的话,它将会在System.err上显示.

专门日志
RMI有五种类型的专门日志用于记录运行时的特定方面.这些日志是transport log(传输日志),proxy log(代理日志),loader log(负载日志),DGC log(分布式垃圾回收日志)以及TCP log(tcp日志).

它们均为java.rmi.server.LogStream(此类从JDK1.3版本时已经被废弃)的实例.为了得到与日志相关的LogStream,可以调用其静态方法:public static LogStream log(String name).

一旦你有了LogStream的实例,就可以使用其 setOutputStream()来设置其输出目的地.如:

FileOutputStream transportLogFile = new FileOutputStream("d:/log/transportFlogFile");
LogStream.log("transport").setOutputStream(transportLogFile):


这些日志通过6个系统属性来操作,每一个属性都接受三个设置:silent(不记录信息),brief(记录少量信息),verbose(记录大量信息),可以使用s,b,v是上述三个值的简写形式.

此6个属性如下:(以下属性因为已被废弃,且属sun公司专有属性,具体含义可查看参考资料)

sun.rmi.server.dgcLevel

sun.rmi.server.logLevel

sun.rmi.loader.logLevel

sun.rmi.transport.logLevel

sun.rmi.transport.tcp.logLevel

sun.rmi.transport.proxy.logLevel

调试日志

同标准日志一样,可以通过设置系统属性sun.rmi.log.debug的boolean值来开启或禁用此功能.但不同于标准日志的是,调试日志总是将信息显示在System.err上.调试日志被用于激活框架的守护线程中.

基本RMI参数

java.rmi.server.randomIDS

其属性值为boolean型.当设置为true时,它强制RMI运行时为新导出的服务对象生成加密的安全对象标识.默认为false.

sun.rmi.server.exceptionTrace

其属性值为boolean型.当其设为true的时候(默认为false),所有的异常将会被打印到System.err上.反之,则不打印.

传输层参数

传输层是底层参数,它能够直接影响RMI底层的sokets的使用和TCP/IP的配置.有三个传输层参数:

sun.rmi.transport.connectionTimeout

用于设置在RMI关闭之前,socket的休眠时间.默认为15秒,在较慢的网络中,应该加大此值。

必须在客户端和服务器端同时设定此值,因为两端都试图重用同一个socket.如果服务端设为60秒的话,客户端设为15秒的话,那么实际上服务端设置的参数将会被客户端参数所否决。

sun.rmi.transport.tcp.readTimeout

设置底层socket读取超时时间,此参数的值实际上只能通过直接传递给socket的方式进行设值(通过socket的setSoTimeOut()).此参数的默认值为2个小时(7200,000毫秒)。

sun.rmi.transport.proxy.connectTimeout

用于设置在两个JVM建立连接时RMI等待的时间。默认为15秒。

注:上述参数的值单位均为毫秒

影响垃圾回收的参数

java.rmi.dgc.leaseValue
此参数只影响服务端,它被用来设置标准的租赁时间。时间单位为毫秒,默认为10分钟。

sun.rmi.dgc.client.gcInterval
此参数用于配置客户端运行时行为。即RMI检测stub是否活动的时间周期。当stub不再被引用的时候,客户端运行时就会发送一个clean()消息给服务端运行时。单位为毫秒,默认为1分钟。

sun.rmi.dgc.server.gcInterval
服务端对于分布式垃圾回收的刷新频率。此参数用于控制检查客户端clean()的动作频率,并试图决定是否调用unreferenced()方法。此值单位为毫秒,缺省为1分钟。

sun.rmi.dgc.checkInterval
用于指定RMI检查过期租赁的频率。此值单位为毫秒,缺省为5分钟。

sun.rmi.dgc.cleanInterval
此参数是客户端的重试参数。当客户端调用clean()时,如果操作失败(如,网络问题),此参数将设定重新调用clean()时,客户端应该等待的时间。此值单位为毫秒,缺省为3分钟。

参考资料

1.O’Reilly <<Java RMI>> Chapter 16.The RMI Runtime

2.[url="http://docs.oracle.com/javase/6/docs/platform/rmi/spec/rmiTOC.html"]Java RMI 规范[/url]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值