Tomcat7 のメモリリーク対策を覗き見るの巻

モレるという言葉に過剰反応してしまう、お腹の弱い日本男子代表のソメダです、こんばんわ。id:c9katayama さんが、こんなエントリ を書いていたり、こんなつぶやきをしていたり、

Tomcat7のJreMemoryLeakPreventionListenerがキモかわいい

http://twitter.com/c9katayama/status/18569788913

で完全に釣られました。チェックアウトしてコードみてみたですよ。c9katayama さんのエントリの補完的な内容をメモするですよ。

JreMemoryLeakPreventionListener

このリスナのそもそもの意図としては、Webapp クラスローダがコンテキストクラスローダの場合に、このリスナが扱ってるクラスをロードするとメモリリーク起きる可能性あるので、Server の初期化時に (正確にはコンテキストクラスローダが Common ローダのうちに)、先読みしておこう、というもののようです。Tomcat のクラスローダの構成は ここ 参照。

で、このクラスは LifeCycleListener の実装クラスで、このリスナを有効にする設定は server.xml に以下のように、Server 要素の子要素として定義されてます。

  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />

機能として利用したくなければ、これをコメントアウトしたり、また JreMemoryLeakPreventionListener で有効、無効にしたい機能などはここで設定すればよくなってるです。この辺りはドキュメントもされてますね。

中身の詳細は先のエントリの通りです。一点補足でいうと、urlCacheProtection はメモリリークというより、意図しないファイルロックが残ってしまうのを防ぐためのようです。appContextProtection は GWT が imageio 使う時に影響ある、gcDaemonProtection は JMX 系のクラスから呼び出される、といった事がコメントには書いてあるので、さらっと見てみてもよいと思います。

JdbcLeakPrevention

中身については先のエントリの通りなのですが、呼び出し元の WebappClassLoader#clearReferencesJdbc の実装のほうがキモいです。クラスローダ周りの処理なのでしょうがないのでしょうが、自分では書きたくない系です。

    private final void clearReferencesJdbc() {
        InputStream is = getResourceAsStream(
                "org/apache/catalina/loader/JdbcLeakPrevention.class");
        // We know roughly how big the class will be (~ 1K) so allow 2k as a
        // starting point
        byte[] classBytes = new byte[2048];
        int offset = 0;
        try {
            int read = is.read(classBytes, offset, classBytes.length-offset);
            while (read > -1) {
                offset += read;
                if (offset == classBytes.length) {
                    // Buffer full - double size
                    byte[] tmp = new byte[classBytes.length * 2];
                    System.arraycopy(classBytes, 0, tmp, 0, classBytes.length);
                    classBytes = tmp;
                }
                read = is.read(classBytes, offset, classBytes.length-offset);
            }
            Class<?> lpClass =
                defineClass("org.apache.catalina.loader.JdbcLeakPrevention",
                    classBytes, 0, offset);
            Object obj = lpClass.newInstance();
            @SuppressWarnings("unchecked") // clearJdbcDriverRegistrations() returns List<String> 
            List<String> driverNames = (List<String>) obj.getClass().getMethod(
                    "clearJdbcDriverRegistrations").invoke(obj);
            for (String name : driverNames) {
                log.error(sm.getString("webappClassLoader.clearJbdc",
                        contextName, name));
            }
        } catch (Exception e) {
            // So many things to go wrong above...
            log.warn(sm.getString(
                    "webappClassLoader.jdbcRemoveFailed", contextName), e);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException ioe) {
                    log.warn(sm.getString(
                            "webappClassLoader.jdbcRemoveStreamError",
                            contextName), ioe);
                }
            }
        }
    }

StandardHost の MemoryLeakTrackingListener 周り

これまた内容は先のエントリの通りで、この機能の呼び出し元は ManagerServlet#findLeaks です。なので、manager をデプロイすればこの機能を利用してのメモリリークの検知をすることが出来るようになります。manager アプリにこんなのが追加されています。

ただし、実際には実行前に System.gc を呼び出すのでプロダクションシステムでは注意しろ、と書いてるので、ご利用は計画的に。

まとめと雑感

メモリリーク対策や検知の対象とされているのは、全般的に Webapp クラスローダ周りです。これ不要になった Webapp クラスローダへの不正な参照が残ってしまう事を防ぐ、という意図のように見えるのですが、そもそも Webapp クラスローダが不要になるのが、コンテキストのリロード時以外にあるのかなぁ、という素朴な疑問。もしそうであれば、プロダクションで、コンテキストのリロードをしないような運用をしていれば、影響ないのかな、と思ったり。ここはもうちょっとコード読む必要あるかも、です。勘違い等してたらコメント頂けると幸いでございます。