Hello world! コンテンツ・メディア第1事業部のjyukutyoこと阪田です。

前回はコレクションのセッションについてのレポートでした。今回はクラスローダーのセッションについてレポートします。

Join the War on ClassLoader Leaks

スライドはこちらにあります。

https://static.rainfocus.com/oracle/oow16/sess/1461358415846001E0TZ/ppt/Classloader%20leaks%20public.pptx

内容

java.lang.OutOfMemoryError(OOME)はみな出会ったことがあるだろう。Java 8になってPermGen spaceとは出なくなったが、代わりにMetaspaceが出るようになった。ローカルでの開発時にも、継続的デプロイでもOOMEは出る。このセッションのアジェンダは次のようなものだ。

  • OOMEは何を意味するのか?
  • OOMEはなぜ起こるのか?
  • どこでリークするのか?
  • どのように避けるのか?
  • OSSの例
  • トレーニング

OOMEは何を意味するのか?

JVMのメモリはヒープとPermGen/Metaspaceとスタックである。スタックはスレッドごと、ローカル変数やメソッドのパラメータを持つ。ヒープはオブジェクトのインスタンスを持つ。PermGen/Metaspaceはjava.lang.Classインスタンスなどを持つ。PermGenという名前はJava EEとクラスのアンロードができる前に名付けられた。Java 8でMetaspaceへ置き換えられた。OOME PermGen space/Metaspaceは、あまりに多くのクラスをロードしたときに起こる。

OOMEはなぜ起こるのか?

OOME PermGen space/Metaspaceが起こる原因は次の2つが考えられる。

  1. アプリケーションが大きすぎる。Java 7までは-XX:MaxPermSize=256Mなどと指定すればよい。Java 8ではMetaspaceは自動的に増えていく。
  2. java.lang.Classインスタンスが再デプロイ後にガベージコレクトされない

参照の型は次の4つがある。

  • 強い参照:到達可能なときは決してGCされない
  • ソフト参照:OOMEになる前にGCされる
  • 弱い参照(WeakHashMap):強い参照とソフト参照がないときにGCされる
  • ファントム参照:GCは妨げない

GCの到達可能性は以下の図のようなことである。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-05-17-40-30

再デプロイでは、新しいWARファイルをデプロイすれば前のクラスローダはGCされる。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-05-17-43-30

クラスローダーの参照は次の図のようになる。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-05-17-44-55

どのようにリークが起きるのか。GCルートからクラスローダへ到達可能だと、クラスローダをGCすることができない。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-05-17-45-38

どこでリークするのか?

Tomcatなどアプリケーションサーバのバグだ。他にCommons LoggingやLog4Jなどロギングフレームワークのバグ、Bean Validation APIやUnified Expression Languageのバグといったこともある。

GCルートでも起こる。まず、システムクラスローダによってロードされるクラス。JDKのクラスにおけるstaticフィールドで起こる。ライブスレッドではローカル変数、メソッドのパラメータのスタック、java.lang.Threadインスタンスで起こる。

システムクラスでは、java.sql.DriverManagerやBean introspectionのキャッシュ、シャットダウンフック、カスタムでデフォルトにしたAuthenticator、カスタムのセキュリティでのProvider、カスタムMBean、カスタムのThreadGroup、カスタムのプロパティエディタ…最初の呼び出し元でのcontextClassLoaderへの参照などで起こる。

DriverManagerをもう少し詳しく見てみよう。たとえばMySQLのJDBCドライバを使うときはこうするだろう。

com.mysql.jdbc.Driverクラスにはstaticイニシャライザがある。

registerDriver()メソッドは以下のようになっている。

このregisterDriversという変数は、以下のものだ。

図で表すと以下のようになる。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-06-16-55-03

そのため、コンテキストのシャットダウンで以下のように書かなければ、リークしてしまう。

スレッドもここで止めておかなければならない。次のようにスレッドを実装するのは悪いアイデアだ。

止められるようには以下のようにする。

このことは、次の記事に紹介されている。

Heinz Kabutz / Java Specialists’ – The Law of the Blind Spot

次にThreadLocalについて考えてみよう。次のように実装してはならない。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-06-17-09-13

ThreadLocalのJavaDocには以下のようにある。

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; …

各スレッドは、スレッドが生存していてThreadLocalインスタンスがアクセス可能である間は、スレッド・ローカル変数のコピーへの暗黙的な参照を保持します。

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-06-17-13-40

プールされたスレッドではスレッドはクラスローダより長生きする。ThreadLocalはThreadGlobalになる!

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-06-17-17-35

%e3%82%b9%e3%82%af%e3%83%aa%e3%83%bc%e3%83%b3%e3%82%b7%e3%83%a7%e3%83%83%e3%83%88-2016-10-06-17-18-20

ThreadLocalMapのJavaDocには以下のようにある。

However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space

(私訳)しかしながらリファレンスキューが使われないので、テーブルが容量不足になり始めたときだけ失効したエントリが削除されることが保証されます。

参照を含んだカスタムの値は、staticなThreadLocalであればリークし、そうでなければ予測されていないGCとなる。カスタムのThreadLocalであればリークしない。

ThreadLocalはクリアするようにしよう。

実際には、多くのOSSライブラリが違反している。

どのように避けるのか?

リーク防止のライブラリがある。アプリケーションサーバ非依存、Tomcatよりも多くをカバーしており、ログで警告を出力、ライセンスはApache 2ライセンスだ。実行時の依存は0で、設定は必要ない。Mavenであれば以下のように依存ライブラリを追加する。

これはServlet 3.0以降用だが、2系のものもある。

OSSの例

Bean Validationの1.0.0.GAでは以下のようになっていた。

(私訳)List<ValidationProvider>はキーであるクラスローダに強い参照を持っている。JPAのCachingPersistenceProviderResolverと同じモデルを使うようにして。

jyukutyo調査

Bean Validationの1.1.0.GAではこのクラスは次のようになっています。

providersPerClassloader.put( classLoader, new SoftReference<List<ValidationProvider<?>>>( providers ) );とあるので、ソフト参照を使うようになったようです。

JPAのCachingPersistenceProviderResolverクラスも調べてみました。

その他のOSS

javax.el.BeanELResolverやorg.apache.cxf.transport.http.CXFAuthenticator、com.sun.star.lib.util.AsynchronousFinalizerも該当する。

トレーニング

Eclipse Memory Analyzer(MAT)を使おう。

http://www.eclipse.org/mat/

次のオプションをつけてアプリケーションを実行しよう。

感想

最後に出てきた、MATについてはこのブログで私が記事を書いています。

ヒープダンプをEclipse Memory Analyzerで解析しよう!

セッションとしては非常におもしろかったです。とくにOSSの実例を挙げながらリークの原因を解説する箇所ではとても興奮しました。こういう内容のセッションはJavaOneだと他にもいろいろとあります。なかなか普段の勉強会でこの分野を扱っていることは少ないと思いますので、興味がある人にはJavaOneはとても楽しめるカンファレンスです!