1. ホーム
  2. java

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

2023-08-18 18:17:59

質問

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

背景 本番環境 (問題が最も顕著な場所) では、私は特に良好なアクセス権を持っておらず、Jprofiler を実行することができませんでした。そのマシンは、64ビットのクアッドコア、8GBのマシンで、centos 5.2、tomcat6、java 1.6.0.11が動作しています。以下のjava-optで起動します。

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
  • Quartz 1.6.1
  • DBCP 1.2.1
  • Mysql 5.0.45
  • Ehcache 1.5.0
  • (そしてもちろん、他の多くの依存関係、特に jakarta-commons ライブラリ)

私がこの問題の再現に最も近づけたのは、より低いメモリ要件を持つ 32 ビット マシンです。これは、私がコントロールできるものです。私は JProfiler で死ぬほど調査し、多くのパフォーマンス問題 (同期の問題、xpath クエリの事前コンパイル/キャッシュ、スレッドプールの削減、不要な hibernate プリフェッチの削除、および処理中の過剰なキャッシュウォーミングアップ) を修正しました。

それぞれのケースで、プロファイラーは、1 つの理由または別の理由で膨大な量のリソースを取っていることを示し、変更が行われると、これらはもはや主要なリソース占有者ではなくなることを示しました。

問題点。 JVMは、メモリ使用量の設定を完全に無視し、すべてのメモリを満たし、応答しなくなるようです。これは、定期的なポーリング (5 分基準と 1 分リトライ) を期待する顧客側の問題であり、同様に、ボックスが応答しなくなったことを常に通知されてそれを再起動する必要がある運用チームにとっても、問題です。このボックスでは、他に重要なことは何も実行されていません。

問題点 が表示されます。 はガベージ コレクションであると思われます。オリジナルの STW コレクターが JDBC タイムアウトを引き起こし、ますます遅くなったため、ConcurrentMarkSweep (前述のとおり) コレクターを使用しています。ログによると、メモリ使用量が増加すると、cms 障害が発生し始め、元の stop-the-world コレクターにキックバックされ、その後適切に収集されないようです。

しかし、jprofiler で実行すると、"Run GC" ボタンは、増加するフットプリントを示すのではなく、メモリをうまくクリーンアップするようですが、jprofiler を本番ボックスに直接接続できず、実証済みのホットスポットを解決しても、機能していないようなので、ガーベッジ コレクションをブラインドで調整するというブードゥーを残しています。

私が試したこと。

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

残念ながら、この問題は散発的に発生し、予測不可能なようで、何日も、あるいは 1 週間も問題なく動作することもあれば、1 日に 40 回失敗することもあり、一貫してキャッチできる唯一のことは、ガベージ コレクションが動作しているということです。

どなたか、次のようなアドバイスをお願いします。

a) 最大で 6 ギガ未満になるように構成されているのに、なぜ JVM が 8 ギガの物理メモリと 2 GB のスワップ領域を使用するのですか。

b) いつ、どのような種類の設定で高度なコレクションを使用するかについて、実際に説明するか合理的な例を示す GC チューニングへの言及。

c) 最も一般的なJavaのメモリリークへの参照(要求されていない参照を理解していますが、ライブラリ/フレームワークレベル、またはハッシュマップのようなデータ構造でより一般的なものを意味します)。

あなたが提供できるすべての洞察に感謝します。

EDIT

エミルH.

1) はい、私の開発クラスタは、メディア・サーバーに至るまで、本番データのミラーです。主な違いは、32/64 ビットと使用可能な RAM の量で、これはあまり簡単に再現できませんが、コードとクエリーと設定は同一です。

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

3) 付記:他の主要な消費者は、JAIからの画像操作(フィードからの画像の再処理)です。私は java のグラフィック ライブラリに詳しくないのですが、私が見つけたものからすると、それらは特に漏れやすいものではありません。

(これまでの回答、みなさんありがとうございました!)

UPDATEです。

私はVisualVMで本番インスタンスに接続することができましたが、それはGCの可視化/実行GCオプションを無効にしていました(私はそれをローカルに見ることができましたが)。興味深いことに、VMのヒープ割り当てはJAVA_OPTSに従っており、実際に割り当てられたヒープは1-1.5ギガで快適に座っており、リークしていないように見えますが、ボックスレベルのモニタリングはまだリークパターンを示していますが、それはVMモニタリングに反映されていません。このボックスでは他に何も実行されていないので、私は困っています。

どのように解決したらよいでしょうか。

さて、私はついにこの原因となっていた問題を発見しました。他の誰かがこのような問題を抱えている場合に備えて、詳細な回答を投稿します。

プロセスが動作している間に 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 ) を試してみたり、いくつかの hibernate 操作を直接jdbcクエリに置き換えてみたり (ほとんどは大きなコレクションの読み込みと操作を扱っているところです。jdbc置換は結合テーブル上で直接動作します)、mysqlが記録している他のいくつかの非効率的なクエリを置き換えたりしました。

これらの手順により、フロントエンドのパフォーマンスの一部が改善されましたが、リークの問題には対処できず、アプリはまだ不安定で予測不可能な動作をしていました。

最後に、私はオプションを見つけました: -XX:+HeapDumpOnOutOfMemoryError 。これは、最終的に、アプリケーションの状態を正確に示す非常に大きな (~6.5GB) hprof ファイルを作成しました。皮肉なことに、このファイルは非常に大きく、16GBのメモリを持つマシン上でさえ、jhatはそれを解析することができませんでした。幸いなことに、MAT は見栄えのするグラフを作成することができ、より良いデータを表示することができました。

今回目立ったのは、単一の Quartz スレッドが 6GB のヒープのうち 4.5GB を占有しており、その大部分はハイバーネートの StatefulPersistenceContext ( https://www.hibernate.org/hib_docs/v3/api/org/hibernate/engine/StatefulPersistenceContext.html ). このクラスは、hibernate によって内部的にプライマリ キャッシュとして使用されます (私は EHCache によってバックアップされる第 2 レベルおよびクエリ キャッシュを無効にしていました)。

このクラスは、hibernate のほとんどの機能を有効にするために使用されるので、直接無効にすることはできません (直接回避することはできますが、spring はステートレス セッションをサポートしません)。では、なぜ今になってリークしたのでしょうか?

まあ、いろいろなことが重なっていたのでしょう。 Spring はセッション ファクトリーを注入し、Quartz スレッドのライフサイクルの開始時にセッションを作成し、それが hibernate セッションを使用するさまざまな Quartz ジョブの実行に再利用されていました。その後、Hibernate セッションを使用するさまざまな Quartz ジョブを実行するために再利用されました。Hibernate はセッションでキャッシュしており、これは期待される動作です。

問題は、スレッドプールがセッションを解放しないため、Hibernate が常駐し、セッションのライフサイクルのキャッシュを維持することです。これはspringsのhibernateテンプレートサポートを使用していたため、セッションを明示的に使用することはありませんでした(私たちはdao -> manager -> driver -> quartz-job階層を使用しており、daoはspringを通じてhibernate設定に注入されているので、操作はテンプレート上で直接行われます)。

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

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

アプリは数日間稼働していますが、監視の問題、メモリー エラー、再起動はありません。

この件に関するみなさんの協力に感謝します。すべてが想定どおりに動いていたので、追跡するのはかなり難しいバグでしたが、最終的に 3 行の方法ですべての問題を解決することができました。