JVMパフォーマンスの最適化、パート3:ガベージコレクション

Javaプラットフォームのガベージコレクションメカニズムは開発者の生産性を大幅に向上させますが、実装が不十分なガベージコレクターはアプリケーションリソースを過剰に消費する可能性があります。JVMパフォーマンス最適化シリーズのこの3番目の記事では、Eva AndreassonがJava初心者に、JavaプラットフォームのメモリモデルとGCメカニズムの概要を説明します。次に、(GCではなく)フラグメンテーションが主要な「落とし穴」である理由を説明します。Javaアプリケーションのパフォーマンスの概要、および世代別のガベージコレクションと圧縮が現在Javaアプリケーションのヒープフラグメンテーションを管理するための主要な(最も革新的ではありませんが)アプローチである理由。

ガベージコレクション(GC)は、到達可能なJavaオブジェクトによって参照されなくなった占有メモリを解放することを目的としたプロセスであり、Java仮想マシン(JVM)の動的メモリ管理システムの重要な部分です。通常のガベージコレクションサイクルでは、まだ参照されているため到達可能なすべてのオブジェクトが保持されます。以前に参照されたオブジェクトが占めていたスペースは解放され、再利用されて、新しいオブジェクトの割り当てが可能になります。

ガベージコレクションとさまざまなGCのアプローチとアルゴリズムを理解するには、まずJavaプラットフォームのメモリモデルについていくつか知っておく必要があります。

JVMパフォーマンスの最適化:シリーズを読む

  • パート1:概要
  • パート2:コンパイラ
  • パート3:ガベージコレクション
  • パート4:GCの同時圧縮
  • パート5:スケーラビリティ

ガベージコレクションとJavaプラットフォームのメモリモデル

-XmxJavaアプリケーションのコマンドラインで起動オプションを指定すると(例:)、java -Xmx:2g MyAppメモリがJavaプロセスに割り当てられます。このメモリは、Javaヒープ(または単にヒープ)と呼ばれます。これは、Javaプログラム(または場合によってはJVM)によって作成されたすべてのオブジェクトが割り当てられる専用のメモリアドレス空間です。 Javaプログラムが実行され続け、新しいオブジェクトが割り当てられると、Javaヒープ(アドレス空間を意味します)がいっぱいになります。

最終的に、Javaヒープはいっぱいになります。つまり、割り当てスレッドは、割り当てたいオブジェクトの空きメモリの十分な大きさの連続したセクションを見つけることができません。その時点で、JVMはガベージコレクションを実行する必要があると判断し、ガベージコレクタに通知します。ガベージコレクションは、Javaプログラムがを呼び出したときにもトリガーできますSystem.gc()。使用するSystem.gc()ガベージコレクションを保証するものではありません。ガベージコレクションを開始する前に、GCメカニズムは最初にガベージコレクションを安全に開始できるかどうかを判断します。アプリケーションのアクティブなスレッドがすべて安全な場所にあるときにガベージコレクションを開始しても安全です。たとえば、進行中のオブジェクト割り当ての途中または途中でガベージコレクションを開始するのは悪いことだと簡単に説明しました。最適化されたCPU命令のシーケンスを実行すると(コンパイラに関する以前の記事を参照)、コンテキストが失われ、最終結果が台無しになる可能性があります。

ガベージコレクターは、アクティブに参照されているオブジェクトを再利用しないでください。これを行うと、Java仮想マシンの仕様が破損します。ガベージコレクターは、死んだオブジェクトをすぐに収集する必要もありません。デッドオブジェクトは、その後のガベージコレクションサイクル中に最終的に収集されます。ガベージコレクションを実装する方法はたくさんありますが、これら2つの仮定はすべての種類に当てはまります。ガベージコレクションの本当の課題は、ライブ(まだ参照されている)のすべてを識別し、参照されていないメモリを再利用することですが、実行中のアプリケーションに必要以上の影響を与えることなく実行します。したがって、ガベージコレクタには2つの義務があります。

  1. アプリケーションの割り当て率を満たし、メモリが不足しないようにするために、参照されていないメモリをすばやく解放します。
  2. 実行中のアプリケーションのパフォーマンス(遅延やスループットなど)への影響を最小限に抑えながら、メモリを再利用するため。

2種類のガベージコレクション

このシリーズの最初の記事では、ガベージコレクションへの2つの主要なアプローチ、つまり参照カウントとトレースコレクターについて触れました。今回は、各アプローチをさらに掘り下げてから、実稼働環境でトレースコレクターを実装するために使用されるアルゴリズムのいくつかを紹介します。

JVMパフォーマンス最適化シリーズを読む

  • JVMパフォーマンスの最適化、パート1:概要
  • JVMパフォーマンスの最適化、パート2:コンパイラ

参照カウントコレクター

参照カウントコレクターは、各Javaオブジェクトを指している参照の数を追跡します。オブジェクトのカウントがゼロになると、メモリをすぐに再利用できます。再利用されたメモリへのこの即時アクセスは、ガベージコレクションへの参照カウントアプローチの主な利点です。参照されていないメモリを保持することになると、オーバーヘッドはほとんどありません。ただし、すべての参照カウントを最新の状態に保つには、かなりのコストがかかる可能性があります。

参照カウントコレクターの主な問題は、参照カウントを正確に保つことです。もう1つのよく知られている課題は、円形構造の処理に関連する複雑さです。2つのオブジェクトが相互に参照し、ライブオブジェクトがそれらを参照していない場合、それらのメモリは解放されません。両方のオブジェクトは、ゼロ以外のカウントで永久に残ります。循環構造に関連付けられたメモリを再利用するには、主要な分析が必要です。これにより、アルゴリズム、ひいてはアプリケーションにコストのかかるオーバーヘッドが発生します。

コレクターのトレース

コレクターのトレースは、ライブオブジェクトであることがわかっている初期セットからのすべての参照と後続の参照を繰り返しトレースすることにより、すべてのライブオブジェクトを見つけることができるという前提に基づいています。 (と呼ばれるライブオブジェクトの初期設定ルートオブジェクトまたはちょうどの短いためには)ガベージコレクションがトリガされた瞬間に、レジスタ、グローバルフィールド、およびスタックフレームを解析することにより、位置しています。最初のライブセットが識別された後、トレースコレクターはこれらのオブジェクトからの参照を追跡し、それらをキューに入れてライブとしてマークし、その後、参照をトレースします。見つかったすべての参照オブジェクトをライブとしてマークする既知のライブセットが時間の経過とともに増加することを意味します。このプロセスは、参照されているすべての(したがってすべてのライブ)オブジェクトが見つかり、マークが付けられるまで続きます。トレースコレクターがすべてのライブオブジェクトを検出すると、残りのメモリを再利用します。

トレースコレクターは、円形構造を処理できるという点で参照カウントコレクターとは異なります。ほとんどのトレースコレクターの問題はマーキングフェーズです。これは、参照されていないメモリを再利用できるようになるまで待機する必要があります。

トレースコレクターは、動的言語でのメモリ管理に最も一般的に使用されます。これらはJava言語で最も一般的であり、本番環境で長年にわたって商業的に証明されています。この記事の残りの部分では、ガベージコレクションへのこのアプローチを実装するいくつかのアルゴリズムから始めて、コレクターのトレースに焦点を当てます。

コレクターアルゴリズムのトレース

コピーマークアンドスイープのガベージコレクションは新しいものではありませんが、今日でもトレースガベージコレクションを実装する最も一般的な2つのアルゴリズムです。

コレクターのコピー

従来のコピーコレクターは、from-spaceto-space、つまり、ヒープの2つの別々に定義されたアドレス空間を使用します。ガベージコレクションの時点で、from-spaceとして定義されたエリア内のライブオブジェクトは、to-spaceとして定義されたエリア内の次に使用可能なスペースにコピーされます。from-space内のすべてのライブオブジェクトが移動されると、from-space全体を再利用できます。割り当てが再開されると、to-spaceの最初の空き場所から開始されます。

このアルゴリズムの古い実装では、from-spaceとto-spaceが切り替わります。つまり、図1に示すように、to-spaceがいっぱいになると、ガベージコレクションが再度トリガーされ、to-spaceがfrom-spaceになります。

コピーアルゴリズムの最新の実装では、ヒープ内の任意のアドレススペースをto-spaceおよびfrom-spaceとして割り当てることができます。これらの場合、必ずしもお互いに場所を切り替える必要はありません。むしろ、それぞれがヒープ内の別のアドレス空間になります。

コレクターをコピーする利点の1つは、オブジェクトがtoスペースに緊密に割り当てられ、断片化が完全に排除されることです。断片化は、他のガベージコレクションアルゴリズムが苦労している一般的な問題です。この記事の後半で説明します。

コレクターのコピーの欠点

コピーコレクターは通常、ストップザワールドコレクターです。つまり、ガベージコレクションが循環している限り、アプリケーションの作業を実行することはできません。ストップザワールドの実装では、コピーする必要のある領域が大きいほど、アプリケーションのパフォーマンスへの影響が大きくなります。これは、応答時間に敏感なアプリケーションにとっては不利です。コピーコレクターでは、すべてがfrom-spaceに存在する場合の、最悪のシナリオも考慮する必要があります。ライブオブジェクトを移動するのに十分なヘッドルームを常に残しておく必要があります。つまり、toスペースは、fromスペース内のすべてをホストするのに十分な大きさである必要があります。この制約のため、コピーアルゴリズムはメモリ効率がわずかに低くなります。

マークアンドスイープコレクター

エンタープライズ実稼働環境にデプロイされたほとんどの商用JVMは、マークアンドスイープ(またはマーキング)コレクターを実行します。これは、コピーコレクターのようなパフォーマンスへの影響はありません。最も有名なマーキングコレクターには、CMS、G1、GenPar、およびDeterministicGCがあります(「参考文献」を参照)。

マークアンドスイープコレクタトレース参照とマーク「ライブ」ビットが見つかった各オブジェクト。通常、セットビットは、ヒープ上のアドレスまたは場合によってはアドレスのセットに対応します。ライブビットは、たとえば、オブジェクトヘッダー、ビットベクトル、またはビットマップにビットとして格納できます。

すべてがライブとしてマークされた後、スイープフェーズが開始されます。コレクターにスイープフェーズがある場合、基本的に、ヒープを再度トラバースして(ライブセットだけでなく、ヒープ全体の長さ)、マークされていないすべての場所を見つけるためのメカニズムが含まれます。連続するメモリアドレス空間のチャンク。マークされていないメモリは無料で再利用可能です。次に、コレクターは、これらのマークされていないチャンクを整理された無料リストにリンクします。ガベージコレクタにはさまざまな空きリストがあります。通常はチャンクサイズで整理されています。一部のJVM(JRockit Real Timeなど)は、アプリケーションプロファイリングデータとオブジェクトサイズ統計に基づいてリストのサイズ範囲を動的に変更するヒューリスティックを備えたコレクターを実装します。

スイープフェーズが完了すると、割り当てが再開されます。新しい割り当て領域は空きリストから割り当てられ、メモリチャンクは、オブジェクトサイズ、スレッドIDごとのオブジェクトサイズの平均、またはアプリケーションで調整されたTLABサイズに一致させることができます。空き領域をアプリケーションが割り当てようとしているサイズにさらに近づけると、メモリが最適化され、断片化を減らすことができます。

TLABサイズの詳細

TLABおよびTLA(スレッドローカル割り当てバッファーまたはスレッドローカルエリア)のパーティショニングについては、JVMパフォーマンスの最適化パート1で説明しています。

マークアンドスイープコレクターの欠点

マークフェーズはヒープ上のライブデータの量に依存し、スイープフェーズはヒープサイズに依存します。メモリを再利用するには、マークフェーズとスイープフェーズの両方が完了するまで待機する必要があるため、このアルゴリズムは、より大きなヒープとより大きなライブデータセットに対して一時停止時間の課題を引き起こします。

メモリを大量に消費するアプリケーションを支援する1つの方法は、さまざまなアプリケーションのシナリオとニーズに対応するGCチューニングオプションを使用することです。多くの場合、チューニングは、これらのフェーズのいずれかがアプリケーションまたはサービスレベル契約(SLA)のリスクになるのを少なくとも延期するのに役立ちます。(SLAは、アプリケーションが特定のアプリケーション応答時間、つまり遅延を満たすことを指定します。)ただし、調整は特定のワークロードと割り当て率に対してのみ有効であるため、すべての負荷の変更とアプリケーションの変更の調整は繰り返し作業です。

マークアンドスイープの実装

マークアンドスイープ収集を実装するための、少なくとも2つの市販の実証済みのアプローチがあります。1つは並列アプローチで、もう1つは同時(またはほとんど同時)アプローチです。

並列コレクター

並列収集とは、プロセスに割り当てられたリソースがガベージコレクションの目的で並列に使用されることを意味します。商業的に実装されているほとんどの並列コレクターは、モノリシックなストップザワールドコレクターです。すべてのアプリケーションスレッドは、ガベージコレクションサイクル全体が完了するまで停止されます。すべてのスレッドを停止すると、すべてのリソースを並行して効率的に使用して、マークフェーズとスイープフェーズでガベージコレクションを完了することができます。これにより、非常に高いレベルの効率が得られ、通常、SPECjbbなどのスループットベンチマークで高いスコアが得られます。アプリケーションにスループットが不可欠な場合は、並列アプローチが最適です。