lipermimodその3 GC

2019年5月21日

lipermimodその2の続きである。lipermimodについての全投稿は/tag/lipermimodにある。

ここでは、ServerGlobal以外の新たなオブジェクトを作成し、クライアントから利用することを考える。そして、クライアント側でそれが必要の無くなった場合の動作を示す。

ServerGlobalに新たなオブジェクト作成を依頼する

ServerGlobalはいわば、サーバが行うサービスとして常に公開しているAPIのセットと思えばよい。このオブジェクトはサーバ内にただ一つだけ存在し、その一つのオブジェクトが複数のクライアントからのリクエストに対応する。

しかし、ときにはそのクライアント専用のオブジェクトをサーバ内に作成したい場合がある。これを行うためにはServerGlobalに新たなオブジェクトを作成するメソッドを追加すればよい。

ごく簡単に書けば、以下のようになる。

  // サーバのサービスオブジェクト
  public static interface ServerGlobal extends IUnreferenced {
    // クライアントが要求したら、新たなオブジェクトServerSubを返す
    public ServerSub getServerSub();
  }

  private static class ServerGlobalImpl implements ServerGlobal {
    public ServerSub getServerSub() {
      return new ServerSubImpl(seq);      
    }
  }

  // 要求してきたクライアント用に返すオブジェクトのインターフェース
  public static interface ServerSub extends IUnreferenced {    
    public void hello();
  }

  public static class ServerSubImpl implements ServerSub {
    public void hello() {
      System.out.println("hello");
    }
  }

あとはほとんど前回と同じである。次のようなコードになる。

    server.registerGlobal("serverGlobal",  new ServerGlobalImpl(seq));
    server.bind(4455);

    client.connect("localhost",  4455);    
    ServerGlobal serverGlobal = client.getGlobal("serverGlobal");
    ServerSub serverSub = serverGlobal.getServerSub();    
    serverSub.hello();

GCはどうなっているのか?

ここで、一つ疑問に思う点としては、もしクライアント側が取得したServerSubへの参照がなくなったらどうなるのか?ということだ。通常のJavaであれば、ServerSubのオブジェクトがGC対象になり、いつかそのメモリ領域が解放されることになる。

しかし、lipermimodの場合、オブジェクトの実体は同じVM内には無い。どうやってオブジェクトが不要になったことを伝えれば良いのか?

この答えもちゃんと用意してある。

クライアントからサーバへの「廃棄通知」

クライアント側にはProxyとして作成されたServerSubがあり、これがlipermod内部でWeakReferenceで参照されている。これによってクライアント側でオブジェクトが不要になったことがわかり、それがサーバ側に伝えられる。

サーバ側では、「不要になった」通知を受け取ると元のオブジェクトを削除する。。。という仕組みになっている。

だから、このリモート環境にあっても特別なGC処理を行う必要はない。通常のJavaオブジェクトとして扱うことができる。

クライアントが切断した場合

上記は、(サーバから見れば)クライアントが自主的にオブジェクトを返納する場合であるが、何も言わずにクライアントが切断した場合はどうなるかであるが、この場合にもサーバ側ではクライアントが接続中であるかを検出できるので、クライアントが切断してしまったら、そのクライアント用に用意したオブジェクトを廃棄することになる。

実際には、複数のクライアントに同じオブジェクトを提供している可能性があるので、参照計数を1減らすことになる。これが0になったらサーバ内のオブジェクトを廃棄することになる。

クライアントが動作していないのにソケットが接続したままの場合

ときには、サーバ側ではクライアントとのソケットが接続したままであるのに、実際にはクライアントが動作していない場合もある。これを検出するために、ハートビートと呼ぶ、クライアントから定期的にサーバに対して「まだ生きてるよ」通知を自動で送ることができる。

これについては項を改めて説明する。

IUnreferencedインターフェース

前述の仕組みがあるため、特にGCを考慮したコードを書く必要は無い。クライアント側で不要になり、それがサーバ側に通知される場合であれ、クライアント側がいきなり切断された場合であれ、サーバ側は自身に保管しているオブジェクトを廃棄することができる。

しかし、もう一つ問題がある。

もしサーバが側のオブジェクトが何らかのサーバ側リソースを掴んでいるとしたら、その解放操作をしたいのである。例えば、あるファイルをクライアントのためにオープンし、クライアントがそれを使っている間はオープンしっぱなしであれば、不要になったときにはクローズしなければならない。

このためにIUnreferencedというインターフェースが用意してある。以下のようなインターフェースだ。

public interface IUnreferenced extends IRemote {
  public void unreferenced();
}

IUnreferencedはIRemoteであるので、IRemoteの代わりに使用することができる。このインターフェースでは、(この場合には)サーバ側のオブジェクトの廃棄決定がされた場合には、このunreferenced()メソッドが呼び出される。この中でしかるべき処理を行うことになる。

※Javaのfinalizeも当然呼び出されるのだが、この動作に依存することはやめた方がよい。VMによって挙動がかなり異なる。

IUnreferencedの使用例

次にIUnreferencedを使い、サーバ側オブジェクトの廃棄タイミングをテストするサンプルを示す。

最初の例は、クライアント側の参照を除去した例、二番目の例はクライアントを強制的にクローズした例である。


import static org.junit.Assert.*; import java.util.*; import org.junit.*; public class IUnreferencedTest { Server server; Client client; List<String>seq; @Before public void before() { server = new Server(); client = new Client(); seq = Collections.synchronizedList(new ArrayList<String>()); } @Test public void クライアントの参照を除去する() throws Exception { // サーバ側 server.registerGlobal("serverGlobal", new ServerGlobalImpl(seq)); server.bind(4455); // クライアント側 client.connect("localhost", 4455); ServerGlobal serverGlobal = client.getGlobal("serverGlobal"); ServerSub serverSub = serverGlobal.getServerSub(); serverSub.hello(); seq.add("クライアントからserverSubへの参照を除去"); serverSub = null; gcAndWait(); server.close(); assertArrayEquals( new String[] { "helloが呼び出された", "クライアントからserverSubへの参照を除去", "GCの開始", "subのunrefrenced呼び出し", "subのfinalize呼び出し", }, seq.toArray(new String[0]) ); } @Test public void クライアントを強制的に切断する() throws Exception { // サーバ側 server.registerGlobal("serverGlobal", new ServerGlobalImpl(seq)); server.bind(4455); // クライアント側 client.connect("localhost", 4455); ServerGlobal serverGlobal = client.getGlobal("serverGlobal"); ServerSub serverSub = serverGlobal.getServerSub(); serverSub.hello(); // クライアントをクローズするserverSubへの参照は保持したまま seq.add("クライアントをクローズ"); client.close(); gcAndWait(); server.close(); assertArrayEquals( new String[] { "helloが呼び出された", "クライアントをクローズ", "GCの開始", "subのunrefrenced呼び出し", "subのfinalize呼び出し", }, seq.toArray(new String[0]) ); } void gcAndWait() throws InterruptedException { seq.add("GCの開始"); System.gc(); Thread.sleep(100); System.gc(); } public static interface ServerGlobal extends IRemote { public ServerSub getServerSub(); } private static class ServerGlobalImpl implements ServerGlobal { List<String>seq; public ServerGlobalImpl(List<String>seq) { this.seq = seq; } public ServerSub getServerSub() { return new ServerSubImpl(seq); } } public static interface ServerSub extends IUnreferenced { public void hello(); } public static class ServerSubImpl implements ServerSub { List<String>seq; public ServerSubImpl(List<String>seq) { this.seq = seq; } public void hello() { seq.add("helloが呼び出された"); } public void unreferenced() { seq.add("subのunrefrenced呼び出し"); } @Override protected void finalize() throws Throwable { super.finalize(); seq.add("subのfinalize呼び出し"); } } }