Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

-XX:+AlwaysPreTouchでJavaVM起動時に全ヒープを物理メモリに載せたらどれくらい起動が遅くなるの?

結論: ヒープサイズ4GBのときWindowsで2秒程度、WSLで3秒程度。ヒープサイズ1GBのときWindowsで1秒程度、WSLで0.5秒程度、起動時のオーバーヘッドが増える模様。

動機

JavaVMの起動時に-Xms-Xmxに同じ値を指定しても、JavaVM起動時点でそれだけの物理メモリが確保されるわけではない、という話があります。-Xms-Xmxでメモリ量を指定してJavaVMを起動した直後の状態だと、おそらくUnix系のOSならbrk(2)sbrk(2)WindowsだったらVirtualAllocあたりが呼ばれるので、この時点でプログラムブレークの位置は設定される*1 。だけど、JavaVMは確保したメモリすべてに触りにいくわけじゃないからその時点ではOSはページフォールトを発生させる理由がない。よってOSから見ると仮想メモリがコミットされるだけで物理メモリは割り当られてない、という動きになるわけです。

stackoverflow.com

それだけでもメモリ確保の都度brk(2)sbrk(2)を呼ぶ動きを抑止できるという意味で、-Xms-Xmxを同じ値に設定するのはスループットを高めるには有用と思われます*2

で、物理メモリ上に本当に領域が確保される必要はないにしても、システムのリソース設計をするときはJavaヒープが全て物理メモリに載った状態でもスワップが発生しないよう設計にしておかないと、スラッシングによる性能劣化が怖い。そういう前提でリソース設計をしてたのに、いざシステムが稼動したら設計をミスっててプロセスが仮想メモリを確保できず起動しない、という状況はテストで潰しておきたい。OSがWindowsであればOSの仕様上オーバーコミットはできないからシステム稼動中と同じようにプロセスを起動しておけばこれがテストできる*3。一方、Linuxの場合はオーバーコミットするのがOSのデフォルト設定になっているので、この目的でテストをしたければ-Xms-XmxでコミットしたメモリすべてにJavaVMが触りに行ってページフォールトを発生させないとテストができないわけです。

そのためかどうかわかりませんが、Javaには-XX:+AlwaysPreTouchという起動オプションがあって、これを指定するとJavaVM起動時に確保したメモリ(ページ)すべてに触りに行く。ただ、もちろん全メモリに触りに行くわけなので起動時のオーバーヘッドはそれなりにかかる。じゃぁどれくらいオーバーヘッドがかかるの?というのが知りたいわけです。毎分1回起動してそれなりの時間処理をするようなプロセスで、プロセス起動時のオーバーヘッドが何十秒も増えたらそれは困るので。

以下のようなプログラムを作って、-XX:+AlwaysPreTouchありとなしで差を確認してみました。

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

public class AlwaysPreTouchTest {
    public static void main(String[] main) {
        OperatingSystemMXBean osMBean = (OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean();
        System.out.printf("Free physical memory: %dkB\n", osMBean.getFreeMemorySize() / 1024);
        System.out.printf("Total Java Heap: %dkB\n", Runtime.getRuntime().totalMemory() / 1024);
    }
}

Windowsの場合

Windowsでの結果は以下のような感じ。ヒープ4GBで-XX:+AlwaysPreTouchを指定したときの起動時間は1秒~2秒でけっこうバラつく。

PS C:\tmp> java --version
openjdk 17.0.9 2023-10-17
OpenJDK Runtime Environment Temurin-17.0.9+9 (build 17.0.9+9)
OpenJDK 64-Bit Server VM Temurin-17.0.9+9 (build 17.0.9+9, mixed mode, sharing)

PS C:\tmp> (Measure-Command { Invoke-Expression "java.exe -Xmx4g -Xms4g AlwaysPreTouchTest" | Out-Default }).TotalSeconds
Free physical memory: 5545840kB
Total Java Heap: 4194304kB
0.3735332
PS C:\tmp> (Measure-Command { Invoke-Expression "java.exe -Xmx4g -Xms4g -XX:+AlwaysPreTouch AlwaysPreTouchTest" | Out-Default }).TotalSeconds
Free physical memory: 1208368kB
Total Java Heap: 4194304kB
2.4802916
PS C:\tmp> (Measure-Command { Invoke-Expression "java.exe -Xmx1g -Xms1g AlwaysPreTouchTest" | Out-Default }).TotalSeconds
Free physical memory: 5530976kB
Total Java Heap: 1048576kB
0.1812453
PS C:\tmp> (Measure-Command { Invoke-Expression "java.exe -Xmx1g -Xms1g -XX:+AlwaysPreTouch AlwaysPreTouchTest" | Out-Default }).TotalSeconds
Free physical memory: 4451272kB
Total Java Heap: 1048576kB
1.2108386

WSLの場合

WSLでの結果は以下のような感じ。ヒープ4GBで-XX:+AlwaysPreTouchを指定したときの起動時間は3秒程度で安定している。一方、ヒープ1GBのときのオーバーヘッドはなぜか生のWindowsより小さい。

satob:~$ java --version
openjdk 17.0.11 2024-04-16
OpenJDK Runtime Environment (build 17.0.11+9-Ubuntu-120.04.2)
OpenJDK 64-Bit Server VM (build 17.0.11+9-Ubuntu-120.04.2, mixed mode, sharing)
satob:~$ time java -Xmx4g -Xms4g AlwaysPreTouchTest
Free physical memory: 7498060kB
Total Java Heap: 4194304kB

real    0m0.091s
user    0m0.090s
sys     0m0.019s
satob:~$ time java -Xmx4g -Xms4g -XX:+AlwaysPreTouch AlwaysPreTouchTest
Free physical memory: 3159932kB
Total Java Heap: 4194304kB

real    0m0.472s
user    0m0.201s
sys     0m3.046s
satob:~$ time java -Xmx1g -Xms1g AlwaysPreTouchTest
Free physical memory: 7529068kB
Total Java Heap: 1048576kB

real    0m0.218s
user    0m0.113s
sys     0m0.135s
satob:~$ time java -Xmx1g -Xms1g -XX:+AlwaysPreTouch AlwaysPreTouchTest
Free physical memory: 6441500kB
Total Java Heap: 1048576kB

real    0m0.184s
user    0m0.096s
sys     0m0.756s

*1:WindowsであればここでパフォーマンスモニターのCommitted Bytesが増える

*2:最近のLinuxではbrk(2)もだいぶ高速化されてるという説明がmalloc動画にありましたが実態がどうかは把握していない。あと、メモリをプロセスからOSに返す際のmadvice(MADV_DONTNEED)が遅いという話がそれとは別であった気がします

*3:もちろんCommit Limitはページングファイルを含めた最大値なのでスラッシングの懸念はありますが……