原文はこちら。
The original article was written by Stefan Johansson (HotSpot GC team at Oracle).
https://kstefanj.github.io/2021/05/19/large-pages-and-java.html
最近、JVMのメモリ予約コードに多くの時間を費やしています。これは、Linuxで複数のサイズのラージページを使えるようにするという外部からのコントリビューションがきっかけでした。これを良い形で実現するためには、まず他のいくつかのものをリファクタリングする必要がありました。記憶を辿っているうちに、JVMによるラージページの利用方法を簡単にまとめると面白い読み物になるかもしれないと思い、このエントリを書いています。
Introduction to large pages
JVMがどのように利用しているかを説明する前に、まず、ラージページとは何かを簡単に紹介します。
ラージページ(Large Pages)、時にはヒュージページ(Huge Pages)と呼ばれることもありますが、これはプロセッサのTLB(Translation lookaside buffer)キャッシュの負担を軽減するための手法です。これらのキャッシュを使って、仮想アドレスを物理メモリのアドレスに変換する時間を短縮します。ほとんどのアーキテクチャは複数のページサイズをサポートしています(ベースページサイズは4KBであることが多い)。大規模なJavaヒープなど、大量のメモリを使用するアプリケーションでは、TLBにヒットする確率を高めるために、より大きなページ粒度でメモリをマッピングするのは理にかなっています。x86-64では、2MBや1GBのページがこの目的のために使用されており、メモリを大量に使用するワークロードでは、これが非常に大きな影響を与えます。

上のグラフは、ラージページを使用した場合と使用しない場合の違いをいくつかのSPECjbb®1ベンチマークで比較したものです。高性能なJVMがラージページを有効にしている点が構成上の唯一の違いです。この結果は非常に印象的で、多くのJavaワークロードにとって、ラージページを有効にすると性能が向上することがわかります。
Enabling large pages
Javaでラージページを有効にする一般的なスイッチは-XX:+UseLargePages
ですが、ラージページを活用するためには、OSも適切に設定する必要があります。ここでは、LinuxとWindowsでの設定方法を見てみましょう。
Linux
Linuxでは、JVMがラージページを利用する方法は、主に2種類、Transparent Huge PagesとHugeTLB pageがあります。これらの方法は、設定方法が異なるだけでなく、パフォーマンス特性にも若干の違いがあります。
Transparent Huge Pages
Transparent Huge Pages(略してTHP)は、Linuxにおけるラージページの使用と有効化を簡素化する方法です。THPを有効にすると、Linuxカーネルは、THPを使用するのに十分な大きさの予約に対してラージページを使用しようとします。THPサポートを3つのレベルで設定できます。
always | 任意のアプリケーションがtransparent huge pagesを自動的に使用する |
madvise | アプリケーションがMADV_HUGEPAGEフラグをつけてmadvise() を使用し、特定のメモリセグメントをラージページで支えるべきであるとマークした場合にのみ、transparent huge pagesを使用する。madvise() https://man7.org/linux/man-pages/man2/madvise.2.html |
never | transparent huge pagesは使用しない。 |
この設定は/sys/kernel/mm/transparent_hugepage/enabled
に保存され、以下のような方法で簡単に変更できます2。
$ echo "madvise" > /sys/kernel/mm/transparent_hugepage/enabled
madvise
モードで構成した場合にJVMはTHPの利用をサポートしますが、その際、-XX:+UseTransparentHugePages
を使って有効化する必要があります。 これを実施すれば、Javaヒープだけでなく、他の内部JVMデータ構造はTHPによって支持されます。
カーネルがTHPの使用要求を満たすためには、利用可能な十分な連続した物理メモリが必要です。利用可能なメモリがない場合、カーネルは要求を満たすためにメモリのデフラグを行います。デフラグはいくつかの方法で設定でき、現在のポリシーは/sys/kernel/mm/transparent_hugepage/defrag
に保存されています。この設定やその他の設定についての詳細は、カーネルのドキュメントを参照してください。
Transparent Hugepage Support
https://www.kernel.org/doc/Documentation/vm/transhuge.txt
HugeTLB pages
この種のラージページは、OSによって事前に割り当てられ、下支えするために利用される物理メモリを消費します。アプリケーションは、MAP_HUGETLBというフラグをつけてmmap()
を使うと、このプールからページを予約できます。これは、LinuxのJVMでラージページを使用するデフォルトの方法であり、-XX:+UseLargePages
または-XX:+UseHugeTLBFS
を設定することで有効になります。
JVMがこの種のラージページを使う場合、ラージページで下支えされるメモリ範囲全体を前もってコミットします3。 これは、OSが割り当てたラージページのプールが他の予約によって枯渇しないようにするために必要です。つまり、予約時にメモリ範囲全体をバックアップするのに十分なラージページが事前に割り当てられている必要があり、そうでない場合はJVMは通常のページを使用するようにフォールバックします。
この種のラージページを設定するには、まず利用可能なページサイズを確認します。
$ ls /sys/kernel/mm/hugepages/
hugepages-1048576kB hugepages-2048kB
続いて以下のようにページ数を構成します2。
$ echo 2500 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
この例では、2MBページを2500個割り当てようとしています。カーネルが要求された量を割り当てられたかどうかを確認するために、nr_hugepages
に実際に格納されている値を常に読む必要があります。
デフォルトでは、ラージページの予約にあたり、JVMは環境のデフォルトサイズのラージページを使用します。システムのデフォルトラージページのサイズは以下を実行して確認できます。
$ cat /proc/meminfo | grep Hugepagesize
Hugepagesize: 2048 kB
LargePageSizeInBytes
というJVMフラグを設定すれば、異なるラージページサイズを使用できます。例えば、1GBのページを使用するには、-XX:LargePageSizeInBytes=1g
とします。
HugeTLBページに関する詳細情報は以下のドキュメントをご覧ください。
HugeTLBページ
https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt
Which one to use
どちらのアプローチにも長所と短所があり、どちらを選択するかは複数の側面から判断します。THPは設定や使用が簡単ですが、HugeTLBページを使用した方がよりコントロールしやすくなります。レイテンシーが最大の関心事であれば、HugeTLBページを使用するべきでしょう。停止してOSが十分な連続したメモリを解放するのを待つことがないからです。代わりに、THPのdefrag
オプションを設定して、ラージページが利用できない場合にストールしないようにすることもできますが、その代わりにスループットが低下する可能性があります。メモリ使用量を気にするなら、Java ヒープ全体を前もってコミットする必要がないTHPがより良い選択です。
どちらのラージページを使用するかは、アプリケーションや環境によって異なりますが、多くの場合、どちらのラージページを使用してもパフォーマンスに良い影響を与えます。
Windows
Windowsでは、少なくとも新しいバージョンでは、設定手順が少し簡単です。ラージページを使用したいプロセスを実行するユーザーは、「メモリ内のページのロック」を構成できる権限を持つ必要があります。Windows 10では、次の手順で行います。
- gpedit.mscを実行
- 「コンピュータの構成」→「Windowsの設定」→「セキュリティの設定」→「ローカルポリシー」の中から「ユーザー権利の割り当て」を選択
- 「メモリ内のページのロック」をダブルクリック
- 「ユーザーまたはグループの追加」をクリックし、正しいユーザーを追加
- ログアウトまたは再起動して変更を有効化
この権限が付与されると、-XX:+UseLargePages
をつけて実行した場合、JVMはラージページを使用できるようになります。WindowsでのJVMのラージページの実装は、LinuxのHugeTLBページと非常によく似ています。ラージページでバックアップされた予約全体が前もってコミットされるため、後になって失敗することがありません。
Windowsでは最近まで、デフォルトのGCであるG1で4GB以上のヒープに対してラージページを使用できないというバグがありました。
[JDK-8266489] Enable G1 to use large pages on Windows when region size is larger than 2m
https://bugs.openjdk.java.net/browse/JDK-8266489
これは現在修正されており、今後G1で大規模なMinecraftサーバーを運用する場合は、ラージページを有効にすることで素晴らしいブーストが得られるはずです。
Checking the JVM
環境が適切に設定され、Java がラージページで動作するようになったら、JVM が本当にラージページを利用しているかどうかを確認するのが良いでしょう。もちろん、お気に入りのOSツールを使って確認することができますが、JVMにはこれを支援するいくつかのログ・オプションもあります。基本的なGCの設定を見るために、-Xlog:gc+init
を実行してみましょう。G1では次のような出力が得られます。
> jdk-16/bin/java -Xlog:gc+init -XX:+UseLargePages -Xmx4g -version
[0.029s][info][gc,init] Version: 16+36-2231 (release)
[0.029s][info][gc,init] CPUs: 40 total, 40 available
[0.029s][info][gc,init] Memory: 64040M
[0.029s][info][gc,init] Large Page Support: Enabled (Explicit)
[0.029s][info][gc,init] NUMA Support: Disabled
[0.029s][info][gc,init] Compressed Oops: Enabled (Zero based)
[0.029s][info][gc,init] Heap Region Size: 2M
[0.029s][info][gc,init] Heap Min Capacity: 8M
[0.029s][info][gc,init] Heap Initial Capacity: 1002M
[0.029s][info][gc,init] Heap Max Capacity: 4G
[0.029s][info][gc,init] Pre-touch: Disabled
[0.029s][info][gc,init] Parallel Workers: 28
[0.029s][info][gc,init] Concurrent Workers: 7
[0.029s][info][gc,init] Concurrent Refinement Workers: 28
[0.029s][info][gc,init] Periodic GC: Disabled
これをLinuxで実行すると、Large Page Supportが有効になっていることがわかります。Explicitとあるのは、HugeTLBページが使われていることを意味しています。もし、-XX:+UseTransparentHugePages
を付けて実行すると、ログは次のようになります。
[0.030s][info][gc,init] Large Page Support: Enabled (Transparent)
上記はラージページが有効か否かを示しているだけですが、JVMのどの部分がラージページを使用しているかについて、より詳細な情報が必要な場合は、-Xlog:pagesize
を有効にすると、次のような出力が得られます。
[0.002s][info][pagesize] CodeHeap 'non-nmethods': min=2496K max=8M base=0x00007fed3d600000 page_size=4K size=8M
[0.002s][info][pagesize] CodeHeap 'profiled nmethods': min=2496K max=116M base=0x00007fed3de00000 page_size=4K size=116M
[0.002s][info][pagesize] CodeHeap 'non-profiled nmethods': min=2496K max=116M base=0x00007fed45200000 page_size=4K size=116M
[0.026s][info][pagesize] Heap: min=8M max=4G base=0x0000000700000000 page_size=2M size=4G
[0.026s][info][pagesize] Block Offset Table: req_size=8M base=0x00007fed3c000000 page_size=2M alignment=2M size=8M
[0.026s][info][pagesize] Card Table: req_size=8M base=0x00007fed3b800000 page_size=2M alignment=2M size=8M
[0.026s][info][pagesize] Card Counts Table: req_size=8M base=0x00007fed3b000000 page_size=2M alignment=2M size=8M
[0.026s][info][pagesize] Prev Bitmap: req_size=64M base=0x00007fed37000000 page_size=2M alignment=2M size=64M
[0.026s][info][pagesize] Next Bitmap: req_size=64M base=0x00007fed33000000 page_size=2M alignment=2M size=64M
これはかなり詳細な情報ですが、JVMのどの部分がラージページの下支えがあるかを確認するには良い方法です。上記の出力はJDK16使用時の例であり、CodeHeapのページサイズが正しくないというバグがありますが、これらもラージページで下支えされています。
[JDK-8261029] Code heap page sizes not traced correctly using os::trace_page_sizes
https://bugs.openjdk.java.net/browse/JDK-8261029
- SPEC® およびベンチマーク名称である SPECjbb® はStandard Performance Evaluation Corporationの登録商標です。SPECjbbの詳細情報は www.spec.org/jbb2015/ をチェックしてください。
- この機能を使用するには、問題のファイルへの書き込み権限が必要です。
- これはヒープサイズを制限することでコミットの失敗を処理する ZGC ヒープの場合とは異なります。