Java.rmi.NoSuchObjectException: 表中没有这样的对象。

28

我正在编写一个非常简单的RMI服务器,在单元测试中看到间歇性的java.rmi.NoSuchObjectExceptions

我有一串调用同一对象的远程方法,虽然前面几个可以正常执行,但后面的有时会失败。 我在之间没有做任何取消注册服务器对象的操作。

这些错误不总是出现,如果我放置断点,它们往往不会出现。这些是Heisenbugs吗?当通过调试器的减速执行来查看它们时,它们的竞争条件会消失吗?我的测试或服务器代码中没有多线程运行(尽管可能在RMI堆栈内部)。

我在Mac OS X 10.5(Java 1.5)上通过Eclipse的JUnit插件运行此操作,RMI服务器和客户端都在同一个JVM中。

什么原因会导致这些异常?

7个回答

68

保持对实现java.rmi.Remote接口的对象的强引用,以使其保持可达性,即不符合垃圾回收的条件。

下面是一个简短的程序,演示了java.rmi.NoSuchObjectException。该脚本是自包含的,在单个JVM中创建一个RMI注册表以及一个"客户端"和一个"服务器"。

只需复制此代码并将其保存在名为RMITest.java的文件中。使用您选择的命令行参数进行编译和调用:

  • -gc(默认)显式地指示JVM在服务器启动后但客户端连接到服务器之前尽力运行垃圾回收。这可能会导致Remote对象被垃圾回收器回收,如果对Remote对象的强引用被释放。当客户端在Remote对象被回收后连接时,将观察到java.rmi.NoSuchObjectException
  • -nogc 不明确请求垃圾回收。这可能会导致Remote对象仍然可以被客户端访问,无论是否持有或释放强引用除非在服务器启动和客户端调用之间存在足够的延迟,以便系统“自然”调用垃圾回收器并回收Remote对象
  • -hold 保留对Remote对象的强引用。在这种情况下,类变量引用Remote对象。
  • -release(默认)将释放对Remote对象的强引用。在这种情况下,方法变量引用Remote对象。方法返回后,强引用将丢失。
  • -delay<S> 在服务器启动和客户端调用之间等待的秒数。插入延迟为垃圾回收器运行“自然”提供了时间。这模拟了一个最初“工作”的过程,但在一段重要的时间后失败。注意,在秒数前没有空格。例如:-delay5将使客户端在服务器启动后5秒钟进行调用。

程序的行为可能因机器和JVM而异,因为诸如System.gc()之类的东西只是提示,设置-delay<S>选项是猜测垃圾收集器的行为。

在我的机器上,在编译javac RMITest.java后,我看到这种行为:

$ java RMITest -nogc -hold
received: foo
$ java RMITest -nogc -release
received: foo
$ java RMITest -gc -hold
received: foo
$ java RMITest -gc -release
Exception in thread "main" java.rmi.NoSuchObjectException: no such object in table
    at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:255)
    at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:233)
    at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:142)
    at java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod(RemoteObjectInvocationHandler.java:178)
    at java.rmi.server.RemoteObjectInvocationHandler.invoke(RemoteObjectInvocationHandler.java:132)
    at $Proxy0.remoteOperation(Unknown Source)
    at RMITest.client(RMITest.java:69)
    at RMITest.main(RMITest.java:46)

这里是源代码:

import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import static java.util.concurrent.TimeUnit.*;

interface RemoteOperations extends Remote {
    String remoteOperation() throws RemoteException;
}

public final class RMITest implements RemoteOperations {
    private static final String REMOTE_NAME = RemoteOperations.class.getName();
    private static final RemoteOperations classVariable = new RMITest();

    private static boolean holdStrongReference = false;
    private static boolean invokeGarbageCollector = true;
    private static int delay = 0;

    public static void main(final String... args) throws Exception {
        for (final String arg : args) {
            if ("-gc".equals(arg)) {
                invokeGarbageCollector = true;
            } else if ("-nogc".equals(arg)) {
                invokeGarbageCollector = false;
            } else if ("-hold".equals(arg)) {
                holdStrongReference = true;
            } else if ("-release".equals(arg)) {
                holdStrongReference = false;
            } else if (arg.startsWith("-delay")) {
                delay = Integer.parseInt(arg.substring("-delay".length()));
            } else {
                System.err.println("usage: javac RMITest.java && java RMITest [-gc] [-nogc] [-hold] [-release] [-delay<seconds>]");
                System.exit(1);
            }
        }
        server();
        if (invokeGarbageCollector) {
            System.gc();
        }
        if (delay > 0) {
            System.out.println("delaying " + delay + " seconds");
            final long milliseconds = MILLISECONDS.convert(delay, SECONDS);
            Thread.sleep(milliseconds);
        }
        client();
        System.exit(0); // stop RMI server thread
    }

    @Override
    public String remoteOperation() {
        return "foo";
    }

    private static void server() throws Exception {
        // This reference is eligible for GC after this method returns
        final RemoteOperations methodVariable = new RMITest();
        final RemoteOperations toBeStubbed = holdStrongReference ? classVariable : methodVariable;
        final Remote remote = UnicastRemoteObject.exportObject(toBeStubbed, 0);
        final Registry registry = LocateRegistry.createRegistry(Registry.REGISTRY_PORT);
        registry.bind(REMOTE_NAME, remote);
    }

    private static void client() throws Exception {
        final Registry registry = LocateRegistry.getRegistry();
        final Remote remote = registry.lookup(REMOTE_NAME);
        final RemoteOperations stub = RemoteOperations.class.cast(remote);
        final String message = stub.remoteOperation();
        System.out.println("received: " + message);
    }
}

1
所以你的意思是,我需要在服务器上手动保留对我的服务器对象的引用,因为我放入注册表中的导出的UnicastRemoteObject不会阻止该对象被垃圾回收,这将使我在注册表中留下一个悬空引用? - Thilo
2
我本希望在“解绑”对象之前,RMI系统能够保持其存活。 - Thilo
8
明白了。保持引用的责任似乎落在程序员身上。我也曾希望RMI系统在绑定生效期间保持引用。这样做的原因很可能是有充分的理由的(我没有研究过RMI源代码)。无论如何,它似乎并不是非常直观。 - Greg Mattes
8
这个回答不正确。它完全忽略了DGC的影响。远程对象存在一个DGC客户端就足以防止其被本地GC回收。在这种情况下,注册表成为远程对象的DGC客户端,因为被提供了远程存根。这段代码之所以会产生这样的行为是因为注册表本身被GC回收了,这同时取消了导出并释放了所有绑定,进而解除了对远程对象的DGC保持,允许远程对象被本地GC回收。 - user207421
1
@EJP,我很感兴趣学习更多。您能否编写另一个脚本来演示您的想法,并将其作为此问题的另一个答案发布? - Greg Mattes
显示剩余14条评论

8
一些需要考虑的其他问题-首先,您是引用对象实例还是存根接口本身已经消失了?如果某个对象实例消失了,那么通常的原因是它被取消引用和垃圾回收了,但如果是接口,则您的RMI服务器端点循环出现了某些问题。

到目前为止,我发现最好的调试工具是打开java.rmi.server.logCalls=true属性(请参见http://java.sun.com/j2se/1.5.0/docs/guide/rmi/javarmiproperties.html),然后观察所有精彩信息流向日志窗口。这每次都告诉我发生了什么。

jos


2

我有同样的问题,现在我已经解决了。解决方案很简单,您必须创建一个强引用“对象”,以避免该对象被垃圾回收。

例如,在您的服务器类中:

...
private static ServiceImpl serviceImpl = null;

public static void register (int port) {
    serviceImpl = new ServiceImpl();
    Registry registry = LocateRegistry.createRegistry(port);
    registry.rebind ("serviceImpl", serviceImpl);
}

public static void main(String[] args) throws RemoteException, NotBoundException {
    register(1099);    
    ...the rest of your code...
}

所以,它保护了“serviceImpl”对象不被垃圾回收。请纠正我如果我错了。


2
你需要静态地存储 Registry 引用。 - user207421

1

以上讨论中有一个遗漏的点。这就是所谓的分布式垃圾回收(DGC)。如果没有对分布式对象的本地和远程引用,GC可以将该对象从内存中删除。有一种复杂的算法来验证这一点。上面的代码片段确实很好地展示了DGC的有效性。

某种程度上看起来像特性的东西实际上只是被设计出来的行为!

Frank


0

在使用Spring Remoting (RMI)时,我遇到了这个错误。我的服务没有被垃圾回收。

在为“org.springframework”打开调试日志后,我发现我的服务器将服务注册到默认端口(1099)而不是客户端尝试连接的端口。

我认为一切端口方面都没问题,因为当客户端尝试连接时,“java.rmi.server.logCalls=true”确实在服务器上显示了一些输出。

当出现此错误时,请仔细检查端口(服务和注册表端口)。


0

没有查看代码很难回答这个问题(我猜代码会很大,无法在此发布)。然而,使用奥卡姆剃刀原理,你有两种可能性:

  • 服务器对象必须以某种方式被注销
  • 由于断点停止了错误,所以这绝对是一种竞争条件。

我建议你仔细检查代码路径,并牢记上述两点。


-1

我遇到了相同的错误,但可能是由于其他(尚未知)原因。

我将导出的对象强制转换为远程接口的类型,然后在绑定名称时出现 NoSuchObjectException 错误。移除强制转换后问题得到解决。

简而言之:

public interface MyRemoteInterface extedns Remote {
    ...
}

public class MyRemoteObject implements MyRemoteInterface {
    ...
}

public static MyRemoteObject obj = new MyRemoteObject();

public static void main(String[] args) {
    //removing cast to MyRemoteInterface fixes the problem
    this.obj = UnicastRemoteObject.exportObject((MyRemoteInterface) this.obj, 0);

    //unless the above cast is removed, this throws NoSuchObjectException occasionally
    LocateRegisry.getRegistry("127.0.0.1", 1099).bind("name", this.obj);
}

去掉强制类型转换并不能解决问题。exportObject() 的结果是一个 Remote 对象,而不是 MyRemoteObject,实际上它是一个存根(stub),而不是提供的 Remote 对象参数。这段代码不可能编译,更不用说执行了。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接