JavaProgrammingSenior Java Developer

**G1**は、ルーチンのガーベジコレクションサイクル中に、ストップ・ザ・ワールドの期間を延ばすことなく、重複する**String**のバックアレイを透過的に統合するための特定の最適化は何ですか?

Hintsage AIアシスタントで面接を突破

質問への回答。

質問の歴史

Java 8アップデート20の以前、開発者は重複するStringインスタンスからヒープの消費量を減らすために、String.intern()に完全に依存していました。このメソッドは文字列をパーマネント世代(後にMetaspace)に配置し、明示的なAPI呼び出しを必要とし、インターンプールでのメモリ圧迫を引き起こす可能性がありました。JEP 192によって、G1ガーベジコレクタは自動String重複排除を導入し、エンタープライズアプリケーションにおける冗長な文字配列の一般的な問題を対象とした透過的な最適化を行いました。

問題

データ集約型のJavaアプリケーション(XML、JSON、またはデータベースの結果セットを解析するアプリケーションなど)では、Stringオブジェクトが生存ヒープの25〜50%を占めることがよくあります。これらの文字列のかなりの部分が文字単位で同一ですが、異なるchar[](またはJava 9以降のCompact Stringsではbyte[])のバックアレイに存在します。介入がない場合、これらの重複した配列はメモリを無駄にし、GCの頻度を増加させます。この冗長性を排除する課題は、追加のストップ・ザ・ワールドの一時停止を導入したり、コードの変更を要求したりすることなく行うことでした。

解決策

G1は、既存の避難一時停止中に機会を捉えて重複排除を実施します(スレッドがすでに停止しているとき)。-XX:+UseStringDeduplicationで有効にすると、コレクタは若い世代のオブジェクトをスキャンします。少なくとも**-XX:StringDeduplicationAgeThreshold**(デフォルトは3)回のガーベジコレクションを生き延びた各Stringに対して、G1はそのバックアレイのハッシュを計算します。その後、重複排除テーブルを参照します。同一の配列が存在する場合、G1はcompare-and-swap (CAS)操作を使用して、Stringvalueフィールドを既存の配列にリダイレクトし、次のサイクルで重複を回収できるようにします。これにより、既存の一時停止を活用し、追加のCPUオーバーヘッドをほとんど加えずに済みます。

// コード変更は不要;JVMフラグで最適化を有効にします: // -XX:+UseG1GC -XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3 public class DeduplicationExample { public static void main(String[] args) { // これらの2つの文字列は重複排除後、同じバックアレイを共有します String a = new String("FinancialInstrument".toCharArray()); String b = new String("FinancialInstrument".toCharArray()); // 十分なGCサイクルと避難の一時停止の後、 // a.value == b.value(内部配列の参照の等しさ) } }

実生活の状況

FIXプロトコルメッセージを処理する高頻度取引プラットフォームは、G1の一時停止時間が200ミリ秒を超える深刻な問題を経験しました。プロファイリングの結果、64GBヒープの30%が標準タグ(例えば、"55", "150", "EUR/USD")および受信バイトストリームから解析された列挙型の値を表すStringオブジェクトによって消費されていることが明らかになりました。各メッセージのインスタンス化は、new String(byte[], Charset)を介して新しいStringインスタンスを作成し、結果として数百万の重複バックアレイが1分間に生成されました。

いくつかのソリューションが評価されました。String.intern()は、50種類以上のメッセージタイプにわたる侵襲的な変更を必要とし、決してガーベジコレクトされない永続的参照でMetaspaceを飽和させるリスクがあるため却下されました。カスタムのWeakHashMapベースのキャッシュがプロトタイプ化されましたが、複雑な同時実行オーバーヘッドと古いエントリのクリーンアップロジックを導入し、逆に追加のWeakReference処理によってGCの圧力が増加しました。

最終的にチームは、デフォルトの閾値3でG1String重複排除を有効にしました。この透過的なアプローチでは、コードの変更は一切必要なく、既存の避難の一時停止中に動作し、新たなストップ・ザ・ワールドのフェーズを回避しました。

その結果、ヒープ使用量は22%削減され、95パーセンタイルの一時停止時間は50ミリ秒未満に低下しました。ピークの市場時間中のCPUオーバーヘッドは約1.5%と測定され、メモリの節約と遅延の改善に対する許容できるトレードオフでした。

候補者がよく見逃すこと

Stringの重複排除は、ラテン1テキストをbyte[]ではなくchar[]として格納するJava 9のCompact Stringsとどのように相互作用しますか?

回答。 String重複排除は、Compact Stringsが有効になっているとき(Java 9以降のデフォルト)にbyte[]配列で動作するように更新されました。重複排除ロジックはcoderフィールド(LATIN1またはUTF16)を検査し、対応するbyte[]またはchar[]のバックアレイを適切にハッシュします。重複排除テーブルは、ハッシュと配列のタイプの両方でキー付けされたエントリを保存し、ラテン1文字列が他のラテン1文字列に対して重複排除され、全角UTF-16文字列がその仲間に対して重複排除されることを保証します。候補者はこの機能がCompact Stringsによって廃止されたと誤解しがちですが、完全に互換性があります。

なぜJVMはStringが重複排除の対象となる前に年齢の閾値(デフォルト3 GC)を課すのですか?

回答。 年齢の閾値は、次の若いコレクションで死ぬ可能性の高い短命の一時的な文字列を重複排除するためにCPUサイクルを無駄にしないようにします。Stringが複数のG1の避難サイクルを生き延びることを要求することにより(EdenからSurvivor領域に昇進し、最終的にはTenuredに向かう)、このヒューリスティックは、長期生存の高い可能性のある「成熟した」文字列のみが処理されることを保証します。これにより、ハッシュ計算とテーブルルックアップのコストがオブジェクトの期待される寿命にわたって償却されます。

String重複排除は、Stringインスタンスの不変性やhashCodeの安定性に影響しますか?

回答。 いいえ。重複排除プロセスは、valueフィールドの参照の変異の実装の詳細に過ぎません。置き換えられた配列には同一のバイトまたは文字が含まれているため、Stringの論理状態やhashCodeは変わりません。hashCodeStringオブジェクト自体の一時フィールドにキャッシュされており、内容が同一であるため、キャッシュされた値は有効なままです。equals契約が保持されるのは、内容の等価性がバックストアの参照の等価性がAPI契約にとって重要でないことを意味するからです。この操作はアプリケーションの視点から原子的であり、Stringの不変性の保証を維持します。