[Java] JRockit: Artificial GC: Cleaning up Finished Threads

原文はこちら。
https://blogs.oracle.com/buck/entry/jrockit_artificial_gc_cleaning_up

実行が完了したアプリケーションスレッドをJVMがクリーンナップしようとするために頻繁にフルGCを体験しているJRockitユーザーがいらっしゃいますので、このエントリでは、このフルGCの発生原因、貴社環境でのフルGCの識別方法、考えられるソリューションをご説明しましょう。

What exactly is happening?

多くのスレッドが実行を終えたにもかかわらず、対応するjava.lang.ThreadオブジェクトをJavaヒープからガベージコレクションしていない場合、JVMは自動的にフルGCイベントを呼び出し、残存するThreadオブジェクトをクリーンアップしようとします。

Why does JRockit do this?

JRockitの内部設計により、 対応するThreadオブジェクトをJavaヒープから除去してからでないと、 JVMは実行スレッドに関連づけられている全ての内部リソースは解放することができません。これは通常問題にならないのですが、ガベージコレクションの間隔が長く、アプリケーションが数多くのスレッドを作成(し、破棄)する場合、こうしたデッドスレッドが消費する メモリや他のネイティブリソースの量は非常に大きくなります。例えば、デッドスレッドを絶えず「リーク」するテストプログラムを実行し、対応するThreadオブジェクトの各々への強参照が残っている場合、JVMのメモリ使用量はすぐに増大します。それは、数GByteのオフヒープ(ネイティブ)メモリを消費してほとんどのデッドスレッドのデータを維持しようとするからです。

What does this actually look like in practice?

この現象があなたのアプリケーションで起きているかどうかを確認する一番簡単な方法は、JRockitの memdbg冗長ログモジュール (*1) を有効にすることです。フルGCがThreadオブジェクトのクリーンアップを待つスレッドによって引き起こされると、GCイベント(*2) の開始時点で次のようなメッセージを確認することでしょう。
===
[DEBUG][memory ] [OC#7] GC reason: Artificial, description: Cleaning up Finished Threads.
===
こうしたスレッド関連のフルGCイベントを提供する別の冗長ログ出力モジュールは、スレッドモジュールです。
===
[INFO ][thread ] Trigging GC due to 5772 dead threads and 251 threads since last collect.
===
上記メッセージは、実行が終了したスレッドが現在5772個ありますが、 GCサブシステムがThreadオブジェクトを集めるのを待っています(その後、各スレッドに関連づけられている全てのリソースを完全に解放できます)。また、スレッド管理コードが引き起こした直近のフルGC以後、251個の新しいスレッドが生成されていることもわかります。つまり、「直近の収集」とは「スレッドクリーンアップコードが引き起こした直近の収集」ということです。この数は通常のGCイベントでゼロにリセットされることはなく、スレッドクリーンアップGCだけがこの数値をリセットします。

What can I do to avoid these collections?

当然ながら、最善のソリューションはそんなに多くのスレッドを作成しないことです。スレッドはコストが高いのです。アプリケーションが非常に多くのスレッドを継続的に作成(そして破棄)しているために、250スレッドごとに発生するフルGCが性能上の問題になるのであれば、おそらくそれはアプリケーションの設計をやり直し、使い捨てするようなスレッドの使い方をしないようにした方が良いでしょう。例えば、スレッドプーリングはリーズナブルな解決策になる可能性があります。HotSpot上でアプリケーションを実行していたとしても、これほど多くのスレッドを一時的に作成すると、パフォーマンスの問題が発生する可能性が高いのです。どんなJVMを使っているかに関係なく、アプリケーションを変更してスレッドを効率良く利用するようにすることを検討すべきです。
しかし、アプリケーションの再設計が有効な選択肢でない場合も当然ながらあり得ます。もしかすると、こうしたスレッドを生成しているコードはサードパーティ製で変更できないものかもしれません。もしくは、アプリケーションの変更を計画してはいるものの、しばしの間はこれらのフルGCイベントに対処する必要がある、といった場合もあるかもしれません。JVMサイドのソリューションはあるにはありますが、一時的な回避策と考えるべきでしょう。

JRockitには、MaxDeadThreadsBeforeGCとMinDeadThreadsSinceGCという2個の診断オプションがあり、これらを使うとGCの挙動を調整することができます。両者の間の違いは微妙ですが、基本的には両者を使ってスレッド管理コードがフルGCを引き起こすタイミングを決定する閾値を変更することができます。
  • MaxDeadThreadsBeforeGC (デフォルト値: 250)
    このオプションは、人為的なフルGCを引き起こすデッドスレッド(実行が完了したものの、対応するThreadオブジェクトがJavaヒープから除去されるのを待っているために 、まだクリーンアップされていないスレッド)の個数を指定します。
  • MinDeadThreadsSinceGC (デフォルト値: 250)
    このオプションは、 スレッドクリーンアップのためのGCイベント間に 新しく生成するスレッドの最小個数を指定します。この意図は、フルGCを引き起こしたとしても、Threadオブジェクトへの強参照があるために、デッドスレッドのサブセットがまだ残存する可能性があることです。つまり、フルGCを生き延びたデッドスレッドの個数がMaxDeadThreadsBeforeGCよりも大きい場合には、新しくスレッドを作成するたびにフルGCを引き起こす可能性があります。MinDeadThreadsSinceGC オプションは、デッドスレッドがMaxDeadThreadsBeforeGCで定義している個数を上回っていたとしても、直近のスレッドクリーンアップGC以後、少なくともある程度の新しいスレッドを生成することを保証します。
これらの2個のオプションは、Xverbose:threadの出力に現れる2個の数値に対応していることにご注意ください。
===
[INFO ][thread ] Trigging GC due to 5772 dead threads and 251 threads since last collect.
===
最初の数値(5772)はクリーンアップを待っているデッドスレッドの個数であり、MaxDeadThreadsBeforeGCの値と比較しています。2個目の数値(251)は、直近のスレッドクリーンアップのためのフルGC以後に生成された新しいスレッドの個数です。この2個目の数値はMinDeadThreadsSinceGCと比較しています。両数値が閾値を上回っている場合に限り、スレッドクリーンアップのGCが発生します。

スレッドクリーンアップのGCの頻度を減らす必要があり、アプリケーションを修正できない場合には、MinDeadThreadsSinceGCの閾値を大きな値にすることをお薦めします。トレードオフとして、JVMが前もって実行終了したスレッドをクリーンアップする頻度が小さくなるためにネイティブメモリの使用量が増えますので、当然ながら、こうしたオプションを本番環境で利用する前に、パフォーマンスおよび負荷テストを実施すべきです。

もう一つ重要なことですが、これらのオプションは両方ともドキュメント化されていない診断オプションです。R28では、これらのオプションを使うためには、 コマンドラインにおいて-XX:+UnlockDiagnosticVMOptionsオプションを、任意の診断オプションの前に付ける必要があります。例えば、MinDeadThreadsSinceGCを1000に設定する場合、以下のようなコマンドラインを使います。
===
$ java -XX:+UnlockDiagnosticVMOptions -XX:MaxDeadThreadsBeforeGC=1000 MyJavaApp
===
この記事全体を読み、ネイティブメモリ消費量が増大するというリスクをを理解し、本番環境に展開する前にアプリケーションの負荷テストを徹底的に実施することをいとわないのであれば、これらのオプションのいずれを使っても安全でしょう。

(*1) Xverboseオプションの使い方の詳細は以下のコマンドラインリファレンスを参照してください。
Oracle® JRockit Command-Line Reference Release R28
http://docs.oracle.com/cd/E15289_01/doc.40/e15062/optionx.htm#i1020876
(*2) この出力例は、R28からのものであることに注意してください。 R27の場合、"Floating Dead Threads"としてGCの原因に言及しています。

0 件のコメント:

コメントを投稿