BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Javaガベージコレクションのエッセンス

Javaガベージコレクションのエッセンス

ブックマーク

原文(投稿日:2013/06/17)へのリンク

Serial、Parallel、Concurrent、CMS、G1、Young Gen、New Gen、Old Gen、Perm Gen、Eden、Tenured、Survivor Space、Safepoint、そして、何百ものJVMスタートアップフラグ。Javaアプリケーションから、要求されたスループットと停止時間を実現しようとして、ガベージコレクタをチューニングしようとする時、これらをどう扱えばいいのか困りませんか? そんな時は心配しないでください。あなただけではありませんから。ガベージコレクタについて書かれたドキュメントは、航空機のマニュアルのようです。ノブとダイアルについて1つ1つ詳細に書かれていますが、飛び方のガイドはどこにもありません。この記事では、特定の仕事負荷に対してガベージコレクションのアルゴリズムを選んで、チューニングする場合のトレードオフを説明します。

ここでは、最も共通的に使われるOracle Hotspot JVMやOpenJDKコレクタに注目します。後半は、別の選択肢を示すために、商用のJVMについて議論します。

トレードオフ

賢い人たちは、「何もしなければ何も得られない」と言い続けています。何かを手に入れたときには、大抵、代わりに何かをあきらめなければなりません。ガベージコレクションに関しては、コレクタにターゲットを設定した3つの主な変数を使います。

  1. スループット: GCで消費する時間の比率により、アプリケーションによって実行される作業量。ターゲットスループット -XX:GCTimeRatio=99 99はデフォルトで、GC時間は1%です。
  2. 停止時間: ガベージコレクションによって発生した停止により影響を受けるイベントに対応して、システムで必要になる時間。GCの停止によるターゲット停止時間 -XX:MaxGCPauseMillis=<n>
  3. メモリ: 状態を格納するためにシステムが使うメモリの量。管理されているときにしばしばコピーされたり、移動されたりします。いずれかの時点でアプリケーションによって保持されるアクティブオブジェクトのセットは、ライブセットとして知られています。最大ヒープサイズ–Xmx<n>は、アプリケーションで利用可能なヒープサイズを設定するチューニングパラメタです。

注: 多くの場合、Hotspotはこれらのターゲットを達成できず、警告もなく静かに継続します。そのため、ターゲットからは大きく外れてしまいます。

停止時間は、イベント全体に配分されます。最長の停止時間を減らしたり、頻繁に起きないようにしたりするために、平均停止時間を増やすことは受け入れられるでしょう。「リアルタイム」という言葉は、最小停止時間の意味に解釈すべきではありません。むしろ、スループットに関わらず、決定的な停止時間を持つことを意味します。

あるアプリケーションの作業にとって、スループットは最も重要なターゲットです。1つ例を挙げると、長時間実行されるバッチ処理のジョブです。ガベージコレクションが実行されている間、バッチジョブが時々1、2秒止まっても、ジョブ全体がすぐに完了すれば問題ありません。

人間が直接対話するアプリケーションから金融取引システムまで、実質的な他のすべての作業では、システムが1、2秒か、数ミリ秒以上反応しない場合、大変なことになり得ます。金融取引では、しばしば一貫した停止時間と引き換えに、スループットを犠牲にするだけの価値はあります。物理的に利用可能なメモリ量によって制限されるアプリケーションを持ったり、footprintを維持しなければならなかったりすることもあります。そのような場合、停止時間とスループットの面の両方で、パフォーマンスをあきらめなければなりません。

以下のトレードオフは度々起こります。

  • 大部分は、ガベージコレクションのコストは、償却コストとして、メモリを大きくしてガベージコレクションのアルゴリズムを使えば減らせます。
  • ガベージコレクションの一時停止を含む、観察される最悪の停止時間は、ライブセットを含んで、ヒープサイズを小さくすることで減らせます。
  • 一時停止が起きる頻度は、ヒープとジェネレーションサイズを管理し、アプリケーションのオブジェクトの割当レートをコントロールすることで減らせます。
  • 長い一時停止の頻度は、時にはスループットを犠牲にして、アプリケーションで同時にGCを動かすことで減らせます。

オブジェクトのライフタイム

ガベージコレクションのアルゴリズムは、ほとんどのオブジェクトがわずかな時間存続し、相対的にほんの少しのオブジェクトが長時間存続するという予想に基づいて、最適化されています。大抵のアプリケーションでは、非常に長い時間存続するオブジェクトは、時間をかけて割り当てられたオブジェクトのほんのわずかなパーセントを構成する傾向があります。ガベージコレクションの理論では、この観察されたふるまいは、しばしば「初期死亡率」や「弱い世代仮説」として知られています。例えば、スタティックのStringが効率的に永続する一方で、ループするイテレータは、大抵、非常に短命です。

実験によると、ジェネレーションのガベージコレクタは、通常、非ジェネレーションコレクタよりも、10倍以上のスループットをサポートできるので、サーバJVMではほとんどどこでも使われています。オブジェクトのジェネレーションを分けることで、新しく割り当てられたオブジェクトの領域は、ライブオブジェクトがわずかになることが分かっています。そのため、この新しい領域にある数少ないライブオブジェクトを取り出し、古いオブジェクトのための別の領域にコピーするコレクタは、とても効率的です。Hotspotガベージコレクタは、生き残ったGCサイクルの数を把握するため、オブジェクトの存続時間を記録します。

注: アプリケーションが、かなり長い時間存続するオブジェクトを連続して生成する場合、そのアプリケーションは、大部分の時間をガベージコレクションに費やし、ホットスポットガベージコレクタのチューニングに時間がかかると予想できます。これは、GCの効率が下がったためであり、ジェネレーションの「フィルタ」があまり効率的ではないときに起こります。そのため、より長く存続するジェネレーションをより頻繁に収集することになります。Oldジェネレーションほど数は少なくならず、その結果、Oldジェネレーションの収集アルゴリズムはずっと遅くなります。通常、ジェネレーションのガベージコレクタは、2つの異なる収集サイクルで実施されます。マイナーコレクションでは存続期間の短いオブジェクトが収集され、あまり実行されないメジャーコレクションでは、古い領域が収集されます。

Stop-The-Worldイベント

ガベージコレクション中にアプリケーションが一時停止する原因は、stop-the-worldイベントとして知られています。ガベージコレクタがこのイベントを実行するのは、実際のエンジニアリング上の理由により、メモリを管理するために、実行中のアプリケーションを定期的に停止する必要があるためです。アルゴリズムにより、種々のコレクタは、様々な存続時間で実行される特定のポイントで、stop-the-worldが起こります。アプリケーションを完全に停止させるには、動いているスレッドをすべて停止する必要があります。ガベージコレクタは、スレッドが「Safepoint」に来たら停止するシグナルを出して、スレッドを停止させます。「Safepoint」とは、すべてのGCルートが認識されていて、すべてのヒープオブジェクトの内容が一致する、プログラム実行中のポイントです。スレッドが何をしているかによって、Safepointに到達するにはいくらか時間がかかるかもしれません。Safepointチェックは、通常、メソッドリターンやループバックのエッジで実行されますが、動的にあまりチェックしないようにするために、ある場所では最適化により取り除かれます。例えば、スレッドが大きな配列をコピーしたり、大きなオブジェクトをクローンしたり、単純に計算する有限のループを実行したりする場合、Safepointに到達する前に、何ミリセコンドもの時間がかかるでしょう。Safepointまでの時間は、低遅延アプリケーションでは、十分に考慮すべき点です。この時間は、GCの他のフラグに
-XX:+PrintGCApplicationStoppedTimeフラグを付けて表示できます。

: 動いているスレッドが沢山あるアプリケーションでstop-the-worldイベントが起きると、Safepointから開放されたスレッドが再開する時に、システムは非常に大きなスケジューリングのプレッシャを受けます。そのため、stop-the-worldにあまり頼らないアルゴリズムが、もしかするとより効率的かもしれません。

ホットスポットのヒープ組織

様々なコレクタがどのように動作するのかを理解するには、Javaヒープがジェネレーションコレクタをサポートするためにどのように組織化されているかを探るのが一番良いでしょう。

Edenは、ほとんどのオブジェクトが最初に割り当てられる領域です。survivorスペースはEdenスペースのコレクションを生き残ったオブジェクトの一時保管場所です。Survivorスペースの使用は、minorコレクションについて述べるときに説明しましょう。ひとまとめにして、Edensurvivorスペースは、「young」、または、「new」ジェネレーションとして知られています。

十分長く存続するオブジェクトは、最終的にtenured (終身) スペースに移動します。

perm (永続) ジェネレーションは、クラスやスタティックなStringなど、実際に永続すると「分かっている」オブジェクトをランタイムに保持します。残念ながら、数多くのアプリケーションで継続してクラスローディングを共通的に利用するのは、クラスが永続するpermジェネレーションの陰で動機づけられている仮定を間違ったものにします。Java 7で、intern()で返されたStringは、permgenからtenuredに移動させられました。Java 8からは使われず、permジェネレーションについてこの記事ではこれ以上議論しません。他の商用コレクタのほとんどは、別々のpermスペースを使わず、長時間存続するすべてのオブジェクトをtenuredとして扱います。

: 仮想スペースにおいて、コレクタはスループットと停止時間のターゲットに合うように、領域のサイズを調整できるようになっています。コレクタは、各コレクションフェーズの統計を保持し、ターゲットを達成できるように領域サイズを調整します。

オブジェクトの割り当て

競合を避けるために、各スレッドは、オブジェクトを割り当てるスレッドローカル割当バッファ (TLAB) が割り当てられます。TLABを使えば、1つのメモリリソースにおける競合を避けることで、オブジェクトの割り当てを多くのスレッドに拡大できます。TLABを使ったオブジェクトの割り当ては、ほとんどコストがかからない操作です。ほとんどのプラットフォームにおいて、おおよそ10命令でオブジェクトサイズのポインタを動かすだけです。Javaのヒープメモリ割り当ては、Cランタイムからmallocを使うよりも、コストがかかりません。

: 個々のオブジェクト割り当てはコストがかかりませんが、マイナーコレクションが発生しなければならないレートは、オブジェクト割り当てのレートに直接比例します。

TLABが使い尽くされると、スレッドは単にEdenスペースから新しい割り当てを要求します。Edenがいっぱいになると、マイナーコレクションが始まります。

大きな配列のような大きなオブジェクト (-XX:PretenureSizeThreshold=n) は、youngジェネレーションに適合しなくなると、oldジェネレーションに割り当てなければなりません。しきい値がTLABサイズよりも低く設定されると、TLABに合うオブジェクトはoldジェネレーションの中には作られません。新しいG1コレクタは、大きなオブジェクトを別の方法で扱います。このことは、後ほど別のセクションで説明します。

マイナーコレクション

マイナーコレクションは、Edenがいっぱいになると実行されます。newジェネレーションにあるすべてのライブオブジェクトが、必要に応じて、survivorスペースか、tenuredスペースにコピーされるのです。tenuredスペースにコピーされることは、移動や終身として知られています。移動は、オブジェクトが十分に古くなったか (– XX:MaxTenuringThreshold)、survivorスペースがあふれた時に起こります。

ライブオブジェクトは、アプリケーションが到達できるオブジェクトです。他のオブジェクトは到達できず、死んでいるとみなされます。マイナーコレクションでは、ライブオブジェクトをコピーすることは、まずGCルートとして知られるものに従って実行され、到達できるものは何でも、繰り返しsurvivorスペースにコピーします。アプリケーションやJVM内部スタティクフィールドからの参照や、スレッドスタックフレームやアプリケーションの到達可能なオブジェクトのグラフを実際に示すポイントすべてからの参照を、GCルートは含んでいます。

ジェネレーションコレクションで、newジェネレーションの到達可能なオブジェクトグラフのGCルートは、oldジェネレーションからnewジェネレーションへの参照も含んでいます。これらの参照は、newジェネレーションのすべての到達可能なオブジェクトがマイナーコレクションを確実に生き残るように、処理されなければなりません。これらのジェネレーションに渡る参照は、「カードテーブル」を使って実現できます。Hotspotカードテーブルは、バイトの配列であり、各バイトはoldジェネレーションの512バイト領域に対応して、ジェネレーションに渡る参照が潜在的に存在することを追跡するために使われます。参照はヒープに保存されるので、「保存バリア」コードによってカードに記録し、oldジェネレーションからnewジェネレーションへの潜在的参照が、関連する512バイトヒープ領域に存在することを示します。コレクション中に、カードテーブルは、そのようなジェネレーションに渡る参照をスキャンするのに使われ、追加のGCルートをnewジェネレーションの中に効果的に示します。そのため、マイナーコレクションの重大な修正コストは、oldジェネレーションの大きさに直接比例します。

Hotspotのnewジェネレーションには、2つのsurvivorスペースがあり、「to-space」と「from-space」の役割を交替します。マイナーコレクションの初めに、「to-spacesurvivorスペースはいつも空で、マイナーコレクションのターゲットコピーエリアの役割をします。前のマイナーコレクションのターゲットsurvivorスペースは、「from-space」の一部であり、Edenを含みます。そこでは、コピーが必要なライブオブジェクトが見つかるかもしれません。

マイナー GCコレクションのコストは、通常、survivorスペースとtenuredスペースにオブジェクトをコピーするコストによって左右されます。マイナーコレクションで生き残れないオブジェクトは、事実上、自由に扱われます。マイナーコレクションの間に行われた作業は、見つかったライブオブジェクトの数にそのまま比例し、newジェネレーションのサイズに比例するのではありません。マイナーコレクションを実行するのに使った全体の時間は、Edenのサイズが2倍になる度に、ほぼ二等分されます。メモリは、そのためスループットと交換されます。Edenのサイズを2倍にすることで、コレクション毎のコレクション時間を増やすことになりますが、移動されるオブジェクトの数とoldジェネレーションの大きさが変わらなければ、これは、比較的小さくなります。

:Hotspotでは、マイナーコレクションはstop-the-worldイベントです。これは、ライブオブジェクトが増えてヒープが大きくなるにつれて、急激に大きな問題になります。そこで、私たちは、停止時間のターゲットを達成するために、youngジェネレーションのコンカレントコレクションの必要性を調べ始めています。

メジャーコレクション

メジャーコレクションがoldジェネレーションを収集すると、オブジェクトはyoungジェネレーションから移動します。大抵のアプリケーションでは、大部分のプログラムの状態は、oldジェネレーションで終わります。GCアルゴリズムの最大の多様性は、oldジェネレーションに存在します。いっぱいになったときにスペース全体を圧縮したり、いっぱいになるのを防ぐためにアプリケーションで同時に収集したりします。

oldジェネレーションのコレクタは、youngジェネレーションからの移動に失敗するのを避けるため、収集する必要がある時期を予想しようとします。コレクタはoldジェネレーションのいっぱいになる限界を追跡し、その限界を過ぎた時に、収集し始めます。この限界が移動の要件に十分合っていなければ、「FullGC」が始まります。FullGCはoldジェネレーションのコレクションとコンパクションに従い、youngジェネレーションからライブオブジェクトを移動させます。移動の失敗は、先に述べたように非常に高価な操作であり、このサイクルから移動したオブジェクトが解放されてから、FullGCイベントが発生します。

: 移動の失敗を避けるために、oldジェネレーションが移動に適応できるようにする (XX:PromotedPadding=) パディングを調節する必要があります。

:ヒープが成長する必要がある時に、FullGCが発生します。これらのヒープをリサイズするFullGCは、–Xms–Xmxに同じ値を設定することで避けられます。

FullGC以外、oldジェネレーションのコンパクションは、アプリケーションが経験する最も大きなstop-the-worldの停止でしょう。このコンパクションの時間は、tenuredスペースのライブオブジェクトの数で段階的に増える傾向があります。

survivorスペースのサイズと、tenuredジェネレーションに移動される前のオブジェクトの存続時間を増やすことで、tenuredスペースがいっぱいになる割合を減らせます。しかし、survivorスペースのサイズと、移動前のマイナーコレクション (–XX:MaxTenuringThreshold) のオブジェクトの存続時間が増えることで、 マイナーコレクションのsurvivorスペース間におけるコピーのコストが増えるため、マイナーコレクションのコストと停止時間も増えるでしょう。

シリアルコレクタ

シリアルコレクタ (-XX:+UseSerialGC) は、最もシンプルなコレクタであり、シングルプロセッサシステムでは良い選択肢です。どのコレクタでも最も小さなfootprintしか必要としません。シリアルコレクタは、マイナーコレクションとメジャーコレクションのシングルスレッドを利用します。オブジェクトは、ポインタアルゴリズムを使ったポインタの移動により、tenuredスペースに割り当てられます。メジャーコレクションは、tenuredスペースがいっぱいになると始まります。

パラレルコレクタ

パラレルコレクタには2つの形式があります。パラレルコレクタ (-XX:+UseParallelGC) は、マルチスレッドでYoungジェネレーションのマイナーコレクションを実行し、シングルスレッドでoldジェネレーションのメジャーコレクションを実行します。Java 7u4からデフォルトになったパラレルオールドコレクタ (-XX:+UseParallelOldGC) は、マイナーコレクションとメジャーコレクションにマルチスレッドを利用します。オブジェクトは、ポインタアルゴリズムを使ったポインタの移動によりtenuredスペースに割り当てられます。メジャーコレクションは、tenuredスペースがいっぱいになると始まります。

マルチプロセッサシステムにおいて、パラレルオールドコレクタはどのコレクタよりも大きなスループットを提供します。これは、コレクションが起きるまで実行アプリケーションには何も影響を与えず、最も効率的なアルゴリズムを使い、マルチスレッドのパラレルコレクションを実行します。このため、パラレルオールドコレクタはバッチアプリケーションに向いています。

oldジェネレーションを収集するコストは、ヒープサイズよりも大部分は保存されるオブジェクトの数に影響されます。そのため、より多くのメモリを提供し、コレクションの停止を大きいけれども数を少なくすることで、より大きなスループットを達成してパラレルオールドコレクタの効率を向上させられます。

tenuredスペースへの移動は、ポインタの移動とコピー操作なので、このコレクタを使って最速のマイナーコレクションが期待できます。

サーバアプリケーションでは、パラレルオールドコレクタは最初の行先であるべきです。しかし、メジャーコレクションの停止がアプリケーションで許容できないほど長い場合、アプリケーション実行中にtenuredオブジェクトを同時に収集するコンカレントコレクタの利用を検討する必要があります。

: oldジェネレーションの圧縮中に、現代のハードウェア上でライブデータのGB毎に1秒から5秒の停止が予想されます。

コンカレントマークスイープ (CMS) コレクタ

CMS (-XX:+UseConcMarkSweepGC) コレクタは、メジャーコレクション中には到達できないtenuredオブジェクトを収集するOldジェネレーションで実行されます。CMSはoldジェネレーションで十分なフリースペースを確保するためにアプリケーションで同時に実行します。 そうすることで、youngジェネレーションからの移動の失敗を防いでいます。

移動の失敗によりFullGCが発生します。CMSは以下の複数のプロセスに従います。

  1. 初期マーク: GCルートを見つける
  2. コンカレントマーク: GCルートから到達できるすべてのオブジェクトをマークする
  3. コンカレントプレクリーン: 更新されたオブジェクトへの参照や、リマークによってコンカレントマークフェーズ中に移動したオブジェクトを確認する
  4. リマーク <stop-the-world>: プレクリーンステージから更新されたオブジェクトの参照をとらえる
  5. コンカレントスイープ: デッドオブジェクトでいっぱいになったメモリを再利用してフリーリストを更新する
  6. コンカレントリセット: 次の実行のためにデータ構造をリセットする

tenuredオブジェクトに到達できなくなると、CMSによってスペースが再利用できるようになり、フリーリストに置かれます。移動が発生したら、移動されたオブジェクトに最適な大きさの部分をフリーリストから探さなければなりません。このため、移動のコストが上昇し、パラレルコレクタと比べて、マイナーコレクションのコストが増加します。

: CMSは圧縮するコレクタではないため、時間が経つと、old ジェネレーションにフラグメンテーションが発生します。オブジェクトの移動は、oldジェネレーションの空き領域に適合せずに、失敗することがあります。失敗した場合、「promotion failed」メッセージがログに書き込まれて、ライブtenuredオブジェクトを圧縮するために、FullGCが発生します。このようなコンパクション形式のFullGCでは、パラレルオールドコレクタを使うメジャーコレクションよりも停止時間が長くなることが予想されます。それは、CMSがコンパクションにシングルスレッドを利用するからです。

CMSは大抵アプリケーションと同時に実行されるため、様々な影響があります。まず、CPU時間がコレクタによって使われ、アプリケーションの利用可能なCPUが減ります。CMSが要求する時間の長さは、tenuredスペースへ移動するオブジェクトの数によって、段階的に長くなります。次に、コンカレントGCサイクルのいくつかのフェーズでは、GCルートをマークし、変更を確認するためにパラレルリマークを実行するSafepointへ、すべてのアプリケーションスレッドを置かなければなりません。

: アプリケーションで、tenuredオブジェクトに大幅な変更がある場合、リマークフェーズは大きくなります。極端な場合、パラレルオールドコレクタのフルコンパクションよりも時間がかかります。

CMSは、スループットの削減やコストのかかるminorコレクション、より大きなfootprintを犠牲にして、FullGCの発生を抑えます。スループットの削減は、パラレルコレクタと比べて、移動の割合により10%-40%になります。CMSには、追加のデータストラクチャと「フローティングガベージ」を合わせるために、20%大きなfootprintが必要です。「フローティングガベージ」は、コンカレントマーキング中に見逃され、次のサイクルに持ち越されることもあります。

高い移動レートやフラグメンテーションの発生は、youngジェネレーションとoldジェネレーションのスペースを大きくすることで減らせます。

: CMSでは、「コンカレントモードの失敗」が起きるかもしれません。移動を続けるために十分なレートでの収集に失敗するとログに書き込まれます。コレクションの開始が遅すぎる場合に発生しますが、これはチューニングで調節できます。しかし、コレクションレートが、移動レートやアプリケーションのオブジェクト変更レートに追いつかない場合に起きることもあります。移動レートやアプリケーションの変更レートが高すぎる場合、移動のプレッシャを減らすために、アプリケーションを変更する必要があるかもしれません。そのようなシステムにメモリを追加すると、CMSがより多くのメモリをスキャンしなければならなくなるため、状況が悪くなることがあります。

ガベージファースト(G1)コレクタ

G1 (-XX:+UseG1GC) は、Java 6で導入された新しいコレクタであり、今ではJava 7で正式にサポートされています。G1は、部分的にコンカレントコレクションのアルゴリズムを使い、フラグメンテーションでCMSを苦しめるFullGCイベントを最小化させるために、繰り返し発生するstop-the-worldの停止を短くして、tenuredスペースを圧縮しようとします。G1は、ジェネレーションコレクタであり、他のコレクタとは異なる方法で、ヒープを管理します。同じ目的で連続する領域を使うのではなく、様々な目的で同一サイズの領域に分割します。

G1は、領域間の参照を追跡し、最も空きスペースのある領域でコレクションに集中するために、同時マーキング領域のアプローチをとります。 これらの領域は、空き領域へライブオブジェクトを退避させることで増加するstop-the-worldの停止中に収集されます。こうして、プロセスの中で圧縮しています。領域の50%よりも大きいオブジェクトは、様々な大きさの領域を持つ、非常に大きな領域に割り当てられます。非常に大きなオブジェクトの割り当てとコレクションは、G1では非常にコストがかかり、今までほとんど最適化が適用されていません。

圧縮しているコレクタは、オブジェクトを動かすのではなく、オブジェクトへの参照を更新します。オブジェクトが多くの領域から参照されている場合、これらの参照を更新することは、オブジェクトを移動するよりも、ずっと時間がかかります。領域のどのオブジェクトが「Remembered Set」を通して他の領域から参照されているかを、G1は追跡します。Remembered Setが大きくなると、G1は非常に遅くなります。1つの領域から別の領域へオブジェクトを退避する時、関連するstop-the-worldイベントの長さは、スキャンして、部分的にパッチをあてる必要のある参照を持つ領域の数に比例する傾向があります。

Remembered Setを維持するには、パラレルオールドやマイナーコレクションのCMSコレクタで見られるよりも待ち時間が長くなるため、マイナーコレクションのコストが増加します。

G1は、停止時間のターゲット –XX:MaxGCPauseMillis=<n> に対して動き、デフォルトは200msです。 ターゲットは、ベストエフォートに基づいて各サイクルで実行される作業量に影響を与えます。10ミリセコンド単位でターゲットを設定するのは大抵無駄であり、10ミリセコンド単位のターゲットを書くことは、G1の中心ではありません。

G1が向いているのは、コンパクションが増加し、0.5-1.0セコンドの範囲の停止にアプリケーションが耐えられなくなると、フラグメンテーションが生じがちな大きなヒープの汎用的コレクタです。G1は、拡張されたマイナーコレクションとoldジェネレーションで増えていくコンパクションのために、CMSで見られる最悪な停止時間の発生を減少させます。ほとんどの停止時間は、フルヒープコンパクションよりも、領域で制限されます。

CMSのように、G1は移動レートに追いつけなくなって、stop-the-world FullGCに後退します。ちょうどCMSに「コンカレントモードの失敗」があるように、G1は退避失敗に陥り、「to-space overflow」とログに表示されます。これは、移動の失敗と同様、オブジェクトが退避できる空き領域がない場合に発生します。この失敗が発生した場合、より大きなヒープやより多くのマーキングスレッドを使用してください。しかし、割当レートを減らすには、アプリケーションの変更が必要になる場合もあります。

G1の難しい問題は、人気のあるオブジェクトや領域を扱うことです。他の領域からそれほど参照されないライブオブジェクトが領域にある場合、増加するstop-the-worldのコンパクションはうまく機能します。オブジェクトや領域の人気がある場合、Remembered Setは大きくなり、G1はこれらのオブジェクトを収集するのを避けようとします。結局、選択肢はなく、ヒープが圧縮されるにつれて、非常に頻繁に中程度の停止が発生します。

もう1つのコンカレントコレクタ

CMSとG1は、大抵コンカレントコレクタと呼ばれます。実行される作業をすべて見れば、youngジェネレーション、移動、そして、oldジェネレーションの大部分の作業でさえ、まったく同時には行われていません。 CMSは、大抵、oldジェネレーションでは同時に実行されます。G1は、stop-the-worldで増加していくコレクタです。CMSとG1は、定期的に発生する大きなstop-the-worldイベントがあり、最悪のシナリオでは、金融取引や反応型ユーザインタフェースのような、厳密な低遅延アプリケーションには適合しない場合があります。

代替するコレクタとして、Oracle JRockit Real TimeやIBM Websphere Real Time、Azul Zingなどを利用できます。JRockitやWebsphereコレクタは、CMSやG1よりも、大抵の場合、停止時間に関して有利ですが、しばしばスループットの制限が見られ、大きなstop-the-worldイベントが発生します。Zingは、Javaコレクタであり、全てのジェネレーションのスループットレートを高く保ちながら、コレクションとコンパクションが本当に同時に実行できます。Zingは、数ミリセコンドのstop-the-worldイベントが発生しますが、ライブセットのサイズとは関係なく、コレクションサイクルのフェーズが変わることが原因です。

JRockit RTにより、ヒープサイズを含んだ高い割り当てレートが、数10ミリセコンドという典型的な停止時間で達成できますが、時には、フルコンパクションの停止になってしまうこともあります。Websphere RTは、強制的な割り当てレートとライブセットのサイズにより、1桁のミリセコンドという停止時間を達成できます。Zingは、マイナーコレクションの間を含むすべての停止を同時に実行することで、高い割り当てレートで数ミリセコンドの停止を実現しています。Zingは、ヒープサイズに関わらず、同様のふるまいを維持できます。そのため、ユーザは、停止時間が増えることを心配せずに、アプリケーションのスループットやオブジェクトモデルの状態の必要性に合わせて、必要であれば大きなヒープサイズを適用できます。

停止時間をターゲットにするすべてのコンカレントコレクタでは、ある程度のスループットをあきらめなければならず、footprintが増えることになります。コンカレントコレクタの効率により、あきらめるスループットは少しかもしれませんが、常に大きなfootprintが追加されます。もし、ほんのわずかなstop-the-worldイベントがあるだけで本当に同時に実行されるのであれば、同時操作を可能にして、スループットを維持するために、より多くのCPUコアが必要です。

注: すべてのコンカレントコレクタは、大きなスペースが割り当てられている時に、より効率的に機能する傾向があります。最初の経験則から、効率的な操作のためには、少なくともライブセットのサイズの2倍から3倍のヒープを確保すべきです。しかし、コンカレント操作を維持するためのスペースの要求は、アプリケーションスループットと、関連する割り当てレートや移動レートにより増えていきます。そのため、スループットの高いアプリケーションには、ライブセットの割合に対して、大きなヒープサイズが保証になります。今日のシステムのfootprintに非常に大きなメモリスペースを割り当てることが、サーバサイドで問題になることはめったにありません。

ガベージコレクションのモニタリングとチューニング

アプリケーションとガベージコレクタの動きを理解するには、少なくとも以下の設定でJVMを起動します。

-verbose:gc
-Xloggc:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime

それから、分析のためにChewiebugのようなツールでログを読み込みます。

GCが動的に動く様子を見るには、JVisualVMを起動し、Visual GCプラグインをインストールします。そうすれば、以下のようにアプリケーションで動いているGCを見られます。

アプリケーションのGCの必要性を理解するには、繰り返し実行できる典型的なロードテストが必要です。各コレクタがどのように動くかを理解し始めたら、ターゲットとするスループットと停止時間に到達するまで、実験として、様々な設定でロードテストを走らせます。この時に重要なのは、エンドユーザの視点で停止時間を測ることです。そうするために、ヒストグラムの各テスト要求の応答時間を把握すると良いでしょう。詳細は、こちらで読むことができます。停止時間が許容範囲を超えている場合は、GCが原因かどうか見極めるためにGCのログと対応させてみましょう。別の原因で停止時間が増加している可能性もあります。もう1つの役に立つツールは、jHiccupであり、JVM内やシステム全体の停止を追跡するのに利用できます。

停止時間の増加の原因がGCにある場合は、停止時間のターゲットに合うかどうかみるために、CMSか、G1をチューニングします。高い割り当てレートや移動レートが、非常に低い遅延要求と組み合わされているためにチューニングできない時があるかもしれません。GCのチューニングには、しばしばオブジェクトの割り当てレートやオブジェクトライフタイムを減らすためにアプリケーションの修正が必要になり、高いスキルが必要とされることがあります。この場合には、GCのチューニングやアプリケーションの修正に時間と費用をかけるか、JRockit Real TimeやAzul Zingなどの商用の同時に圧縮するJVMの1つを購入するかのトレードオフになるでしょう。

著者について

Martin Thompson氏は、ハイパフォーマンスと低遅延の専門家であり、大規模トランザクションとビックデータシステムに20年以上取り組んできた経験を持っています。Martin氏は、上質でハイパフォーマンスなソリューションを提供するために、基礎としてハードウェアの理解をソフトウェア製作に適用するようなMechanical Sympathy (機械的共感) を信じています。Disruptorフレームワークは、Mechanical Sympathyが作り出した1つの例にすぎません。 Martin氏は、LMAXの共同創立者でCTOです。ブログはこちらで、パフォーマンスやコンカレンシやシステムをより良くするためのコードハッキングのトレーニングコースを見つけられます。

この記事に星をつける

おすすめ度
スタイル

BT