CLOVER🍀

That was when it all began.

Java VMのヒープサイズをパーセンテージで指定する(-XX:InitialRAMPercentage、-XX:MaxRAMPercentage)

これは、なにをしたくて書いたもの?

Java VMのヒープサイズを指定するオプションといえば、-Xmx(下限値)と-Xmx(上限値)だと思います。

これらのオプションは、ヒープサイズを直接指定するものですが、パーセンテージでも指定できる-XX:InitialRAMPercentage
-XX:MaxRAMPercentageを今回は試してみたいと思います。

-XX:InitialRAMPercentage、-XX:MaxRAMPercentage

JDK-8186248で、ヒープサイズを使用可能なメモリーに対するパーセンテージで指定できるようになりました。

[JDK-8186248] Allow more flexibility in selecting Heap % of available RAM - Java Bug System

指定できるのは-XX:InitialRAMPercentage(初期値のパーセンテージ)、-XX:MaxRAMPercentage(最大値のパーセンテージ)、
そして-XX:MinRAMPercentageの3つです。

これはコンテナ環境での対応の一環で、コンテナに割り当てるメモリーの量が設定で簡単に変更できる環境だと、直接値を指定するより
パーセンテージで指定できた方がより便利だからですね。

ちなみに、同様の意味を持つ-XX:InitialRAMFraction-XX:MaxRAMFraction-XX:MinRAMFractionもあるのですが、こちらは割合で
指定するものになります。パーセンテージで指定したわかりやすいため、これらのオプションはOpenJDK 10で非推奨になっています。
今後は-XX:InitialRAMPercentage-XX:MaxRAMPercentage等を使えばよいでしょう。

なお、Java VMのコンテナ対応は、JDK-8146115を参照することになります。

JavaがDockerコンテナ内でどのようにCPU数、メモリサイズを取得しているのかを調べてみる - CLOVER🍀

コンテナ環境のサポートを有効にするには-XX:+UseContainerSupport(デフォルト)、無効にするには-XX:-UseContainerSupportです。
また、Java VMに使えるCPU数は-XX:ActiveProcessorCountで指定することもできます。

今回は、-XX:InitialRAMPercentage-XX:MaxRAMPercentageを使ってJava VMに割り当てるヒープサイズがどう変わっていくか
見ていきたいと思います。コンテナ環境下でも見ていきましょう。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.7 2023-04-18
OpenJDK Runtime Environment (build 17.0.7+7-Ubuntu-0ubuntu122.04.2)
OpenJDK 64-Bit Server VM (build 17.0.7+7-Ubuntu-0ubuntu122.04.2, mixed mode, sharing)

Docker。

$ docker version
Client: Docker Engine - Community
 Version:           24.0.4
 API version:       1.43
 Go version:        go1.20.5
 Git commit:        3713ee1
 Built:             Fri Jul  7 14:50:55 2023
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          24.0.4
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.5
  Git commit:       4ffc614
  Built:            Fri Jul  7 14:50:55 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.21
  GitCommit:        3dce8eb055cbb6872793272b4f20ed16117344f8
 runc:
  Version:          1.1.7
  GitCommit:        v1.1.7-0-g860f061
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

コンテナイメージを使う時は、Eclipse Temurinを使いたいと思います。

eclipse-temurin / DockerHub

サンプルプログラム

ヒープサイズの指定方法を確認するということで、その結果を確認する必要があります。

今回は、MemoryMXBean-XX:+PrintFlagsFinalオプションを使って確認していきたいと思います。

MemoryMXBean (Java SE 17 & JDK 17)

こんなプログラムを用意しました。

PrintHeapSize.java

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import com.sun.management.OperatingSystemMXBean;

public class PrintHeapSize {
    public static void main(String... args) {
        MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage();
        OperatingSystemMXBean operatingSystemMXBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();

        System.out.printf(
                          "Heap init = %dMB, used = %dMB, max = %dMB, Physical Memory = %dMB%n",
                          toMB(heapMemoryUsage.getInit()),
                          toMB(heapMemoryUsage.getUsed()),
                          toMB(heapMemoryUsage.getMax()),
                          toMB(operatingSystemMXBean.getTotalMemorySize())
                          );
    }

    private static long toMB(long value) {
        return value / 1024 / 1024;
    }
}

まずはデフォルト値を確認する

まずは、デフォルト値を確認してみます。実行時の環境のメモリーは、約16Gです。

実行。

$ java PrintHeapSize.java
Heap init = 250MB, used = 21MB, max = 3976MB, Physical Memory = 15900MB

初期ヒープサイズが250MB、最大ヒープサイズが約4GBです。

どうしてこうなるかというと、Java VMのエルゴノミクスからですね。

  • 初期ヒープ・サイズ: 物理メモリーの1/64
  • 最大ヒープ・サイズ: 物理メモリーの1/4

HotSpot仮想マシン・ガベージ・コレクション・チューニング・ガイド / エルゴノミクス / ガベージ・コレクタ、ヒープおよびランタイム・コンパイラのデフォルト選択

モリーが約16GBなので、その1/64で250MB、1/4で約4GBですね。

-XX:+PrintFlagsFinalオプションで、各値を見てみます。

$ java -XX:+PrintFlagsFinal --version | grep -E '(Max|Min|Initial)HeapSize|RAM(Percentage|Fraction)'
   size_t InitialHeapSize                          = 262144000                                 {product} {ergonomic}
    uintx InitialRAMFraction                       = 64                                        {product} {default}
   double InitialRAMPercentage                     = 1.562500                                  {product} {default}
   size_t MaxHeapSize                              = 4169138176                                {product} {ergonomic}
    uintx MaxRAMFraction                           = 4                                         {product} {default}
   double MaxRAMPercentage                         = 25.000000                                 {product} {default}
   size_t MinHeapSize                              = 8388608                                   {product} {ergonomic}
    uintx MinRAMFraction                           = 2                                         {product} {default}
   double MinRAMPercentage                         = 50.000000                                 {product} {default}
   size_t SoftMaxHeapSize                          = 4169138176                             {manageable} {ergonomic}

こんな感じです。初期ヒープサイズ、最大ヒープサイズはエルゴノミクス決定していることがわかりますね。

なお、-XX:InitialRAMPercentageの初期値は1.5625、-XX:MaxRAMPercentageの初期値は25、-XX:MinRAMPercentageの初期値は50
みたいです。
doubleなので、小数での指定が可能ですね

1.5625は1/64、25は1/4なのでエルゴノミクスとも合った値になっているようです。

-Xms、-Xmxを指定してみる

次に-Xms-Xmxオプションを指定して実行してみます。

$ java -Xms1G -Xmx8G PrintHeapSize.java
Heap init = 1024MB, used = 23MB, max = 8192MB, Physical Memory = 15900MB

当然ですが、最小ヒープサイズ、最大ヒープサイズが変わりました。

-XX:+PrintFlagsFinalオプションで確認。

$ java -Xms1G -Xmx8G -XX:+PrintFlagsFinal --version | grep -E '(Max|Min|Initial)HeapSize|RAM(Percentage|Fraction)'
   size_t InitialHeapSize                          = 1073741824                                {product} {command line}
    uintx InitialRAMFraction                       = 64                                        {product} {default}
   double InitialRAMPercentage                     = 1.562500                                  {product} {default}
   size_t MaxHeapSize                              = 8589934592                                {product} {command line}
    uintx MaxRAMFraction                           = 4                                         {product} {default}
   double MaxRAMPercentage                         = 25.000000                                 {product} {default}
   size_t MinHeapSize                              = 1073741824                                {product} {command line}
    uintx MinRAMFraction                           = 2                                         {product} {default}
   double MinRAMPercentage                         = 50.000000                                 {product} {default}
   size_t SoftMaxHeapSize                          = 8589934592                             {manageable} {ergonomic}

InitialHeapSizeMaxHeapSizeが変わりましたね。

-XX:InitialRAMPercentage、-XX:MaxRAMPercentageを指定してみる

次に、-XX:InitialRAMPercentage-XX:MaxRAMPercentageを指定してみましょう。

$ java -XX:InitialRAMPercentage=6.25 -XX:MaxRAMPercentage=50 PrintHeapSize.java
Heap init = 996MB, used = 23MB, max = 7952MB, Physical Memory = 15900MB

ざっくり、先ほどの-Xms`、-Xmx`に近い値になるパーセンテージにしてみました。ちょっと足りませんが。

この結果を見ると、-XX:InitialRAMPercentage-XX:MaxRAMPercentageのコントロール対象は-Xms-Xmxと一致することが
わかりますね。

この時、-XX:+PrintFlagsFinalで値を確認してみます。

$ java -XX:InitialRAMPercentage=6.25 -XX:MaxRAMPercentage=50 -XX:+PrintFlagsFinal --version | grep -E '(Max|Min|Initial)HeapSize|RAM(Percentage|Fraction)'
   size_t InitialHeapSize                          = 1044381696                                {product} {ergonomic}
    uintx InitialRAMFraction                       = 64                                        {product} {default}
   double InitialRAMPercentage                     = 6.250000                                  {product} {command line}
   size_t MaxHeapSize                              = 8338276352                                {product} {ergonomic}
    uintx MaxRAMFraction                           = 4                                         {product} {default}
   double MaxRAMPercentage                         = 50.000000                                 {product} {command line}
   size_t MinHeapSize                              = 8388608                                   {product} {ergonomic}
    uintx MinRAMFraction                           = 2                                         {product} {default}
   double MinRAMPercentage                         = 50.000000                                 {product} {default}
   size_t SoftMaxHeapSize                          = 8338276352                             {manageable} {ergonomic}

よく見ると、InitialHeapSizeMaxHeapSizeの値が変動しています。-Xms`、-Xmxを指定しても -XX:InitialRAMPercentage-XX:MaxRAMPercentage`には影響していませんでしたが、
ヒープサイズの値には反映してくれるんですね。

なお、InitialRAMFractionMaxRAMFractionは変わっていません。

-XX:InitialRAMFraction、-XX:MaxRAMFractionも試してみる

ついでに、-XX:InitialRAMFraction-XX:MaxRAMFractionも試してみましょう。こちらは「割合」で指定します。

$ java -XX:InitialRAMFraction=16 -XX:MaxRAMFraction=2 PrintHeapSize.java
OpenJDK 64-Bit Server VM warning: Option InitialRAMFraction was deprecated in version 10.0 and will likely be removed in a future release.
OpenJDK 64-Bit Server VM warning: Option MaxRAMFraction was deprecated in version 10.0 and will likely be removed in a future release.
Heap init = 996MB, used = 23MB, max = 7952MB, Physical Memory = 15900MB

-XX:InitialRAMPercentage-XX:MaxRAMPercentageを指定した時と、同じになる割合を指定しました。
思いきり非推奨警告が出ていますね。

-XX:+PrintFlagsFinalで見てみます。

$ java -XX:InitialRAMFraction=16 -XX:MaxRAMFraction=2 -XX:+PrintFlagsFinal --version | grep -E '(Max|Min|Initial)HeapSize|RAM(Percentage|Fraction)'
OpenJDK 64-Bit Server VM warning: Option InitialRAMFraction was deprecated in version 10.0 and will likely be removed in a future release.
OpenJDK 64-Bit Server VM warning: Option MaxRAMFraction was deprecated in version 10.0 and will likely be removed in a future release.
   size_t InitialHeapSize                          = 1044381696                                {product} {ergonomic}
    uintx InitialRAMFraction                       = 16                                        {product} {command line}
   double InitialRAMPercentage                     = 6.250000                                  {product} {default}
   size_t MaxHeapSize                              = 8338276352                                {product} {ergonomic}
    uintx MaxRAMFraction                           = 2                                         {product} {command line}
   double MaxRAMPercentage                         = 50.000000                                 {product} {default}
   size_t MinHeapSize                              = 8388608                                   {product} {ergonomic}
    uintx MinRAMFraction                           = 2                                         {product} {default}
   double MinRAMPercentage                         = 50.000000                                 {product} {default}
   size_t SoftMaxHeapSize                          = 8338276352                             {manageable} {ergonomic}

こちらもInitialHeapSizeMaxHeapSizeの値に反映されるようです。InitialRAMPercentageMaxRAMPercentage
変わっていませんが。

コンテナ環境で試してみる

最後に、コンテナ環境で試してみましょう。

先ほどのプログラムを、コンテナ内で実行。

$ docker container run -it --rm --name temurin \
    -v $(pwd):/host \
    eclipse-temurin:17.0.7_7-jdk-jammy java /host/PrintHeapSize.java
Heap init = 250MB, used = 20MB, max = 3976MB, Physical Memory = 15900MB

結果は、ホスト側で実行した時と同じです。リソースを絞っていませんからね。

コンテナに割り当てるメモリーを2Gにしてみましょう。

$ docker container run -it --rm --name temurin \
    -v $(pwd):/host \
    --memory 2G \
    eclipse-temurin:17.0.7_7-jdk-jammy java /host/PrintHeapSize.java
Heap init = 32MB, used = 9MB, max = 512MB, Physical Memory = 2048MB

コンテナに割り当てたメモリーサイズを認識していますね。

では、-XX:InitialRAMPercentage-XX:MaxRAMPercentageを指定してみます。

docker container run -it --rm --name temurin \
    -v $(pwd):/host \
    --memory 2G \
    eclipse-temurin:17.0.7_7-jdk-jammy java -XX:InitialRAMPercentage=6.25 -XX:MaxRAMPercentage=50 /host/PrintHeapSize.java
Heap init = 128MB, used = 19MB, max = 1024MB, Physical Memory = 2048MB

コンテナに割り当てたメモリーサイズを元に、パーセンテージで指定した値にヒープサイズが設定されましたね。

これで、確認したいことはできた感じです。

まとめ

Java VMのヒープサイズをパーセンテージで指定できる、-XX:InitialRAMPercentage-XX:MaxRAMPercentageを試してみました。

コンテナ環境だとヒープサイズをパーセンテージ(割合)で指定したくなることも多いと思いますので、覚えておきましょう。

参考)

List of JVM memory configuration flags