Metaspace in OpenJDK 16

原文はこちら。
The original article was written by Leo Korinth (Principal Member of Technical Staff at Oracle).
https://lkorinth.github.io/posts/2020-11-27-metaspace.html

Thomas Stüfe (SAP) がJEP 387に記載の通り新しいMetaspaceに貢献してくださいました。これはOracle以外のコントリビューターによる、OpenJDKになされた改善の素晴らしい例です。

8251158: Implementation of JEP 387: Elastic Metaspace #336
https://github.com/openjdk/jdk/pull/336
JEP 387: Elastic Metaspace
https://openjdk.java.net/jeps/387

Metaspace improvements in OpenJDK 16

Java 16を使い始めると、何もしなくても新しいMetaspaceの恩恵を受けることができます。

  • Metaspaceの断片化が減り、結果としてメモリ使用量が減ります。
  • メモリが使用されなくなると、オペレーティングシステムに戻される確率が格段に高くなり、JVMプロセスのメモリ・フットプリントが削減されます。

多くクラスをロードしていて、メモリ使用量を削減したい場合は、この変更に注目すべきでしょう。この変更までは、小さなクラスローダはチャンクサイズの柔軟性がないため、多くのメモリを浪費していました。WildflyやEclipseのような通常のアプリケーションでも、Metaspaceではメモリを大幅に削減(10%超)することができます。Thomas Stüfeによるプレゼンテーションを見てください。統計はp.47から始まります。

Elastic Metaspace
https://github.com/tstuefe/jep387/blob/master/pres/elastic-metaspace.pdf

Metaspace background

Metaspaceとは、メモリアロケータ(またはアロケータが使用する空間)の名前で、Javaヒープ外のメモリを割り当てます。このメモリは主にクラスのメタデータに使用されますが、一般的なアリーナ・アロケータとしても使用できます。JDK 8以前では、メタデータはpermanent generation (PermGen) と呼ばれるJavaヒープの特別な部分に割り当てられていました。

アロケータは、メモリの割り当てが可能であること、そしてほとんどの場合、メモリの解放が可能である必要があります。Metaspaceにはもう一つの重要な機能として、指定されたアドレス空間内でMetaspaceにメモリを割り当てることができるというものがあります。この機能を使うと、各Javaオブジェクトは、各オブジェクトヘッダーの64ビットポインタを無駄にせず、32ビットインデックスを使ってクラスデータに到達できます。この機能は圧縮クラス・ポインタ(compressed class pointers)と呼ばれ、圧縮oops(compressed oops)と非常によく似た動作をします。

What is Compressed Class Space?
https://stuefe.de/posts/metaspace/what-is-compressed-class-space/
CompressedOops
https://wiki.openjdk.java.net/display/HotSpot/CompressedOops

クラスのロードやアンロードは、ほとんどの場合、クラスローダが破棄されるまで(その後、すべてが解放されるまで)解放せずにデータを確保するというアロケーションパターンに従います。この利用パターンを使うと、圧縮クラス・ポインタのサポートに加えて、アロケータを多少チューニングできます。

Why not use an ordinary allocator such as malloc?

一般的なmallocは、(圧縮クラスポインタのために)制限されたメモリ空間でのメモリ割り当て方法を知りません。そのため、クラス・メタデータへのすべての参照は間接テーブル(indirection table)を経由しなければなりません(その場合はコンパイラの変更が必要で、パフォーマンスに影響を与える可能性があります)。それ以外の方法であれば、特別なmallocが必要です。小さなアドレス空間への割り当て方法を知っている特別なmallocとして見ることができます。(通常の)mallocベースの方法をテストした結果、使わないことになりました。

JEP 387: Elastic Metaspace – Alternatives
https://openjdk.java.net/jeps/387#Alternatives

Short walk-through

Chunk allocation using a buddy allocator

メモリはOSからVirtualSpaceNodesに割り当てられます。圧縮クラス空間に割り当てられていない場合(青)、VirtualSpaceNodesは多くのノードを持つ1つのLinkedListに格納されます。圧縮クラス空間に割り当てられている場合(オレンジ)、リストには32ビットのアドレス空間を予約している正確に1つのVirtualSpaceNodeが含まれるため、圧縮ポインタでのアクセスが保証されます。アドレス空間全体が一度に予約されますが、メモリは使用されるときだけ遅延コミットされます。

各VirtualSpaceNodeには、どのcommit granule(コミットの粒)がコミットされたかを追跡するビットマップがあります。詳細は後ほどご紹介します。

圧縮クラスポインタを使用して実行されているJVMでは、2つのVirtualSpaceNodeListがある。1つは通常のオブジェクト用、もう1つは圧縮クラスポインタで指定する必要のあるオブジェクト用。

各VirtualSpaceNodeは、アドレス空間内の連続したRootChunkAreasに分割されます(現在のサイズは4MiB)。これはメモリ割り当ての最大サイズです。RootChunkAreaは、再帰的に対数サイズの4, 2, 1, 1/2, … MiBの小さな領域に半減することができる領域です。これらのチャンクは、バディアロケータ(buddy allocator)スキームを使用して、安価に削除したり、マージして戻すことができます。追加されると、チャンクは隣のバディに再帰的にマージしようとするので、外部からの断片化を減らすことができます。

Buddy Allocator
https://en.wikipedia.org/wiki/Buddy_memory_allocation

青のクラスローダーが1MiBのブロックを確保するにあたり、ルートチャンクを2回分割する必要がある。
オレンジのクラスローダーが256KiBを確保する。両クラスローダーが同じRootChunkAreaからチャンク
を確保する可能性がある。
オレンジのクラスローダーが破棄されると、解放された256KiBがそのバディのチャンクとマージし、それが2回続く。これにより、外部フラグメンテーションなしで大きな連続領域が残る。

一般的なケースでは、JVMは、クラスデータが置かれる圧縮空間と、アドレス空間に制限されず、圧縮クラスポインタを必要としないすべてのものに使用される非圧縮空間の2つのVirtualSpaceNodeリストのインスタンスを持ちます。

Class loader allocation and destruction

JVMは常にクラスローダを使ってメモリ確保します。JVMには多くのクラスローダが含まれており、それらはJVMプロセス生存期間中に作成され、破棄されます。各クラスローダは、2つのJVM全体のVirtualSpaceListからチャンクを確保します。これらのメモリの大きなチャンクから、現在の割り当てに必要なバイト量をユーザにバンプ割り当てされます。残りのチャンクは後からバンプ割り当て(bump allocation)可能です。

チャンク内のバンプ割り当て

バンプアロケーションが使われるのは、(内部フラグメントを除去して)空きが少なくなり、安価だからです。メモリの早期返却は珍しく、それによりバンプアロケーションの外部フラグメントの問題が少なくなり、クラスローダがパージされた際にバディアロケータが外部フラグメントを残しません。あまり一般的ではないケースでは、クラスローダ破棄前にメモリが解放された場合、そのメモリはFreeBlocksに配置されます。

FreeBlocks (解放されたメモリのストレージ)

この free list(フリーリスト)は、小さなサイズではルックアップテーブルとして、大きなサイズではツリーとして実装されています。クラスローダが破壊されると、空きチャンクが返され、バディアロケータがマージし、断片化を減らします。

全てのパーツが相互作用する

Committing and releasing memory

各VirtualSpaceNodeにはビットマップが含まれており、どのメモリがコミットされたかを追跡しています。各ビットはcommit granuleを表します。大きなチャンクが割り当てられていても、バンプ割り当ての際にメモリを遅延コミットするため、メモリ使用量をcommit granuleにタイトに保ちます。

新しいMetaspaceの改良点の1つは、メモリのコミットをグラニュールレベルで処理する点です。これにより、これまでの事前決定されたチャンクサイズよりもはるかにタイトになります。これはフラグメント化の削減と組み合わせて、JVMプロセスによるメモリ使用量の削減につながります。

(バディ・アロケータにより)特定のクラス・ローダに属するチャンクが常にオブジェクトを割り当てるので、異なるクラス・ローダ間のメモリ割り当てが(チャンク内で)インターリーブすることはありません。クラスローダがパージされると、他のクラスローダからのクラスローダのデータは干渉しないため、チャンクのほとんどのメモリをOSに戻すことができます。

Further reading

Metaspaceのより完全な全体像を知りたいのであれば、実装者の Thomas Stüfeが書いた Metaspace のより詳細なレビューガイドを読むか、彼のブログでビデオプレゼンテーションやその他のトピックを読むことをおすすめします。OpenJDKにはMetaspace wikiもありますので、そちらもどうぞ。

Elastic Metaspace Review Guide
http://cr.openjdk.java.net/~stuefe/jep387/review/2020-09-03/guide/review-guide.html
stuefe.de
https://stuefe.de/
Metaspace wiki
https://wiki.openjdk.java.net/display/HotSpot/Metaspace

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中