これらの高度な GC 技術でアプリケーション パフォーマンスを向上させる

アプリケーション パフォーマンスは私たちの頭の中の最重要事項であり、ガーベッジ コレクション最適化は小さいながらも意味のある前進をする良い場所です

自動ガーベッジ コレクション (JIT HotSpot Compiler) は、JVM の最も高度で最も価値のあるコンポーネントの 1 つですが、多くの開発者やエンジニアはガーベッジ コレクション (GC) やそれがどう動作しアプリケーション パフォーマンスにどう影響するかにあまり慣れていないのが現実です。

まず、GC とは何のためにあるのでしょうか。 ガベージ コレクションは、ヒープ内のオブジェクトのメモリ管理プロセスです。 オブジェクトがヒープに割り当てられると、いくつかの収集フェーズを実行します。ヒープ内のオブジェクトの大部分は寿命が短いので、通常はかなり速く実行されます。

ガーベッジ コレクション イベントには、マーキング、削除、コピー/コンパクションという 3 つのフェーズがあります。 最初のフェーズでは、GC はヒープを実行し、ライブ (参照) オブジェクト、未参照オブジェクト、または利用可能なメモリ領域として、すべてをマークします。 参照されないオブジェクトは削除され、残りのオブジェクトは圧縮されます。 世代別ガベージコレクションでは、オブジェクトは「老化」し、エデン、サバイバー空間、テニュア(古い)空間という3つの空間を経て昇格していきます。 このシフトも圧縮フェーズの一部として行われます。

でも、それはもう十分です、楽しい部分に行きましょう!

Psst! アプリケーション パフォーマンスを改善するソリューションをお探しですか?OverOps は、スローダウンがいつ、どこで発生するかだけではなく、なぜ、どうやって発生するかを企業が特定できるようにします。 8191>

Getting to Know Garbage Collection (GC) in Java

自動 GC の素晴らしい点の 1 つは、開発者がその仕組みを本当に理解する必要がないことです。 残念ながら、それは、多くの開発者が GC がどのように機能するかを理解していないことを意味します。 ガベージ コレクションと多くの利用可能な GC を理解することは、Linux CLI コマンドを知ることと多少似ています。 CLI コマンドと同様に、絶対的な基本があります。親フォルダー内のフォルダー リストを表示する ls コマンド、ある場所から別の場所にファイルを移動する mv などです。 GCでは、これらの種類のコマンドは、選択する複数のGCがあり、GCはパフォーマンス上の懸念を引き起こすことができることを知っていることと同等であるでしょう。 もちろん、(Linux CLI の使用とガベージ コレクションについて)学ぶべきことはもっとたくさんあります。

Javaのガベージ コレクション プロセスについて学ぶ目的は、単にありがたい(そして退屈な)会話のきっかけとしてではなく、特定の環境に対して最適なパフォーマンスを持つ正しいGCを効果的に実装および維持する方法を学ぶためです。 ガベージ コレクションがアプリケーションのパフォーマンスに影響することを知ることは基本であり、GC パフォーマンスを強化し、アプリケーションの信頼性への影響を低減するための多くの高度なテクニックがあります。

GC パフォーマンスに関する懸念事項

Memory Leaks –

ヒープ構造とガベージ コレクションがどのように実行されるかに関する知識により、ガベージ コレクション イベントが発生して使用率が下がるまでメモリ使用が徐々に増加するということがわかっています。 メモリ リークでは、各 GC イベントはヒープ オブジェクトのより小さい部分をクリアするので (残された多くのオブジェクトは使用されていませんが)、ヒープ メモリが一杯になり OutOfMemoryError 例外がスローされるまでヒープ使用量は増加しつづけます。 この原因は、GCが参照されないオブジェクトだけを削除の対象とすることにあります。 つまり、参照されているオブジェクトが使われなくなったとしても、ヒープから消去されることはないのです。

Continuous “Stop the World” Events –

あるシナリオでは、ガベージ コレクションは Stop the World イベントと呼ばれることがあり、それはそれが発生すると、JVM (従って、その上で実行しているアプリケーション) のすべてのスレッドが停止して GC が実行できるようにするからです。 健全なアプリケーションでは、GC 実行時間は比較的低く、アプリケーション パフォーマンスに大きな影響を与えません。

しかしながら、最適でない状況では、Stop the World イベントがアプリケーションのパフォーマンスと信頼性に大きな影響を与える可能性があります。 GC イベントが Stop the World の一時停止を必要とし、実行に 2 秒かかる場合、アプリケーションを実行しているスレッドが GC を許可するために停止されるので、そのアプリケーションのエンドユーザーは 2 秒間の遅延を経験することになります。 GC の実行ごとにパージされるヒープ メモリ領域が少なくなると、残りのメモリが一杯になるまでの時間が短くなります。 メモリが満杯になると、JVMは別のGCイベントをトリガーします。 最終的に、JVM は大きなパフォーマンスの懸念を引き起こすストップ・ザ・ワールド イベントを繰り返し実行することになります。

CPU Usage –

そして、それはすべて CPU 使用率に帰着します。 連続した GC / Stop the World イベントの主な症状は、CPU 使用率の急増です。 GC は計算上重い操作であり、したがって、CPU パワーの公正なシェアよりも多くを取ることができます。 同時スレッドを実行する GC の場合、CPU 使用率はさらに高くなる可能性があります。 アプリケーションに適した GC を選択することが CPU 使用率に最大の影響を与えますが、この領域でより良いパフォーマンスのために最適化する他の方法もあります。

GC がどんなに高度になっても (そしてかなり高度になっています)、その致命的な弱点は変わらないことを、ガベージ コレクションを取り巻くこれらのパフォーマンスの懸念から理解することができます。 冗長で予測不可能なオブジェクトの割り当て。 アプリケーションのパフォーマンスを向上させるには、正しいGCを選択するだけでは十分ではありません。 プロセスがどのように動作するかを知り、GC が過剰なリソースを引き出したり、アプリケーションに過剰な休止を引き起こしたりしないように、コードを最適化する必要があります。

世代別 GC

さまざまな Java GC とそのパフォーマンスへの影響に飛び込む前に、世代別ガベージ コレクションの基本を理解することが重要です。 世代別 GC の基本概念は、ヒープ内のオブジェクトへの参照が長く存在すればするほど、削除の対象になる可能性が低くなるという考えに基づいています。 オブジェクトに比喩的な「年齢」のタグを付けることにより、オブジェクトを異なるストレージ スペースに分離して、GC によってマークされる頻度を少なくすることができます。 そこはオブジェクトの出発点であり、ほとんどの場合、そこは削除のためにマークされる場所です。 この段階で生き残ったオブジェクトは「誕生日を祝う」ことになり、Survivorスペースにコピーされる。 このプロセスを以下に示します。

EdenスペースとSurvivorスペースは、いわゆるヤングジェネレーションを構成しています。 ここがアクションの大部分を占める。 ヤングジェネレーションのオブジェクトがある年齢に達すると(If)、テニュアード(オールドとも呼ばれる)スペースに昇格する。 8191>

Minor GCは、若い世代にのみ焦点を当て、事実上Tenured空間を完全に無視するコレクションです。 一般に、若い世代のオブジェクトの大部分は削除のためにマークされ、メジャーまたはフルGC(古い世代を含む)は、ヒープ上のメモリを解放するために必要ではありません。

これに基づき GC 操作を最適化するための 1 つのクイック トリックは、アプリケーションのニーズに最も合うようにヒープ領域のサイズを調整することです。

Collector Types

G1 は Java 9 でデフォルトの GC になりましたが、もともと低ポーズ CMS コレクターを交換するためのもので、したがって Throughput コレクターで実行中のアプリケーションは現在のコレクターで維持するほうが適している場合があります。 Java ガーベッジ コレクターの運用上の違い、およびパフォーマンスへの影響の違いを理解することは、依然として重要です。

Throughput Collectors

高スループットに最適化する必要があり、それを達成するために高いレイテンシと交換できるアプリケーションに最適です。

シリアル –

シリアルコレクターは最もシンプルなもので、主にシングルスレッド環境 (32 ビットや Windows) と小さなヒープ用に設計されているため、使用する可能性は最も低いものです。 このコレクターは、JVMのメモリ使用量を垂直にスケールすることができますが、未使用のヒープリソースを解放するために、いくつかのメジャー/フルGCを必要とします。 8191>

Parallel –

その名前が示すように、この GC はヒープを走査して圧縮するために並行して実行される複数のスレッドを使用します。 Parallel GC はガベージ コレクションに複数のスレッドを使用しますが、実行中にすべてのアプリケーション スレッドを一時停止します。 8191>

Low Pause Collectors

Most User-facing applications will require a low pause GC, so that user experience is not affected by long or frequent pauses. これらの GC は、応答性 (時間/イベント) と強力な短期パフォーマンスを最適化します。

Concurrent Mark Sweep (CMS) –

Parallel コレクターと同様に、CMS (Concurrent Mark Sweep) コレクターは参照されないオブジェクトにマークを付けて掃除 (remov) するために複数のスレッドを使用します。 しかし、この GC は、2 つの特定のインスタンスでのみ、Stop the World イベントを開始します。

(1) ルート (スレッドのエントリ ポイントまたは静的変数から到達可能な古い世代のオブジェクト) または main() メソッドからの任意の参照の初期化時、およびいくつかのより

(2) アプリケーションがアルゴリズムを同時に実行していた間にヒープの状態を変更した場合。 にマークされた正しいオブジェクトがあることを確認するために、戻って最終的な処理を行うことを余儀なくされる

G1 –

Garbage first collector (一般に G1 として知られている) は複数の背景スレッドを使用して、領域に分割されたヒープを走査します。 8191>

この戦略により、バックグラウンド スレッドが未使用オブジェクトのスキャンを終了する前にヒープが枯渇する可能性を減らすことができます。 G1 コレクターのもう 1 つの利点は、CMS コレクターが完全な Stop the World コレクションの間だけ行う、移動中にヒープを圧縮することです。

GC パフォーマンスの向上

アプリケーション パフォーマンスは、ガベージ コレクションの頻度と期間によって直接影響を受け、GC プロセスの最適化はこれらの指標を減らすことによって行われることを意味します。 これを行うには、2 つの主要な方法があります。 1 つは、若い世代と古い世代のヒープ サイズを調整することで、2 つ目は、オブジェクトの割り当てと昇格の割合を減らすことです。

ヒープ サイズの調整という点では、期待するほど簡単なことではありません。 論理的な結論は、ヒープ サイズを増加させると、期間を増加させながら GC の頻度を減少させ、ヒープ サイズを減少させると、頻度を増加させながら GC の期間を減少させることです。

しかし、問題の事実は、マイナー GC の期間はヒープ サイズではなく、コレクションを生き残るオブジェクトの数に依存していることです。 つまり、ほとんどが短命のオブジェクトを作成するアプリケーションでは、若い世代のサイズを増やすと、実際に GC の期間と頻度の両方を減らすことができます。 しかし、若い世代のサイズを増やすと、生存者スペースでコピーする必要があるオブジェクトが大幅に増加する場合、GCの一時停止が長くなり、待ち時間が増加します。

GC-Efficient Code を書くための 3 つのヒント

ヒント 1: コレクション容量を予測する –

すべての標準 Java コレクションと、ほとんどのカスタムおよび拡張実装 (Trove や Google の Guava など) は、基本的に配列 (原始ベースまたはオブジェクト ベース) を使用します。 配列は一度割り当てられるとサイズが不変なので、コレクションにアイテムを追加すると、多くの場合、より大きな新しく割り当てられた配列のために古い基礎となる配列が削除される可能性があります。

ほとんどのコレクション実装は、コレクションの予想サイズが提供されていなくても、この再割り当てプロセスを最適化して償却済みの最小値に維持しようとします。 しかし、構築時に期待されるサイズをコレクションに提供することにより、最良の結果を得ることができます。

Tip #2: ストリームを直接処理する –

たとえば、ファイルから読み込んだデータやネットワーク経由でダウンロードしたデータなど、データのストリームを処理すると、次の行の何かを見ることは非常によくあります。

これにアプローチする良い方法は、適切な InputStream (この場合は FileInputStream) を使用して、最初に全体をバイト配列に読み込まずに直接パーサーに送り込むことです。 すべての主要なライブラリは、ストリームを直接パースする API を公開しています (例:

)。 8191>

不変オブジェクトは、オブジェクトが構築された後にそのフィールド (私たちの場合は特に非プリミティブ フィールド) が変更されないオブジェクトです。 GC用語では コンテナは、それが保持する最も若い参照と少なくとも同じだけ若いです。 これは、若い世代のガベージ コレクション サイクルを実行するとき、GC は、収集されている世代で何も参照できないことを確実に知っているので、古い世代にある不変のオブジェクトをスキップできることを意味します。

さらなるヒントと詳細な例については、よりメモリ効率の高いコードを書くための詳細な戦術を網羅したこの投稿をご覧ください。

*** OverOps の R&D チームの Amit Hurvitz のこの投稿に対する情熱と考察に大いに感謝します!

コメントする