RMIで公開されたオブジェクトが破棄される時を知る

先日書いたエントリRMIでいいじゃないかは、たくさんのブックマーク登録を頂いたようでちょっとびっくりしている。
これは温故知新ということもあろうが、それよりもRMIの最新の仕様に関して、私も含めて知らなかった開発者が多いことの証明でもあったのではないだろうか。(私もRMIはJava2出た頃に勉強したっきりだった)
さて、該当のエントリの最後にきしださんの日記にリンクが貼られていたのを思い出して欲しい。

RMIでNoSuchObjectExceptionが出る件 - きしだのはてな

RMIで公開対象となったオブジェクトが破棄されることで、クライアント側で例外が発生するというものだが、エントリは2006年に書かれたものであり実装が変わっている可能性もあるため、私も実際にテストしてみることにした。

リモートオブジェクトの公開に関しては前回のエントリと同様。

Registry registry = LocateRegistry.createRegistry(Registry.REGISTRY_PORT);
IFoo foo = new FooImpl();
Remote remote= UnicastRemoteObject.exportObject(foo, 0);
registry.rebind("foo", remote);

実際の実行ではリモートオブジェクトが破棄された時点でサーバアプリケーションは終了してしまうので、メインスレッドを待機させておく必要があるが、いろいろな所でサンプルは散見されるので割愛。

サーバアプリケーションを起動後、定期的にクライアント側のコードを実行したがリモートオブジェクトの元になったオブジェクトのインスタンスは中々GCの対象になってくれない。おそらく時間がかかるだけなのだろうが、待てないのでちょっと調べてみたところ、まずはクライアント側に与えるリース切れをサーバが検知することが必要らしい。

D.5 分散型ガベージコレクタは、接続の切れたクライアントをどのようにして検出するのですか。クライアントを適切に終了するために System.exit を使うのは賢明でしょうか。- Java RMI とオブジェクト直列化
D.6 クライアントがクラッシュしたことをサーバーはどのように知るのですか。- Java RMI とオブジェクト直列化

ドキュメントによると、DGC(分散ガベージコレクタ)が公開後のリモートオブジェクトを破棄の対象にするかを判定するまで、クライアントがリモートオブジェクトへのアクセスを止めたとしても(アプリケーションを終了しても)一定の待機時間(リース時間)があるらしく、この待機時間を越えて始めて廃棄対象とされるらしい。

この待機時間:リース時間はJavaシステムパラメタ"java.rmi.dgc.leaseValue"で設定することができる。

-Djava.rmi.dgc.leaseValue=30000

この設定ではリース時間を30秒に設定している(デフォルトは600000msec=10分間)。
リモートオブジェクトをエクスポートする仮想マシン (VM) の設定に有用なプロパティ - java.rmi プロパティ

この設定でサーバアプリケーションを起動後30秒間待ち、サーバに対してGCを強制的に発生させた※1後にクライアント側のアプリケーションを実行してみた所、きしださんのエントリにあるように期待通りの例外が発生したのを確認できた。(リース時間を越えてしまってもGCを実行しないままでクライアントアプリケーションを実行するとリース時間がリセットされるため、例外は発生しない。)

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.enumMesssgeMap(Unknown Source)
        

私も含めて永続的にリモートオブジェクトを公開し続けるような用途に限っては、このように例外が発生するのを避けるためにはきしださんが書いている通り、リモートに公開される側のオブジェクトの参照をスタティック変数等にコピーするなど、参照を強参照にしておくことで回避が可能だ。※2

余計なリモートオブジェクトはリソースを圧迫するので不要になったら破棄したい用途(むしろこちらが普通だろうか)場合は通常通りの実装でリース時間を調整して対応すればよいだろう。ただし、この場合例外が発生してしまうとサーバの再起動が必要になる場合があるので適宜対策が必要だろう。

※1 jconsoleでサーバアプリケーションにデマンドアタッチして、「メモリ」タブからGCを実行した。これでFullGCが実行される。
※2 .NET RemotingのSAO(Server Activation Object)の場合、MarshalByRefObject#InitializeLifetimeServiceメソッドの戻り値を無効(null)にすることでリースを無効にし、永続的なリモートオブジェクトの公開を行うことができる。