24
Tomcatの実装から学ぶ ClassLoaderLeak @n-agetsu 上妻 宜人 (あげつま のりと) 第十回 #渋谷java

Tomcatの実装から学ぶクラスローダリーク #渋谷Java

Embed Size (px)

Citation preview

Page 1: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

Tomcatの実装から学ぶClassLoaderLeak

@n-agetsu

上妻宜人 (あげつまのりと)

第十回 #渋谷java

Page 2: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

あげつまのりと

• SIer勤務

• Javaトラブルシューティング

• JBoss, Tomcat社内サポート

• はてな見習いプログラミング日記

• Software Design 2014 10月号寄稿

Page 3: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

ClassLoaderLeakって?

Page 4: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

ホットデプロイ時に古いクラスローダがGCされない困った不具合です

(〜JDK7) java.lang.OutOfMemoryError : Perm Gen

(JDK8〜) Metaspaceの増大/OutOfMemoryError(-XX:MaxMetaSpaceSize設定時)

Page 5: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

ホットデプロイの普及により遭遇率があがっています

OOM!.war

deploy

deploy

deploy

.war

Page 6: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷javahttp://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Edit-clear_mirrored.svg/120px-Edit-clear_mirrored.svg.png

【NEW】test.war

【OLD】test.war

TomcatはClassLoaderLeakの検知・解放機能を実装しています。

参考: O’Reilly Japan 詳解Tomcat

Page 7: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

Tomcat リーク検知実装コードを読む(org.apache.catalina.loader.WebappClassLoaderBase)

何をするとリークするかわかる

Page 8: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

Commonクラスローダ

Webappクラスローダ

(稼働中WAR)

アンデプロイ済Webapp

クラスローダ

ClassLoaderLeakの主な原因

Bootstrapクラスローダ

Systemクラスローダ Tomcatスレッドプール

1. 上位クラスローダで読み込まれたクラスからの強参照

例 : java.sql.DriverManagerのフィールド変数⇒WEB-INF/libのJDBCドライバ など

2. プール内スレッドからの参照・ スタック中スレッドが旧APクラスを参照・ ThredLocal.remove() の漏れ

Tomcat8のデフォルトのクラスローダ階層

(クラスローダについては以下参照)org.apache.catalina.startup.Bootstrap.initメソッドとorg.apache.catalina.loader.WebappLoaderクラス

Page 9: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

Commonクラスローダ

Webappクラスローダ

(稼働中WAR)

アンデプロイ済Webapp

クラスローダ

ClassLoaderLeakの主な原因

Bootstrapクラスローダ

Systemクラスローダ Tomcatスレッドプール

1. 上位クラスローダで読み込まれたクラスからの強参照

例 : java.sql.DriverManagerのフィールド変数⇒WEB-INF/libのJDBCドライバ など

2. プール内スレッドからの参照・ スタック中スレッドが旧APクラスを参照・ ThredLocal.remove() の漏れ

Tomcat8のデフォルトのクラスローダ階層

(クラスローダについては以下参照)org.apache.catalina.startup.Bootstrap.initメソッドとorg.apache.catalina.loader.WebappLoaderクラス

JDKクラス、Tomcatスレッドプール などアンデプロイしても使われるクラスから、

アンデプロイ済APへの参照が残るのが共通点

Page 10: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

Tomcatが検知するClassLoaderLeak

• java.lang.ThreadLocal.remove 漏れ

• java.sql.DriverManager.deregisterDriver 漏れ

• その他

• RMI関連 sun.rmi.transport.ObjectTable のobjTable, implTable クリア漏れ

• 実行中スレッドによる参照

• java.beans.Introspector.flushCaches()の実行漏れ

• Commons HttpClientの keep alive 用スレッドが残り続ける

• java.util.Timer.cancel() キャンセル漏れ

• 古いJVM?のGCバグ static / final 変数解放漏れ (JDK7, 8では再現せず)

org.apache.catalina.loader.WebappClassLoaderBase クラスを読んでみると..

Page 11: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

ThreadLocal.remove 実行漏れ

public class UserThreadContext {

private static ThreadLocal<User> context = ...;

public static void setUser(User user) {

userContext.set(user);

}

}

掃除されてないスレッドローカル

Page 12: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

ThreadLocal.remove 実行漏れ

public class UserThreadContext {

private static ThreadLocal<User> context = ...;

public static void setUser(User user) {

userContext.set(user);

}

// filterとかinterceptorとかでリクエスト終了までに実行public static void cleanup() {

userContext.remove();

}

}

データは入れたら消す。cleanupコードの追加。

Page 13: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

なぜremove漏れでリークする?

ThreadLocalにユーザ情報を追加

HTTPリクエスト

Tomcatスレッドプール

ThreadLocalMap

Entry

User

プールに戻ったスレッドが持つThreadLocalMapからアプリケーションへの参照が残り続ける

ThreadLocalMap

Entry

User

Page 14: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

なぜremove漏れでリークする?

ThreadLocalにユーザ情報を追加

HTTPリクエスト

Tomcatスレッドプール

ThreadLocalMap

Entry

UserTomcatの場合、アンデプロイ時にプールのチェック・リフレッシュにより、ThreadLocalによるリークを検知・解放

解放: ThreadPoolExecutor.setCorePoolSize(0);

(プースサイズを0にしてidleを全て解放)

Page 15: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

DriverManager.deregisterDriver 漏れ

.war のWEB-INF/libにJDBCドライバを含めて以下コードで再現

@WebServlet(“/leak”)

public class LeakServlet extends HttpServlet {

@Override

public void doGet(...) {

try {Class.forName(“org.postgresql.Driver”);

} catch (ClassNot.. e) { ... }

}

}

Page 16: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

JDBCクラスロード時にDriverManagerに登録

import org.postgresql;

public class Driver implements java.sql.Driver {

static {try {

java.sql.DriverManager.registerDriver(new Driver());....

Common

Webapp UndeployedWEB-INF/lib/xxxJDBC.jar

Bootstrap

System

DriverManager

• .warにJDBCに含めた場合、参照が残る。

• tomcat/libに置いた場合は、ドライバがCommonでクラスロードされるので残らない。

Page 17: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

DriverManagerによるリークの対処

• JDBCドライバを.warに含めない

• ServletContextListenerでAP終了時に解放

• java.sql.DriverManager.deregisterDriver実行

• Tomcat8ではデフォルトで検知・解放が有効

• JREMemoryLeakPreventionListenerで解放される

Page 18: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

どうやってClassLoaderLeakを解析するか?

Page 19: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

ClassLoaderLeakの見つけ方

1. HeapDump取得jcmd <pid> GC.heapdump filename=...

2. Eclipse Memory Analyzerロードしたら “Duplicate Classes”

Page 20: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

ClassLoaderLeakの見つけ方

同じクラスが複数のWebappClassLoaderからロードされていればClassLoaderLeakの可能性大

Page 21: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

Path To GC Roots で原因を特定

TomcatのTaskThreadが持つThreadLocalからの参照が原因

GCして欲しいWebappClassLoaderを選択してGCルートをチェック

Page 22: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

ライブラリによるClassLoaderLeak

• 伝統的なライブラリにあるClassLoaderLeak

• Commons Logging 1.0.4 以前

• iBATIS 2.3.4 以前 (iBATIS-540)

• log4j 1.2.16 以前 (Bug 50486, MDC利用時のみ)

• その他

• ClassLoader, ThreadLocal, HotDeploy Leak で検索

Page 23: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

ClassLoaderLeakはTomcatだけではない

• WebLogicServer やWildFlyでも起こり得る

(JVM起動中に動的にクラスローダの生成・破棄があれば起こる可能性有)

• 個人的にはWebLogicServerで何度か遭遇

• プロダクション再デプロイメントの使用を意図

• WebLogicは悪くない, 前述の AP or ライブラリ起因でリーク

Page 24: Tomcatの実装から学ぶクラスローダリーク #渋谷Java

第十回 #渋谷java

まとめ

• ClassLoaderLeakはホットデプロイにより顕在化

• Tomcatは色々なClassLoaderLeakの検知・解放が可能

• 原因はThreadLocal解放漏れを筆頭に多種多様

• ClassLoaderLeakは怖くない

• ヒープダンプより比較的簡単に原因特定が可能

• Duplicate Class => Path to GC Roots