web-dev-qa-db-ja.com

Javaでのメモリリーク/ガベージコレクションの問題の追跡

これは、私が数か月間追跡しようとしてきた問題です。 xmlフィードを処理し、結果をデータベースに保存するJavaアプリを実行しています。リソースを断続的に追跡するのは非常に困難です。

背景:本番ボックス(問題が最も顕著)では、ボックスへのアクセスが特に良くなく、Jprofilerを実行できませんでした。このボックスは、centos 5.2、Tomcat6、およびJava 1.6.0.11。を実行しているこれらのJava-optsで実行されている64ビットクアッドコア、8GBマシンです。

Java_OPTS="-server -Xmx5g -Xms4g -Xss256k -XX:MaxPermSize=256m -XX:+PrintGCDetails -
XX:+PrintGCTimeStamps -XX:+UseConcMarkSweepGC -XX:+PrintTenuringDistribution -XX:+UseParNewGC"

技術スタックは次のとおりです。

  • Centos 64ビット5.2
  • Java 6u11
  • Tomcat 6
  • Spring/WebMVC 2.5
  • Hibernate 3
  • クォーツ1.6.1
  • DBCP 1.2.1
  • MySQL 5.0.45
  • Ehcache 1.5.0
  • (そしてもちろん、他の依存関係のホスト、特にjakarta-commonsライブラリ)

問題を再現するのに最も近いのは、メモリ要件の低い32ビットマシンです。私がコントロールしていること。私はJProfilerでそれを徹底的に調査し、多くのパフォーマンスの問題を修正しました(同期の問題、xpathクエリのプリコンパイル/キャッシュ、スレッドプールの削減、不要な休止状態のプリフェッチの削除、処理中の過度の「キャッシュウォーミング」)。

いずれの場合も、プロファイラーはこれらを何らかの理由で膨大な量のリソースを消費するものとして示し、変更が行われるとこれらはもはや主要なリソースを消費しないことを示しました。

問題: JVMはメモリ使用設定を完全に無視しているようで、すべてのメモリがいっぱいになり、応答しなくなります。これは、定期的なポーリング(5分ごとおよび1分間の再試行)を期待しているお客様と、ボックスが応答しなくなって再起動する必要があることが常に通知される運用チームにとっての問題です。このボックスで実行される重要なものは他にありません。

問題が表示されますガベージコレクション。元のSTWコレクターがJDBCタイムアウトを引き起こし、ますます遅くなったため、ConcurrentMarkSweep(上記の)コレクターを使用しています。ログには、メモリ使用量が増加するにつれて、つまりcms障害がスローされ始め、元のストップザワールドコレクターにキックバックされ、適切に収集されないように見えることが示されています。

ただし、jprofilerで実行すると、「GCを実行」ボタンはフットプリントを増やすのではなく、メモリをきれいにクリーンアップするように見えますが、jprofilerをプロダクションボックスに直接接続できず、実証済みのホットスポットの解決が機能していないようですガベージコレクションブラインドのチューニングのブードゥーと共に残されました。

私が試したもの:

  • ホットスポットのプロファイリングと修正。
  • STW、パラレル、およびCMSガベージコレクターを使用します。
  • 1/2、2/4、4/5、6/6刻みの最小/最大ヒープサイズで実行します。
  • 256Mのpermgenスペースで実行すると、1Gbまで増加します。
  • 上記の多くの組み合わせ。
  • JVM [チューニングリファレンス](http://Java.Sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html)も参照しましたが、この動作を説明するものや_which_チューニングの例を見つけることができません。このような状況で使用するパラメーター。
  • また、jconsole、visualvmに接続してjprofilerをオフラインモードで(失敗して)試しましたが、gcログデータに干渉するものは見つかりません。

残念ながら、この問題は散発的に発生し、予測不可能であるように見え、問題なく数日間または1週間も実行できるか、1日に40回失敗する可能性があり、一貫してキャッチできる唯一のことはそのガベージコレクションが機能しています。

誰でもアドバイスを与えることができます:
a)JVMが6未満で最大になるように構成されている場合に、8つの物理ギグと2 GBのスワップスペースを使用している理由。
b)GCチューニングへの参照。実際に、高度なコレクションを使用するタイミングと種類の設定の合理的な例を説明または提供します。
c)最も一般的なJavaメモリリークへの参照(要求されていない参照は理解できますが、ライブラリ/フレームワークレベル、またはデータ構造におけるより詳細な情報を意味します)ハッシュマップ)。

提供できるあらゆる洞察に感謝します。

[〜#〜] edit [〜#〜]
エミールH:
1)はい、私の開発クラスターは本番データのミラーであり、メディアサーバーまでです。主な違いは、32/64ビットと利用可能なRAMの量です。これは非常に簡単に複製できませんが、コードとクエリと設定は同じです。

2)JaxBに依存するいくつかのレガシーコードがありますが、スケジューリングの競合を回避するためにジョブを並べ替える際、1日1回実行されるため、その実行は通常排除されます。プライマリパーサーは、Java.xml.xpathパッケージを呼び出すXPathクエリを使用します。これがいくつかのホットスポットの原因でした。1つはクエリがプリコンパイルされておらず、2つはそれらへの参照がハードコードされた文字列であったためです。スレッドセーフキャッシュ(ハッシュマップ)を作成し、xpathクエリへの参照を最終的な静的文字列に分解しました。これにより、リソース消費が大幅に削減されました。クエリは依然として処理の大部分を占めていますが、それはアプリケーションの主な責任であるためです。

3)追加の注意事項として、他の主要な消費者はJAIからの画像操作(フィードからの画像の再処理)です。私はJavaのグラフィックライブラリに慣れていませんが、私が発見したことから、それらは特に漏れやすいものではありません。

(これまでの回答に感謝します、皆さん!)

更新:
VisualVMを使用して実稼働インスタンスに接続できましたが、GCの視覚化/ GCの実行オプションを無効にしていました(ローカルで表示できましたが)。おもしろいこと:VMのヒープ割り当てはJava_OPTSに従い、実際に割り当てられたヒープは1〜1.5ギグで快適に座っており、リークしているようには見えませんが、ボックスレベルの監視リークパターンは引き続き表示されますが、VMモニタリングに反映されません。このボックスでは他に何も実行されていないため、困惑しています。

79
liam

さて、私はついにこれを引き起こしている問題を発見し、誰かがこれらの問題を抱えている場合に備えて詳細な回答を投稿しています。

プロセスが動作している間にjmapを試しましたが、これにより通常jvmがさらにハングアップするため、-forceで実行する必要があります。これにより、大量のデータが欠落しているか、少なくともそれらの間の参照が欠落しているように見えるヒープダンプが発生しました。分析のために、jhatを試しました。これは、大量のデータを表示しますが、その解釈方法にはあまり影響しません。次に、Eclipseベースのメモリ分析ツール( http://www.Eclipse.org/mat/ )を試しました。これは、ヒープの大部分がTomcatに関連するクラスであることを示していました。

問題は、jmapがアプリケーションの実際の状態を報告せず、シャットダウン時にクラス(ほとんどの場合Tomcatクラス)のみをキャッチすることでした。

さらに数回試してみましたが、モデルオブジェクトの数が非常に多いことに気づきました(実際には、データベースで公開されているマークの2〜3倍)。

これを使用して、スロークエリログと、いくつかの無関係なパフォーマンスの問題を分析しました。超遅延ロード( http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html )を試し、いくつかの休止状態の操作を直接jdbcクエリ(主に大規模なコレクションの読み込みと操作を扱っていた場所-jdbcの置換は結合テーブルで直接機能していました)、およびmysqlがログに記録していた他の非効率的なクエリを置き換えました。

これらの手順により、フロントエンドのパフォーマンスが改善されましたが、リークの問題は解決されませんでした。アプリは依然として不安定で、予測できない動作をしていました。

最後に、オプション-XX:+ HeapDumpOnOutOfMemoryErrorを見つけました。これにより、最終的にアプリケーションの状態を正確に示す非常に大きな(〜6.5GB)hprofファイルが生成されました。皮肉なことに、ファイルは非常に大きかったため、16GBのRAMを備えたボックスであっても、jhatはファイルを分析できませんでした。幸いなことに、MATは見栄えの良いグラフを作成し、より良いデータを示しました。

今回出てきたのは、6GBのヒープのうち4.5GBを占める単一のクォーツスレッドであり、その大半は休止状態のStatefulPersistenceContext( https://www.hibernate.org/hib_docs/v3/api /org/hibernate/engine/StatefulPersistenceContext.html )。このクラスは、一次キャッシュとして内部的にhibernateによって使用されます(EHCacheによってバックアップされた2次レベルとクエリキャッシュを無効にしました)。

このクラスは、休止状態のほとんどの機能を有効にするために使用されるため、直接無効にすることはできません(直接回避できますが、スプリングはステートレスセッションをサポートしていません)。成熟した製品の主要なメモリリーク。それで、なぜ今漏れたのですか?

それは、クォーツスレッドプールが特定の事柄をthreadLocalとしてインスタンス化し、春がクォーツスレッドライフサイクルの開始時にセッションファクトリをインジェクトし、それを実行するために再利用されていた休止状態セッションを使用したさまざまなクォーツジョブ。 Hibernateはセッションでキャッシュしていましたが、これは予想される動作です。

問題は、スレッドプールがセッションを解放しなかったため、休止状態が維持され、セッションのライフサイクルの間キャッシュを維持していたことです。これはsprings hibernateテンプレートサポートを使用していたため、セッションの明示的な使用はありませんでした(dao-> manager-> driver-> quartz-job階層を使用しており、daoにはspringを介してhibernate configsが挿入されているため、操作はテンプレートで直接行われます)。

そのため、セッションは閉じられず、休止状態はキャッシュオブジェクトへの参照を維持していたため、ガベージコレクションされませんでした。そのため、新しいジョブが実行されるたびに、スレッドのローカルキャッシュがいっぱいになり、異なるジョブ間の共有。また、これは書き込み集約型のジョブ(読み取りが非常に少ない)であるため、キャッシュはほとんど無駄になり、オブジェクトが作成され続けました。

解決策:session.flush()およびsession.clear()を明示的に呼び出すdaoメソッドを作成し、各ジョブの開始時にそのメソッドを呼び出します。

このアプリは、監視の問題、メモリエラー、再起動なしで数日間実行されています。

すべての人がこれを助けてくれてありがとう、追跡するのは非常に厄介なバグでした。すべてが意図したとおりに動作していたので、最終的には3行の方法ですべての問題を修正できました。

90
liam

ヒープ以外のメモリがリークしているようです。ヒープは安定したままです。古典的な候補は、ロードされたクラスオブジェクトとインターンされた文字列の2つのことで構成されるpermgen(永続的な世代)です。 VisualVMに接続したことを報告するので、ロードされたクラスの量を確認できるはずです。loadedクラスの継続的な増加がある場合(重要、visualvmはこれまでにロードされたクラスの合計量も表示します。これが上がっても大丈夫ですが、ロードされたクラスの量は一定時間後に安定するはずです)。

Permgenリークであることが判明した場合、permgen分析用のツールがヒープと比較してかなり不足しているため、デバッグが難しくなります。最善の策は、サーバー上で繰り返し(1時間ごとに)起動する小さなスクリプトを開始することです。

jmap -permstat <pid> > somefile<timestamp>.txt

このパラメーターを指定したjmapは、ロードされたクラスの概要とバイト単位のサイズの推定値を生成します。このレポートは、特定のクラスがアンロードされないかどうかを識別するのに役立ちます。 (注:プロセスIDを意味し、ファイルを区別するために生成されたタイムスタンプでなければなりません)

特定のクラスがロードされ、アンロードされていないことを特定したら、それらがどこで生成される可能性があるかを精神的に把握できます。あなたが情報を必要とするならば、私は将来のアップデートのためにそれを保ちます。

4
Boris Terzic

JMXを有効にしてプロダクションボックスを実行できますか?

_-Dcom.Sun.management.jmxremote
-Dcom.Sun.management.jmxremote.port=<port>
...
_

JMXを使用した監視と管理

そして、JConsoleでアタッチします VisualVM

jmap でヒープダンプを行うことはできますか?

はいの場合、JProfiler(すでに持っている)、 jhat 、VisualVM、 Eclipse MAT でリークのヒープダンプを分析できます。また、リーク/パターンの検出に役立つ可能性があるヒープダンプを比較します。

そして、あなたがjakarta-commonsに言及したように。クラスローダーへの保持に関連するjakarta-commons-loggingを使用すると問題が発生します。そのチェックの良い読み物のために

メモリリークハンターの人生の1日release(Classloader)

4
jitter

直接割り当てられたByteBufferを探します。

Javadocから。

このクラスのallocateDirectファクトリメソッドを呼び出すことにより、直接バイトバッファを作成できます。このメソッドによって返されるバッファーは、通常、非直接バッファーよりも割り当てと割り当て解除のコストがいくらか高くなります。ダイレクトバッファの内容は、通常のガベージコレクションヒープの外側に存在する可能性があるため、アプリケーションのメモリフットプリントへの影響は明らかではありません。したがって、直接バッファは、主に、基になるシステムのネイティブI/O操作の影響を受ける大規模で長期間存続するバッファに割り当てることをお勧めします。一般に、ダイレクトバッファは、プログラムのパフォーマンスが測定可能なほど向上した場合にのみ割り当てるのが最適です。

おそらく、TomcatコードはこれをI/Oに使用します。別のコネクタを使用するようにTomcatを構成します。

System.gc()を定期的に実行するスレッドを作成できない場合。 「-XX:+ ExplicitGCInvokesConcurrent」は、試してみるのに興味深いオプションです。

2
Sean McCauliff

同じ問題がありましたが、いくつかの違いがありました。

私の技術は次のとおりです。

聖杯2.2.4

Tomcat7

quartz-plugin 1.0

アプリケーションで2つのデータソースを使用します。これは、バグの原因に対する特定の決定要因です。

考慮すべきもう1つのことは、クォーツプラグインが、@ liamが言うようにクォーツスレッドで休止状態セッションを注入し、クォーツスレッドがアプリケーションを終了するまで生きていることです。

私の問題は、プラグインがセッションと2つのデータソースを処理する方法と組み合わされたgrails ORMのバグでした。

Quartzプラグインには、Hibernateセッションを初期化および破棄するためのリスナーがありました

_public class SessionBinderJobListener extends JobListenerSupport {

    public static final String NAME = "sessionBinderListener";

    private PersistenceContextInterceptor persistenceInterceptor;

    public String getName() {
        return NAME;
    }

    public PersistenceContextInterceptor getPersistenceInterceptor() {
        return persistenceInterceptor;
    }

    public void setPersistenceInterceptor(PersistenceContextInterceptor persistenceInterceptor) {
        this.persistenceInterceptor = persistenceInterceptor;
    }

    public void jobToBeExecuted(JobExecutionContext context) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.init();
        }
    }

    public void jobWasExecuted(JobExecutionContext context, JobExecutionException exception) {
        if (persistenceInterceptor != null) {
            persistenceInterceptor.flush();
            persistenceInterceptor.destroy();
        }
    }
}
_

私の場合、persistenceInterceptorインスタンスはAggregatePersistenceContextInterceptorであり、HibernatePersistenceContextInterceptorのリストがありました。データソースごとに1つ。

すべての操作は、AggregatePersistenceContextInterceptorを使用して、変更や処理なしでHibernatePersistenceに渡されます。

HibernatePersistenceContextInterceptorinit()を呼び出すと、以下の静的変数をインクリメントします

private static ThreadLocal<Integer> nestingCount = new ThreadLocal<Integer>();

その静的なカウントの目的がわかりません。 AggregatePersistenceの実装により、データソースごとに1つずつ、2回インクリメントされることを知っています。

ここまでは、シナリオを説明するだけです。

問題は今来ています...

クォーツジョブが終了すると、プラグインはリスナーを呼び出して、SessionBinderJobListenerのソースコードに見られるように、休止状態のセッションをフラッシュして破棄します。

フラッシュは完全に行われますが、破棄は行われません。なぜなら、HibernatePersistenceはセッションを閉じる前に1回検証を行うからです... nestingCountを調べて、値が1より大きいかどうかを確認します。

Hibernateによって行われたことを単純化する:

_if(--nestingCount.getValue() > 0)
    do nothing;
else
    close the session;
_

これが私のメモリリークの原因です。2つのデータソースがあるために発生するバグのため、grails ORMはセッションを閉じないため、セッションで使用されるすべてのオブジェクトでクォーツスレッドがまだ生きています。

それを解決するために、destroyの前にclearを呼び出し、destroyを2回呼び出すようにリスナーをカスタマイズします(データソースごとに1回)。私のセッションがクリアで破壊されていることを確認し、破壊が失敗した場合、少なくとも彼はクリアでした。

1
jpozorio

JAXB? JAXBはperm space stufferであることがわかりました。

また、JDK 6に同梱されている visualgc は、メモリで何が起こっているのかを確認するのに最適な方法であることがわかりました。エデン、世代、パーマの各スペースとGCの過渡的な動作を美しく示しています。必要なのは、プロセスのPIDだけです。 JProfileで作業しているときに役立つかもしれません。

また、Springのトレース/ロギングの側面についてはどうですか?たぶん、あなたは単純な側面を書いて、それを宣言的に適用し、貧しい人のプロファイラーをそのように行うことができます。

1
duffymo

「残念ながら、問題は散発的に発生し、予測不可能であるように思われます。何日も、または1週間も問題なく実行できます。1日に40回失敗することもあります。ガベージコレクションが機能しているということです。」

どうやら、これは1日に40回まで実行され、その後数日間は実行されないユースケースにバインドされているようです。症状だけを追跡するのではないことを願っています。これは、アプリケーションのアクター(ユーザー、ジョブ、サービス)のアクションをトレースすることで絞り込むことができるものでなければなりません。

これがXMLインポートによって発生する場合、40クラッシュ日のXMLデータをゼロクラッシュ日にインポートされたデータと比較する必要があります。多分それはある種の論理的な問題で、コードの中だけでは見つけられないでしょう。

1
cafebabe