1. 概要

JVMがメモリを管理します。 これにより、開発者のメモリ管理の負担がなくなるため、オブジェクトポインタを手動で操作する必要がなくなります。これは時間がかかり、エラーが発生しやすいことが証明されています。

内部的には、JVMには、メモリ管理プロセスを最適化するための多くの優れたトリックが組み込まれています。 1つのトリックは、この記事で評価する圧縮ポインターの使用です。 まず、JVMが実行時にオブジェクトをどのように表すかを見てみましょう。

2. ランタイムオブジェクト表現

HotSpot JVMは、oopsまたはOrdinaryObjectPointersと呼ばれるデータ構造を使用してオブジェクトを表します。 これらのoopsは、ネイティブCポインターと同等です。 instanceOopsは、Javaのオブジェクトインスタンスを表す特別な種類のoopです。 さらに、JVMは、OpenJDKソースツリーに保持されている他のいくつかのoopsもサポートします。

JVMがメモリ内にinstanceOopをどのようにレイアウトするかを見てみましょう。

2.1. オブジェクトメモリレイアウト

instanceOop のメモリレイアウトは単純です。これは、オブジェクトヘッダーの直後に、インスタンスフィールドへの0個以上の参照が続くだけです。

オブジェクトヘッダーのJVM表現は、次のもので構成されます。

  • 1つのマークワードは、バイアスロック IDハッシュ値、GCなどの多くの目的に役立ちます。 これはoop、ではありませんが、歴史的な理由から、OpenJDKのoopソースツリーに存在します。 また、マークワードの状態には uintptr_t、のみが含まれるため、そのサイズは32ビットアーキテクチャと64ビットアーキテクチャでそれぞれ4〜8バイトの間で変化します
  • クラスメタデータへのポインタを表す1つの、おそらく圧縮されたKlassワード。 Java 7より前は、永久世代を指していたが、Java 8以降は、メタスペースを指していた。
  • オブジェクトの位置合わせを強制するための32ビットギャップ。 これにより、後で説明するように、レイアウトがハードウェアに適したものになります。

ヘッダーの直後には、インスタンスフィールドへの参照が0個以上ある必要があります。 この場合、はネイティブマシンワードであるため、従来の32ビットマシンでは32ビット、最新のシステムでは64ビットです。

配列のオブジェクトヘッダーには、マークワードとクラスワードに加えて、その長さを表す32ビットワードが含まれています。

2.2。 廃棄物の解剖学

従来の32ビットアーキテクチャから最新の64ビットマシンに切り替えるとします。 最初は、すぐにパフォーマンスが向上することが期待できます。 ただし、JVMが関係している場合は必ずしもそうとは限りません。

このパフォーマンス低下の主な原因は64ビットオブジェクト参照です。64ビット参照は32ビット参照の2倍のスペースを占めるため、一般的にメモリ消費量が増え、GCサイクルが頻繁になります。 。 GCサイクルに費やす時間が長いほど、アプリケーションスレッドのCPU実行スライスは少なくなります。

では、元に戻して、これらの32ビットアーキテクチャを再度使用する必要がありますか? これがオプションであったとしても、もう少し作業をしなければ、32ビットのプロセススペースに4GBを超えるヒープスペースを確保することはできませんでした。

3. 圧縮されたOOP

結局のところ、JVMはオブジェクトポインタまたは oops、を圧縮することでメモリの浪費を回避できるため、両方の長所を活用できます。32ビットで4GBを超えるヒープスペースを許可64ビットマシンでの参照!

3.1。 基本的な最適化

前に見たように、JVMはオブジェクトにパディングを追加して、オブジェクトのサイズが8バイトの倍数になるようにします。 これらのパディングを使用すると、oopsの最後の3ビットは常にゼロになります。 これは、8の倍数である数値が常にで終わるためです。 000 バイナリで。

JVMは、最後の3ビットが常にゼロであることをすでに認識しているため、これらの重要でないゼロをヒープに格納しても意味がありません。 代わりに、それらが存在することを前提とし、以前は32ビットに収めることができなかった他の3つの重要なビットを格納します。 これで、右にシフトされた3つのゼロを持つ32ビットアドレスができたので、35ビットポインターを32ビットポインターに圧縮しています。 これは、64ビット参照を使用せずに最大32 GB – 232 + 3 = 235 = 32 GB –のヒープスペースを使用できることを意味します。

この最適化を機能させるために、JVMがメモリ内のオブジェクトを見つける必要がある場合、ポインタを3ビット左にシフトします(基本的にこれらの3つのゼロを最後に追加します)。 一方、ヒープへのポインタをロードする場合、JVMはポインタを右に3ビットシフトして、以前に追加されたゼロを破棄します。 基本的に、JVMはスペースを節約するためにもう少し計算を実行します。 幸いなことに、ビットシフトはほとんどのCPUにとって本当に簡単な操作です。

oop 圧縮を有効にするには、 -XX:+UseCompressedOops調整フラグを使用できます。 oop 圧縮は、最大ヒープサイズが32GB未満の場合のJava7以降のデフォルトの動作です。最大ヒープサイズが32GBを超える場合、JVMは自動的にoopをオフにします。圧縮。したがって、32Gbヒープサイズを超えるメモリ使用率は別の方法で管理する必要があります。

3.2。 32GBを超える

Javaヒープサイズが32GBを超える場合は、圧縮ポインターを使用することもできます。 デフォルトのオブジェクト配置は8バイトですが、この値は-XX:ObjectAlignmentInBytesチューニングフラグを使用して構成できます。 指定する値は2の累乗で、8〜256の範囲内である必要があります

次のように、圧縮されたポインタを使用して可能な最大ヒープサイズを計算できます。

4 GB * ObjectAlignmentInBytes

たとえば、オブジェクトの配置が16バイトの場合、圧縮されたポインターで最大64GBのヒープスペースを使用できます。

アラインメント値が大きくなると、オブジェクト間の未使用スペースも増える可能性があることに注意してください。 その結果、Javaヒープサイズが大きい圧縮ポインターを使用してもメリットが得られない場合があります。

3.3。 未来のGC

Java11に新しく追加されたZGCは、実験的でスケーラブルな低レイテンシのガベージコレクターでした。

GCの一時停止を10ミリ秒未満に保ちながら、さまざまな範囲のヒープサイズを処理できます。 ZGCは64ビットカラーポインタを使用する必要があるため、圧縮参照をサポートしていません。 したがって、ZGCのような超低遅延のGCを使用することと、より多くのメモリを使用することを比較検討する必要があります。

Java 15の時点で、ZGCは圧縮クラスポインターをサポートしていますが、圧縮OOPのサポートはまだありません。

ただし、すべての新しいGCアルゴリズムは、メモリと低レイテンシのトレードオフにはなりません。 たとえば、 Shenandoah GC は、休止時間が短いGCであることに加えて、圧縮された参照をサポートします。

さらに、ShenandoahとZGCの両方がJava15の時点で完成しています。

4. 結論

この記事では、64ビットアーキテクチャでのJVMメモリ管理の問題について説明しました。 圧縮ポインターとオブジェクトアラインメントを調べ、JVMがこれらの問題にどのように対処できるかを確認しました。これにより、無駄の少ないポインターと最小限の余分な計算で、より大きなヒープサイズを使用できるようになります。

圧縮された参照の詳細については、AlekseyShipilëvのさらに別のすばらしい記事を確認することを強くお勧めします。 また、HotSpot JVM内でオブジェクトの割り当てがどのように機能するかを確認するには、Javaでのオブジェクトのメモリレイアウトの記事を確認してください。