HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

Webアプリケーション開発の際の注意事項

実際に直面しているのはIBM WebSphere Application Server(WAS)で稼動するWebアプリケーションなのだが、Webアプリケーションを停止しても、そのアプリケーションをロードしたクラスローダーが残ってしまうという問題に悩まされている人がいる。‥いや、他人事のように書いたが、自分も関係者の一人。
要は、アプリケーションコンテキストのオブジェクトがGCの対象とならないためにクラスローダーへの参照が残ってしまい、クラスローダー自身が残ってしまうということなのだが‥
ということで、問題となるコーディングについて検証してみる。

検証環境

  • IBM WebSphere Application Server v6.1 Base Trial
    • FixPack 6.1.0.27を適用

検証方法

次の手順で検証を行う。

  1. Webアプリケーションをインストールして起動する。
  2. 必要な操作があれば行う。(あるページを開く、とか)
  3. javacoreを採取する。(jc1)
  4. Webアプリケーションを停止する。
  5. javacoreを採取する。(jc2)
  6. アプリケーションがきれいに終了しているのであれば、jc1には存在するアプリケーションコンテキストのクラスが、jc2には存在しないはずである。

問題となるコーディング

ThreadLocalに格納されるオブジェクト

一番シンプルなのは次のようなコード。

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

public final class TestServlet extends HttpServlet {

    private static ThreadLocal variable = new ThreadLocal();

    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        variable.set(new HogeBean());
    }
    public void destroy() {
        try {
        } finally {
            super.destroy();
        }
    }
}

このServletを、アプリケーションの起動と同時にロードされるように設定しておく。
クラス"HogeBean"はこのWebアプリケーションコンテキストに含まれるクラスのひとつ。
ThreadLocalオブジェクトがstatic変数に保持されているのがひとつのポイント。
ThreadLocalクラスとThreadクラスのソースを参照してみると分かるが、「ThreadLocalオブジェクトがキー、それに格納されるオブジェクトが値」というマップがThreadクラスに保持されている。
アプリケーションサーバーのワーカースレッドであるThreadオブジェクトはサーバーコンテキストの管轄になるのでWebアプリケーションを停止しても当然残るが、そこにアプリケーションコンテキストのオブジェクトが保持されていると、そのオブジェクトはGCの対象にならず、そのためアプリケーションクラスローダーが破棄されないということになってしまう。
アプリケーションとしては、

        variable.set(new WeakReference(new HogeBean()));

というようにThreadLocal以外からの参照がなくなったらGCの対象になるようにしたりする必要がある。アプリケーション終了時にThreadLocalオブジェクトが破棄されるように(上記の例ではvariable変数にnullをセット)するということもある程度の効果がある。
ひとつ注意が必要なのは、「ThreadLocalオブジェクトに格納されるオブジェクトはスレッドと関連付けられる形になるが、ある処理の実行に割り当てられるスレッドが前回と同じとは限らない」ということ。
たとえば、あちこちでThreadLocalオブジェクトを生成して各種オブジェクトを格納し、アプリケーションの終了時にまとめてクリアしよう、ということをやっても、「ThreadLocalに各種オブジェクトを格納する処理を実行したスレッド」と「「アプリケーションの終了時にまとめてクリアする処理を実行するスレッド」が異なると、(無理やりなことをしない限り)格納されたオブジェクトをすべてThreadLocalから取り出すことはできない。

スレッドの生成

一番簡単なのは次のようなコードだろうか。

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

public final class TestServlet extends HttpServlet {

    private Thread thread;
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        thread = new Thread() {
            public void run() {
                while (true) {
                    System.out.println("running");
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            }
        };
        thread.setDaemon(true);
        thread.start();
    }
    public void destroy() {
        try {
        } finally {
            super.destroy();
        }
    }
}

スレッドの実装クラス(クラス名は"TestServlet$1"のようになる)がアプリケーションコンテキストの管轄だが、生成したスレッドを停止する処理がどこにもないので、Webアプリケーションを停止しても(アプリケーションサーバーを停止しない限り)、標準出力に5秒ごとに"running"と出力し続ける。
上記のサンプルでは setDaemon(true) しているが、daemonスレッドかどうかは、「Java VMが終了するときに待つか待たないか」を決める基準になるだけなので、Webアプリケーションにおいては何の意味もない。
これを解決するには、スレッドをきちんと停止させるしかない。

    public void destroy() {
        try {
            thread.interrupt();
        } finally {
            super.destroy();
        }
    }

サンプルの場合は、interruptをすれば処理が終了するようにスレッドクラスを作っているので、停止処理はシンプル。

Timerの放置

先の「スレッドの生成」とも関連する。

import java.util.Timer;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

public final class TestServlet extends HttpServlet {

    private static Timer timer = new Timer(true);
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
    }
    public void destroy() {
        try {
        } finally {
            super.destroy();
        }
    }
}

こちらもTimerオブジェクトがstatic変数に保持されているのがひとつのポイント。
Timerオブジェクトは内部でスレッドを生成するので、これを停止させてやる必要がある。

    public void destroy() {
        try {
            timer.cancel();
        } finally {
            super.destroy();
        }
    }

staticフィールドに保持されるTimerをインスタンスメソッドで停止させるという違和感はあるが、まぁサンプルなので。

    • -

まぁ、ほかにもいろいろあると思うが、めんどくさくなった(ぉい)のでとりあえずここまで。