Java仮想マシン(JVM:Java Virtual Machine、Java VM)とは、classファイルに収められたバイトコードを実行する仮想的な計算機です。JVMはJavaのソースコードではなくコンパイル済みのバイトコードを読むので、同じclassファイルをWindowsでもmacOSでもLinuxでも動かせます。
この記事では、JVMが命令を実行する仕組み、ヒープとメタスペースの区別、ガベージコレクタが選ばれる条件、JITコンパイラの段階とAOTキャッシュ、各OSでのインストール手順を扱います。掲載した出力は、Eclipse Temurin 25.0.5+8(2026年10月20日版)とAzul Zulu 27をmacOS 26.6.2(Intel・メモリ16GB)で実行して採取したものです。
まとめ:JVMの要点
- JVMは
classファイルのバイトコードを実行する抽象機械で、動作は「The Java Virtual Machine Specification」が定めています。Java SE 27 Editionではmajor_versionの範囲が45から71です。 - 命令はスタックに値を積んで計算する形式です。
a + bはiload_0・iload_1・iadd・ireturnの4命令になります。 - バイトコードは可搬ですが、JVM自体はOSとCPUごとのネイティブプログラムです。macOSでもIntel用とApple Silicon用は別のバイナリです。
- クラスのメタデータはヒープ外のメタスペースに置かれ、
jcmdでもヒープとは別に報告されます。 - 既定のガベージコレクタは版と環境で変わります。Java 25ではCPUを1個に制限するとSerial GCが選ばれ、Java 27ではJEP 523によりどの環境でもG1になりました。
- JITはC1で一度コンパイルしてから、頻繁に呼ばれるメソッドをC2で作り直します。
fib(32)はJIT有効で13〜16ms、JITを止めると315〜357msでした。 - JVMだけの配布物は無く、JDKかJREを入れるとJVMが入ります。macOSのHomebrewでは、同じ
brew install --cask temurinでもApple Siliconは27、Intelは26が入ります。
JVMとは何か:バイトコードを実行するスタック型の仮想マシン
JVMが読むのは、javacなどのコンパイラが出力したclassファイルです。その形式と命令の意味を規定しているのがThe Java Virtual Machine Specification(JVMS)で、最新はJava SE 27 Editionです。
classファイルのメジャーバージョンと対応するJava
JVMSの4.1節は、major_versionが45から71の範囲でなければならず、Java SE 27に適合する実装はこの範囲をちょうど扱うと定めています。番号はjavap -vで確認できます。
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, JVM");
}
}
$ javac Hello.java
$ javap -v Hello.class | head -8
Classfile /private/tmp/jvmt2/Hello.class
Last modified 2026/11/05; size 414 bytes
SHA-256 checksum 8a17d83bc53bf37ebbbca8313b3a83bdc9d3fb4c0ddfc85ee319d82bf90193c8
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 69
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
Java 25でコンパイルすると69です。メジャーバージョンはJavaのバージョンに44を足した値で、Java 21なら65、Java 27なら71になります。JVMが受け付けるのは自分の版以下の番号だけです。例外はプレビュー機能を使ったクラスで、minor_versionが65535になり、同じ版のJVMでプレビュー機能を有効にしたときしか読み込めません。Java 25のプレビュー機能を使ったクラスは、Java 27のJVMでも動きません。
javapで読むスタック型の命令列
JVMの命令はレジスタではなくオペランドスタックを介して値を受け渡します。2つのintを足すだけのメソッドをjavap -cで逆アセンブルすると、その動きがそのまま見えます。
public class Calc {
static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
System.out.println(add(2, 3));
}
}
$ javac Calc.java
$ javap -c Calc.class # addメソッドの部分だけ抜粋
static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
iload_0とiload_1が局所変数0番(a)と1番(b)をスタックに積み、iaddが上の2つを取り出して和を積み、ireturnがそれを戻り値にします。先頭のiはint型の命令であることを示し、longならlload・ladd、参照ならaloadと型ごとに命令が分かれています。呼び出し側はiconst_2・iconst_3で引数を積んでからinvokestaticでこのメソッドを呼んでいました。
レジスタ数を前提にしない命令形式なので、CPUのレジスタ構成が違ってもバイトコードは同じで済みます。実際のCPU命令への割り当ては、後述のJITコンパイラが実行時に行います。ラムダ式がinvokedynamicに変換される様子など、命令列から読める事例はキャプチャリングとは?Javaのラムダ式・匿名クラスと実質finalの制約でも扱っています。
JDK・JRE・JVMの関係
JVMは単体の製品として配布されておらず、JDK(Java Development Kit)とJRE(Java Runtime Environment)の両方に含まれています。JREはJVMと標準ライブラリの組み合わせで、JDKはそこにjavacやjcmdなどの開発ツールを加えたものです。三者の違いと、Oracle Java SEとOpenJDKのライセンスの違いはJava SEとは?JDK・JRE・JVMの違いとOracle Java/OpenJDKのライセンス【2026年10月】で扱っています。
Write Once, Run Anywhere が成り立つ範囲
「一度書けばどこでも動く」が指すのはバイトコードの可搬性です。JVM自体はOSとCPUアーキテクチャごとにビルドされたネイティブのプログラムで、Intel Mac用のJVMはx86_64、Apple Silicon用はaarch64の別バイナリです。
JNIで呼ぶネイティブライブラリ、パスの区切り文字、OS依存のシステムプロパティには環境差が残ります。文字コードはJDK 18以降のJava SE APIでUTF-8が既定になりましたが、コンソール入出力は別途確認が必要です。可搬なのはバイトコードであってアプリケーション全体ではない、という切り分けが実務では効きます。
JVMがプログラムを実行するまでの流れ
javacがソースをバイトコードに変換するまでがJVMの外側で、classファイルを読み込んだ時点からがJVMの担当です。
クラスローダーによるロード・リンク・初期化
クラスローダーは、クラスが必要になった時点で読み込みます。ロード後、初期化に先立って検証と準備を行い、シンボリック参照の解決は必要になるまで遅延できます。この検証があるため、型の辻褄が合わないバイトコードは実行前に弾かれます。
クラスローダーは階層構造で、ブートストラップクラスローダーがコアクラスを、プラットフォームクラスローダーが標準モジュールを、アプリケーションクラスローダーがクラスパス上のクラスを担当します。親に問い合わせてから自分で探す委譲モデルなので、アプリケーション側でjava.lang.Stringを定義しても標準のクラスは置き換わりません。
インタプリタとJITの併用
本記事で使うHotSpotの通常設定では、バイトコードを解釈するインタプリタと、よく実行される部分をネイティブコードに変換するJITコンパイラを併用します。この状態はjava -versionに表示されます。
$ java -version
openjdk version "25.0.5" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode)
最終行のmixed modeがインタプリタとJITの混在実行を意味します。-XintでJITを止めるとinterpreted modeに変わります。
JVMのメモリ領域とメタスペース
ここからのメモリ構成と診断オプションはHotSpotを対象にしています。次の表は仕様上の実行時領域と実装上の領域を並べたもので、各行の容量は合算できません。
| 領域 | 格納するもの | スレッドごとか | GCの対象 | 主な設定 |
|---|---|---|---|---|
| ヒープ | newで作ったオブジェクトと配列 | 共有 | 対象 | -Xms / -Xmx |
| メタスペース | クラスのメタデータ | 共有 | クラスのアンロード時 | -XX:MaxMetaspaceSize |
| Javaスタック | メソッドのフレームと局所変数 | スレッドごと | 対象外 | -Xss |
| PCレジスタ | 実行中の命令位置 | スレッドごと | 対象外 | 設定不可 |
| ネイティブメモリ | JITのコードキャッシュ、スレッドスタック等 | 混在 | 対象外 | -XX:ReservedCodeCacheSize ほか |
HotSpotはJava 7まで、クラスのメタデータを恒久世代(PermGen)に置いていました。監視API上、恒久世代は通常のオブジェクト用ヒープとは別の非ヒープ領域に分類されます。JEP 122: Remove the Permanent GenerationによりJava 8で恒久世代は廃止され、メタデータはヒープ外のメタスペースへ移りました。java.lang.OutOfMemoryError: PermGen spaceが現行のJVMで出ないのはこのためです。
ヒープの中身にもJava 27で変化があります。JEP 534によりCompact Object Headersが既定になり、64ビット環境のオブジェクトヘッダが96ビットから64ビットに縮みました。UseCompactObjectHeadersはTemurin 25ではfalse、Zulu 27ではtrueと表示されます。小さなオブジェクトを大量に持つアプリケーションほど、版を上げただけでヒープ使用量が変わります。
ヒープとメタスペースが別勘定であることは、稼働中のJVMにjcmdを投げると確かめられます。jcmd -lで得たプロセスIDに対して、2つの診断コマンドを実行した結果です。
$ jcmd <pid> GC.heap_info
garbage-first heap total reserved 4194304K, committed 262144K, used 4096K [0x0000000700000000, 0x0000000800000000)
region size 2048K, 2 young (4096K), 0 survivors (0K)
$ jcmd <pid> VM.metaspace basic
Metaspace used 4009K, committed 4160K, reserved 1114112K
class space used 311K, committed 384K, reserved 1048576K
ヒープが4,194,304KB(4GB)を予約しているのとは別に、メタスペースが1,114,112KBを予約しています。-Xmxでヒープを絞っても、クラスを大量に生成するアプリケーションではメタスペースが膨らむので、プロセス全体のメモリはヒープ外まで見て判断します。
ヒープのサイズ設計とOutOfMemoryErrorの切り分けはJavaヒープとは?既定サイズ・-Xmx設定と「Java heap space」エラーの対処をJDK 25で実測、ヒープ外の内訳はJava Native Memory Tracking(NMT)とは?-XX:NativeMemoryTracking=summaryとjcmdの出力をJDK 25で実測、解放されない参照の追い方はメモリリークとは?原因の特定方法と直し方を言語・環境別に解説で扱っています。
HotSpotの既定GCとエルゴノミクスによる選択条件
「JavaのデフォルトGCはG1」という説明の出典はJEP 248: Make G1 the Default Garbage Collectorで、Java 9でG1がサーバークラスのマシンにおける既定になりました。条件付きの既定なので、実行時に何が選ばれるかはマシン次第です。
次は物理メモリ16GBのマシンで、既定の状態とCPUを1個に制限した状態のフラグを比べた結果です。
$ java -XX:+PrintFlagsFinal -version | grep -E " UseG1GC | UseSerialGC "
bool UseG1GC = true {product} {ergonomic}
bool UseSerialGC = false {product} {default}
$ java -XX:ActiveProcessorCount=1 -XX:+PrintFlagsFinal -version | grep -E " UseG1GC | UseSerialGC "
bool UseG1GC = false {product} {default}
bool UseSerialGC = true {product} {ergonomic}
値の右の{ergonomic}は、JVMが実行時に判断した設定であることを示します。Java 25では、メモリは16GBのままでもCPUが1個と見えるだけでSerial GCに切り替わりました。CPUを2個にすると、-XX:MaxRAM=1gでヒープ計算の元になるメモリ量を絞ってもG1のままでした。CPUを1コアに絞ったコンテナで開発マシンと違うGCが動く現象は、ここから起きます。
この挙動はJava 27で変わりました。JEP 523: Make G1 the Default Garbage Collector in All Environmentsにより、GCを指定しなければHotSpotは常にG1を選びます。Zulu 27で同じ-XX:ActiveProcessorCount=1を付けても、-Xmx200mを足してもUseG1GC = trueのままでした。Java 25以前とJava 27以降が混在する環境では、同じ起動オプションでも小さなコンテナだけGCが違い得ます。本番と同じ版・CPU・メモリ制限で-XX:+PrintFlagsFinalを1回流しておくと、前提の取り違えを防げます。
Java 27で既定が変わった項目と移行時の影響はJava 27とは|非LTSの半年運用とG1・オブジェクトヘッダ既定化の影響でまとめています。
低遅延のZGCは、JEP 474によってJava 23で世代別モードが既定になり、JEP 490によってJava 24で非世代別モードが削除されました。JEP 490で廃止扱い(obsolete)になったZGenerationalオプションは、Java 24と25では警告で済みますが、JDK-8369983: Remove expired ZGC flags for JDK 26でJava 26から削除され、指定するとJVMが起動しません。古い起動スクリプトに-XX:+ZGenerationalが残っていると、Java 26以降への更新で止まります。
各GCのアルゴリズムと選び分けはJavaのGC(ガベージコレクション)とは?Full GCの発生条件とGCログの見方をJDK 25/27で実測、言語をまたいだGCの考え方はガベージコレクション(GC)とは?仕組み・アルゴリズムとGCログの読み方を8言語の実測で解説、ZGCはZGCとは?Javaの低遅延GCの仕組みとG1GCとの違い・有効化手順をJDK 25・27の実測で解説で扱っています。
JITコンパイラの段階とAOTキャッシュによる高速化
JVMは起動直後はインタプリタで実行し、実行回数が増えたメソッドをJITコンパイラでネイティブコードへ変換します。HotSpotのJITはC1とC2の2段構えで、TieredCompilationが既定で有効、TieredStopAtLevelの既定値は4です。
C1からC2への段階コンパイルとログの読み方
計測には、fib(32)の再帰計算をSystem.nanoTime()で挟んだだけのクラスを使いました。次のコードをBench.javaとして保存し、javac Bench.javaでコンパイルしてから実行します。
public class Bench {
static long fib(int n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
public static void main(String[] args) {
long start = System.nanoTime();
long r = fib(32);
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println("fib(32)=" + r + " " + ms + "ms");
}
}
-XX:+PrintCompilationを付けると、どのメソッドがどの段階でコンパイルされたかが出力されます。
$ java -XX:+PrintCompilation -cp . Bench | grep -E "Bench::fib|fib\(32\)"
152 80 3 Bench::fib (24 bytes)
153 81 4 Bench::fib (24 bytes)
154 80 3 Bench::fib (24 bytes) made not entrant: not used
fib(32)=2178309 15ms
3列目が段階(tier)です。fibは起動から152ms時点でtier 3(プロファイル情報を集めるC1のコード)としてコンパイルされ、1ms後にtier 4(C2の最適化コード)で作り直されました。役目を終えたtier 3の版にはmade not entrantが付き、以後は呼ばれません。C1でネイティブコードを生成しながら実行時のプロファイルを収集し、その情報を使ってC2が最適化したコードを生成する分担が出力から読み取れます。
-Xintとの比較で見るJITの効果
同じクラスを別プロセスで3回ずつ実行した結果です。
$ for i in 1 2 3; do java -cp . Bench; done
fib(32)=2178309 16ms
fib(32)=2178309 13ms
fib(32)=2178309 15ms
$ for i in 1 2 3; do java -XX:TieredStopAtLevel=1 -cp . Bench; done
fib(32)=2178309 34ms
fib(32)=2178309 16ms
fib(32)=2178309 32ms
$ for i in 1 2 3; do java -Xint -cp . Bench; done
fib(32)=2178309 319ms
fib(32)=2178309 315ms
fib(32)=2178309 357ms
既定は13〜16ms、C1だけに止めた-XX:TieredStopAtLevel=1は16〜34ms、インタプリタだけの-Xintは315〜357msでした。この程度の小さな計算でもJITの有無で1桁以上の差が出ます。一方でC1だけの実行が16msから34msまで振れたように、プロセスごとのばらつきもあるので、ベンチマークは1回の数字で判断せず、JMHのような専用の計測ツールで暖機を挟んで測るのが前提になります。
AOTキャッシュによる起動準備の削減
起動直後の準備を減らす仕組みが、Project Leydenから入ったAOTキャッシュです。JEP 483: Ahead-of-Time Class Loading & LinkingがJava 24でクラスのロードとリンクの結果を保存する仕組みを導入し、Java 25のJEP 514: Ahead-of-Time Command-Line Ergonomicsで作成が1コマンドになり、JEP 515: Ahead-of-Time Method Profilingでメソッドのプロファイルも保存対象に加わりました。Java 26ではJEP 516: Ahead-of-Time Object Caching with Any GCにより、ZGCを含むどのGCでもAOTキャッシュを使えるようになっています。
注意したいのは、アプリケーションのクラスがJARに入っていないとキャッシュに入らない点です。-cp .のようにディレクトリを指定して作ると、作成時に[warning][aot] Skipping Hello: Unsupported locationと警告が出てHelloは除外されます。JEP 483はクラスパスにJARファイルだけを含めるよう定めているので、先にJARへまとめてから作成します。
$ jar --create --file hello.jar Hello.class
$ java -XX:AOTCacheOutput=hello.aot -cp hello.jar Hello
Reading AOTConfiguration hello.aot.config and writing AOTCache hello.aot
AOTCache creation is complete: hello.aot 9822208 bytes
$ java -XX:AOTCache=hello.aot -Xlog:class+load=info -cp hello.jar Hello | grep " Hello "
[0.098s][info][class,load] Hello source: shared objects file
2回目の実行ではHelloがJARではなくキャッシュ(shared objects file)から読み込まれています。クラス1個のプログラムでも約9.8MBになるのは、起動時に使うJDK側のクラスも一緒に保存されるためで、効果が見込めるのは大量のクラスを読むフレームワーク上のアプリケーションです。作成時と実行時でJDKのリリース、OSとCPUアーキテクチャ、クラスパスが一致している必要があり、JDKを更新したりIntel用からApple Silicon用へ移したりしたら作り直します。JITの仕組みそのものはJITコンパイラとは?仕組み・インタプリタやAOTとの違いと主要言語の実装で詳しく扱っています。
JVMをインストールする手順(macOS・Windows・Linux)
JVMだけを取り出した配布物はありません。開発するならJDK、実行だけならJREを入れれば、どちらにもJVMが含まれます。配布元はOracle JDKのほか、Eclipse Temurin、Amazon Corretto、Azul Zulu、Microsoft Build of OpenJDKなどがあり、いずれもHotSpotを載せたOpenJDKのビルドです。迷ったら現行のLTSであるJava 25を選びます。
macOSへのJDK導入とJAVA_HOMEの設定
JDKを入れていないmacOSでjavaを実行すると、/usr/bin/javaのスタブが次のメッセージを返して終わります。macOSにJVMは同梱されていません。
$ java -version
The operation couldn’t be completed. Unable to locate a Java Runtime.
Please visit http://www.java.com for information on installing Java.
Homebrewを使う場合は、版を指定したcaskを選びます。2026年11月5日時点のtemurinのcaskは、Apple Siliconでは27.0.1.0.2、Intel Macでは26.0.2.1を入れる定義になっていて、版指定なしだと機種によって入るメジャー版が変わります。TemurinはJDK 27からmacOS x64版を出していないためです。
$ brew install --cask temurin@25
$ /usr/libexec/java_home -V
Matching Java Virtual Machines (2):
27 (x86_64) "Azul Systems, Inc." - "Zulu 27.28.101" ...
25.0.5 (x86_64) "Eclipse Adoptium" - "OpenJDK 25.0.5" ...
/usr/libexec/java_home -Vは/Library/Java/JavaVirtualMachinesと~/Library/Java/JavaVirtualMachinesにあるJDKを一覧します。tar.gz版を自分で展開した場合も、ここに置けば認識されます。上の例のように複数の版があると、/usr/bin/javaは既定で一番新しい版(ここでは27)を使います。版を固定するには、~/.zshrcでJAVA_HOMEを指定します。
export JAVA_HOME=$(/usr/libexec/java_home -v 25)
export PATH="$JAVA_HOME/bin:$PATH"
Intel MacでJava 27を使いたい場合は、macOS x64版を提供している配布元を選びます。本記事のJava 27の出力に使ったAzul Zulu 27はIntel Mac用(x86_64)のビルドです。プロジェクトごとに版を切り替えるならSDKMAN!が便利で、手順はSDKMAN!の使い方|Javaのインストール・切り替えと.sdkmanrc、Windows・macOSの注意点にまとめています。
WindowsとLinuxでのインストール
Windowsはmsiインストーラーのほか、wingetで入れられます。TemurinのパッケージIDは版ごとに分かれており、Java 25のJDKはEclipseAdoptium.Temurin.25.JDKです。
winget install EclipseAdoptium.Temurin.25.JDK
Ubuntuはディストリビューションのパッケージで入ります。サーバーで実行するだけなら、GUIライブラリを含まないopenjdk-25-jre-headlessで足ります。Ubuntu 26.04ではdefault-jdkもJava 25を指しています。
sudo apt install openjdk-25-jdk
# 実行だけならこちら
sudo apt install openjdk-25-jre-headless
ディストリビューションのパッケージは配布元のビルドより更新が遅れることがあります。2026年11月5日時点で、Ubuntu 26.04のopenjdk-25-jdkはamd64・arm64・i386向けが25.0.4.1+1-1~26.04.4で、armhf・ppc64el・riscv64・s390x向けは25.0.3~7ea-2でした。比較対象のTemurinは25.0.5+8です。セキュリティ更新を早く適用したい場合は、対象OS・CPU向けの公開状況を確認し、配布元のリポジトリも比較します。リポジトリを登録しても、上流の更新公開日と同日の提供が保証されるわけではありません。
インストール後の確認とUnsupportedClassVersionErrorの直し方
導入後はjava -versionを実行します。前掲の出力で言えば、1行目がJavaのバージョンとLTSかどうか、2行目がランタイムのビルド元、3行目がVMの種別と実行モードです。javaとjavacが別の版を指していないかも、javac -versionで合わせて確かめます。
版が混在した環境でよく出るのがUnsupportedClassVersionErrorです。Zulu 27でコンパイルしたクラスをTemurin 25で動かすと、次のように止まります。
$ java -cp z27 Calc
エラー: メイン・クラスCalcのロード中にLinkageErrorが発生しました
java.lang.UnsupportedClassVersionError: Calc has been compiled by a more recent version of the Java Runtime (class file version 71.0), this version of the Java Runtime only recognizes class file versions up to 69.0
71はJava 27、69はJava 25です。直し方は2通りで、実行側のJVMを27以上に上げるか、コンパイル側でjavac --release 25を指定して69のクラスを出力します。--releaseは出力形式だけでなく、使える標準APIもその版に合わせて検査します。実際に--release 25で作り直したクラスはTemurin 25でそのまま動きました。バージョンを上げる作業全体はJava近代化の進め方|Java 8から25へ上げる手順と実機で落ちる箇所【2026年10月】で扱っています。
JVMの実装とJVM上で動く言語
JVMの仕様を満たす実装や、それを基盤にした製品には次のものがあります。GraalVMのJVM実行はHotSpotを基盤とし、Native Imageは別の実行方式です。
| 実装 | 開発元 | 特徴 |
|---|---|---|
| HotSpot | OpenJDKコミュニティ | 事実上の標準。Temurin・Corretto・Oracle JDK等が採用 |
| GraalVM | Oracle | Graal JITとNative Imageによる事前ネイティブ化 |
| Eclipse OpenJ9 | Eclipse Foundation | IBMが寄贈した実装。起動の速さとクラウドでの効率を掲げる |
| Azul Platform Prime | Azul Systems | Falcon JITコンパイラとC4ガベージコレクタを搭載 |
混同しやすいのが配布物と実装の違いです。Temurin、Corretto、Azul Zulu、Microsoft Build of OpenJDKはいずれもHotSpotを載せたOpenJDKのビルドで、JVMの実装としては同じ系統です。違うのはビルド元、サポート期間、商用利用の条件です。GraalVMとJVMの関係はGraalVMとは?JVMとの違いとNative Imageの作り方【2026年版】で整理しています。
JVM上で動く言語はJavaに限りません。KotlinはAndroidとサーバーサイドの双方で使われ、Scalaは関数型の記法を持ち込み、Groovyはビルドスクリプトなどの動的な用途で使われ、Clojureはリスプ系の構文をJVMに載せています。どれもclassファイルを出力するので、既存のJavaライブラリをそのまま呼べます。JVM側の新機能も共通の基盤として効き、たとえばJava 21で正式化された仮想スレッドはJava 仮想スレッド(Virtual Thread)とProject Loom入門|作り方・仕組み・Spring Boot設定をJDK 25で実測で扱っています。
チューニングの初手:ヒープ上限と稼働中のJVMの調べ方
設定を変える前に、ヒープ使用量とGCログを確認します。ヒープ外メモリの増加が原因なら、ヒープ上限を増やしても解決しません。上限を省略した場合、HotSpotは利用可能なメモリから自動で決めます。
$ java -XX:+PrintFlagsFinal -version | grep -E " MaxHeapSize | InitialHeapSize | MaxRAMPercentage "
size_t InitialHeapSize = 268435456 {product} {ergonomic}
size_t MaxHeapSize = 4294967296 {product} {ergonomic}
double MaxRAMPercentage = 25.000000 {product} {default}
物理メモリ16GBで上限が4GB、つまりMaxRAMPercentageの既定値25%がそのまま効いています。-XX:MaxRAM=1gを付けると上限は268,435,456バイト(256MB)になりました。Linux上のHotSpotでコンテナ対応が有効な場合は、cgroupのメモリ制限がこの計算に使われます。小さいメモリ構成では別のヒューリスティクスも働くので、常に25%になるとは限りません。固定したいときは-Xmxを明示するか、-XX:MaxRAMPercentageで割合を指定します。
稼働中のJVMを調べる道具はJDKに同梱されています。jcmd -lでプロセスを一覧し、jcmd <pid> GC.heap_infoでヒープ、jcmd <pid> VM.metaspaceでメタスペース、jcmd <pid> Thread.printでスレッドダンプ、jcmd <pid> JFR.startでJDK Flight Recorderの記録を始められます。外部ツールを入れる前に、この範囲で事実を集めます。JVMのチューニングを含むJavaシステムの設計・開発を外部に依頼する場合は、要件に合わせて作るフルスクラッチ開発の範囲になります。
どのJDKを本番で使うかは、技術要件だけでなくライセンスとサポート期限の問題でもあります。無償で使える条件の違いはOpenJDKは商用利用できる?無料の範囲とOracle JDKとの違い・ライセンス・サポート期限【2026年10月】で扱っています。
よくある質問
JVMは何の略ですか?
Java Virtual Machineの略で、日本語では「Java仮想マシン」と訳します。「Java VM」も同じものを指し、古い文書では「JavaVM」と一語で書かれていることもあります。
JVMだけをインストールできますか?
JVM単体の配布物はありません。実行だけならJDKでなくJREで足り、Eclipse TemurinはJREパッケージと、GUI関連を除いたheadless JREパッケージの導入方法を公式に案内しています。自分のアプリ向けにさらに小さくしたい場合は、jlinkで必要なモジュールだけを含むランタイムを作れます。
JVMとDockerなどの仮想化は何が違いますか?
ハイパーバイザー型の仮想マシンはOSごと仮想化し、コンテナはOSのカーネルを共有してプロセスを隔離します。JVMが仮想化しているのはOSでもプロセス環境でもなく命令セットです。層が違うので排他的ではなく、コンテナの中でJVMを動かす構成が一般的です。その際は前述のGC選択とヒープ上限がコンテナの制限で変わる点に注意します。
Javaの最新バージョンはどれですか?
OpenJDKは半年ごとにフィーチャーリリースを出しており、JDKプロジェクトのページによればJava 27が2026年9月15日にGAを迎えています。OracleはLTSを2年ごとに提供する方針で、直近のLTSはJava 25(2025年9月16日GA)です。OracleのJava SE Support Roadmapでは、非LTSのJava 27のPremier Supportが2027年3月まで、Java 25は2030年9月までです。本番の選定ではGAの新しさよりこの期間が判断材料になります。21と25の違いはJava 21と25の違い|正式化した新機能・サポート期限・移行で止まる箇所を実機で比較で比べています。
JITを無効にするとどうなりますか?
-Xintを付けるとインタプリタだけで実行され、java -versionの表示がinterpreted modeになります。前述のfib(32)では13〜16msが315〜357msに延びました。JITが原因と疑われる不具合の切り分けには使えますが、本番の設定にはしません。