3 RMI系统概述


3.1 存根和骨架

RMI使用一种标准机制(在RPC系统中使用)与远程对象进行通信:存根和骨架。远程对象的存根充当客户端的本地代表或代理。调用者在本地存根上调用方法,本地存根负责在远程对象上执行方法调用。在RMI中,远程对象的存根实现与远程对象实现相同的一组远程接口。

当调用存根的方法时,它会执行以下操作:

存根隐藏了参数的序列化和网络级通信,以向调用者呈现简单的调用机制。

在远程JVM中,每个远程对象可能有一个相应的骨架(在仅Java 2平台环境中,不需要骨架)。骨架负责将调用分派到实际的远程对象实现。当骨架接收到传入的方法调用时,它会执行以下操作:

在Java 2 SDK标准版v1.2中,引入了一个额外的存根协议,消除了在Java 2平台环境中骨架的需要。相反,使用通用代码来执行JDK1.1中骨架执行的职责。存根和骨架由rmic编译器生成。

3.2 远程方法调用中的线程使用

RMI运行时分派给远程对象实现的方法可能会在单独的线程中执行,也可能不会。RMI运行时不保证将远程对象调用映射到线程。由于对同一远程对象的远程方法调用可能会并发执行,远程对象实现需要确保其实现是线程安全的。

3.3 远程对象的垃圾收集

在分布式系统中,就像在本地系统中一样,自动删除那些不再被任何客户端引用的远程对象是可取的。这使程序员无需跟踪远程对象的客户端,以便它可以适当地终止。RMI使用类似于Modula-3的Network Objects的引用计数垃圾收集算法。(参见1994年Birrell、Nelson和Owicki的《Digital Equipment Corporation Systems Research Center Technical Report 115》中的"Network Objects"。)

为了实现引用计数垃圾收集,RMI运行时会跟踪每个Java虚拟机中的所有活动引用。当活动引用进入Java虚拟机时,其引用计数会递增。对对象的第一个引用会向服务器发送一个“引用”消息。当发现本地虚拟机中的活动引用不再被引用时,计数会递减。当最后一个引用被丢弃时,将发送一个“未引用”消息给服务器。协议中存在许多微妙之处;其中大多数与维护引用和未引用消息的顺序有关,以确保对象不会过早被收集。

当一个远程对象不再被任何客户端引用时,RMI运行时会使用弱引用来引用它。弱引用允许Java虚拟机的垃圾收集器丢弃对象,如果没有其他本地引用指向该对象。分布式垃圾收集算法通过以通常方式持有对象的普通或弱引用与本地Java虚拟机的垃圾收集器进行交互。

只要存在对远程对象的本地引用,它就不能被垃圾收集,并且可以在远程调用中传递或返回给客户端。传递远程对象会将其传递到其所传递的虚拟机的标识符添加到引用集中。需要未引用通知的远程对象必须实现java.rmi.server.Unreferenced接口。当这些引用不再存在时,将调用unreferenced方法。由于引用集为空时才调用unreferenced,因此可能会多次调用unreferenced。只有在不再存在任何引用(本地或远程)时,才会收集远程对象。

请注意,如果客户端和远程服务器对象之间存在网络分区,则可能会发生远程对象的过早收集(因为传输可能认为客户端已崩溃)。由于可能发生过早收集的可能性,远程引用无法保证引用完整性;换句话说,远程引用实际上可能不引用现有对象。尝试使用此类引用将生成一个RemoteException,必须由应用程序处理。

3.4 动态类加载

RMI允许在RMI调用中传递的参数、返回值和异常为可序列化的任何对象。RMI使用对象序列化机制将数据从一个虚拟机传输到另一个虚拟机,并在调用流中注释适当的位置信息,以便接收方可以加载类定义文件。

当远程方法调用的参数和返回值在接收JVM中解组为接收JVM中的活动对象时,需要所有类型对象的类定义。解组过程首先尝试在其本地类加载上下文(当前线程的上下文类加载器)中按名称解析类。RMI还提供了一种机制,用于从传输端指定的网络位置动态加载传递为参数和返回值的实际对象类型的类定义,以及包含特定远程对象实现类的远程存根类(用于包含远程引用)以及在RMI调用中传递的任何其他类型,例如声明的参数类型的子类,如果在解组端的类加载上下文中尚不可用。

为了支持动态类加载,RMI运行时使用java.io.ObjectOutputStreamjava.io.ObjectInputStream的特殊子类作为它用于编组和解组RMI参数和返回值的编组流。这些子类分别覆盖了ObjectOutputStreamannotateClass方法和ObjectInputStreamresolveClass方法,以通信有关在流中的类描述符对应的类定义文件的位置的信息。

对于写入到RMI编组流的每个类描述符,annotateClass方法将调用java.rmi.server.RMIClassLoader.getClassAnnotation的结果添加到流中,该结果可能为null,也可能是一个String对象,表示远程端应从中下载给定类的类定义文件的代码库URL路径(一系列URL的空格分隔列表)。

对于从RMI编组流中读取的每个类描述符,resolveClass方法从流中读取一个对象。如果对象是一个字符串(并且java.rmi.server.useCodebaseOnly属性的值不是true),那么resolveClass将调用RMIClassLoader.loadClass,第一个参数是带注释的String对象,第二个参数是类描述符中所需类的名称。否则,resolveClass将调用RMIClassLoader.loadClass,参数是所需类的名称。

有关RMI中类加载的更多详细信息,请参阅“RMIClassLoader类”部分。

3.5 通过代理在防火墙上进行RMI

自JDK 9起,通过代理在防火墙上进行RMI调用的实现已被移除。