Java

Java仮想マシン(JVM)とは?仕組み・メモリ構成・JITを実機の出力で解説

Java仮想マシン(JVM)とは?仕組み・メモリ構成・JITを実機の出力で解説

Java仮想マシン(JVM:Java Virtual Machine、Java VM)とは、classファイルに収められたバイトコードを実行する仮想的な計算機です。Javaのソースコードを直接実行するのではなく、コンパイル済みのバイトコードを読み込んで実行するため、同じclassファイルをWindowsでもmacOSでもLinuxでも動かせます。

この記事では、JVMがプログラムを動かすまでの流れ、ヒープとメタスペースの区別、ガベージコレクタが選ばれる条件、JITコンパイラとAOTキャッシュの効果を扱います。説明に使う数値は、Eclipse Temurin 25.0.4.1+1(64-Bit Server VM)をmacOS上で実際に動かして採取した出力です。

まとめ:JVMの要点

  • JVMはclassファイルのバイトコードを実行する抽象機械で、その動作は「The Java Virtual Machine Specification」が定めています。Java SE 27 Edition ではmajor_versionの範囲が45から71と規定されています。
  • JVMはJava専用ではありません。バイトコードさえ生成できればKotlinやScalaなど別の言語からも使えます。
  • バイトコードは可搬ですが、JVM自体はOSとCPUごとに用意されたネイティブのプログラムです。移植性はJVMが吸収しているのであって、Javaのコードが自動的にどこでも動くわけではありません。
  • メモリはヒープだけではありません。クラスのメタデータはヒープ外のメタスペースに置かれ、jcmdでも別々に報告されます。
  • 既定のガベージコレクタは固定ではなく、CPU数とメモリ量から実行時に選ばれます。同じJVMでも条件次第でG1になったりSerialになったりします。
  • JVMはインタプリタとJITコンパイラを併用します。後述の実測では、JITを止めた実行が3回とも374msから394msに収まったのに対し、既定の実行は11msから200msに分布しました。

JVMとは何か:バイトコードを実行するスタック型の仮想マシン

JVMは、Javaのソースコードを知りません。JVMが読むのは、javacなどのコンパイラが出力したclassファイルです。JVMの命令セットはスタックを介して値をやり取りする形式で、オペランドを積んで命令を適用し、結果を積み直すという動作を繰り返します。

この動作を規定しているのがThe Java Virtual Machine Specification(JVMS)です。最新はJava SE 27 Edition で、4.1節はmajor_versionが45から71の範囲でなければならず、Java SE 27に適合する実装はこの範囲をちょうど扱うと定めています。つまりJVMが実行できるバイトコードの世代は、仕様で明示的に区切られています。

この番号は手元で確認できます。次のクラスをJava 25でコンパイルし、javapで逆アセンブルしました。

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/jvmtest/Hello.class
  Last modified 2026/09/20; 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

major version が69で、これがJava 25に対応します。メジャーバージョンはJavaのバージョンに44を足した値で、Java 21なら65、Java 27なら71です。古いJVMで新しいclassファイルを動かそうとしたときに出るUnsupportedClassVersionErrorは、この数値の食い違いを知らせるエラーです。

JDK・JRE・JVMの関係

JVMは単体の製品として配布されているわけではなく、JDK(Java Development Kit)の一部として同梱されています。JDKにはJVMに加えてjavacやjcmdなどの開発ツールが含まれ、JVMと標準ライブラリなどを含む実行環境がJRE(Java Runtime Environment)です。三者の役割分担と、Oracle Java SEとOpenJDKのライセンスの違いはJava SEとは?JDK・JRE・JVMの違いとOracle Java/OpenJDKのライセンスを最新版で解説で扱っています。

Write Once, Run Anywhere が成り立つ範囲

「一度書けばどこでも動く」という標語は、バイトコードの可搬性を指しています。ただしJVM自体は、OSとCPUアーキテクチャごとにビルドされたネイティブのプログラムです。macOSのIntel機で使うJVMとApple Silicon機で使うJVMは別のバイナリで、配布サイトでも別の成果物として並んでいます。

さらに、JNIで呼び出すネイティブライブラリ、ファイルパスの区切り文字、OS依存のシステムプロパティなどには環境差が残ります。文字コードについては、JDK 18以降のJava SE APIは原則としてUTF-8が既定ですが、コンソール入出力や互換設定では別途確認が必要です。可搬なのはバイトコードであって、アプリケーション全体ではない、という切り分けが実務では重要になります。

JVMがプログラムを実行するまでの流れ

ソースコードが動き出すまでは、コンパイル、ロード、リンク、実行の順に進みます。javacがソースをバイトコードに変換するところまでがJVMの外側で、classファイルを読み込んだ時点からがJVMの担当です。

クラスローダーによるロード・リンク・初期化

クラスローダーは、必要になった時点でクラスを読み込みます。ロード後、初期化に先立って検証と準備を行います。リンクにはシンボリック参照の解決も含まれますが、解決は必要になるまで遅延でき、クラスの初期化後も続く場合があります。この検証があるため、不正なバイトコードや型の辻褄が合わないコードは実行前に弾かれます。

クラスローダーは階層構造を持ち、ブートストラップクラスローダーがJavaプラットフォームのコアクラスを、プラットフォームクラスローダーが標準モジュールを、アプリケーションクラスローダーがクラスパス上のクラスを担当します。親に問い合わせてから自分で探す委譲モデルのため、アプリケーション側でjava.lang.Stringを定義しても標準のクラスを置き換えることはできません。

インタプリタとJITの併用

JVMはバイトコードを1命令ずつ解釈するインタプリタと、よく実行される部分をネイティブコードに変換するJITコンパイラを併用します。この併用状態はjava -versionの出力にそのまま現れます。

$ java -version
openjdk version "25.0.4.1" 2026-08-18 LTS
OpenJDK Runtime Environment Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.4.1+1 (build 25.0.4.1+1-LTS, mixed mode)

最終行のmixed modeが、インタプリタとJITの混在実行を意味します。ここがinterpreted modeと表示される場合は、-XintなどでJITが無効化されています。

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で出ないのはこのためです。

ヒープとメタスペースが別勘定であることは、稼働中のJVMにjcmdを投げると確認できます。対象はJavaプロセスであれば何でも構いません。jcmd -lで稼働中のJVMとそのプロセスIDが一覧できるので、そこで得たPIDを渡します。次は同じプロセスに対して2つの診断コマンドを実行した結果です。

$ jcmd <pid> GC.heap_info
garbage-first heap   total reserved 4194304K, committed 262144K, used 4014K [0x0000000700000000, 0x0000000800000000)
 region size 2048K, 2 young (4096K), 0 survivors (0K)

$ jcmd <pid> VM.metaspace basic
Metaspace        used 4005K, committed 4160K, reserved 1114112K
 class space     used 311K, committed 384K, reserved 1048576K

ヒープが4,194,304KB(4GB)を予約しているのとは別に、メタスペースが1,114,112KBを予約しています。-Xmxでヒープを絞っても、クラスを大量に生成するアプリケーションではメタスペースが膨らみます。プロセス全体のメモリ使用量を追うときは、ヒープ外まで見る必要があります。

ヒープ側のサイズ設計とOutOfMemoryErrorの切り分けはJavaヒープとは?メモリ構造・サイズ設定・OutOfMemoryErrorの対処、ヒープ外まで含めた内訳の調べ方はNative Memory Tracking(NMT)とは?jcmdでJVMのネイティブメモリを調べる手順、解放されない参照の追い方はメモリリークとは?原因の特定方法と直し方を言語・環境別に解説で扱っています。

HotSpotの既定GCとエルゴノミクスによる選択条件

「JavaのデフォルトGCはG1」という説明をよく見かけます。出典としてはJEP 248: Make G1 the Default Garbage Collectorがあり、Java 9でG1がサーバークラスのマシンにおける既定になりました。ただし実行時に本当に何が選ばれるかは、マシンの条件で変わります。

次は、物理メモリ16GBのマシンで既定の状態と、CPU1個かつヒープ上限200MBに制限した状態で、同じJVMのフラグを確認した結果です。

$ java -XX:+PrintFlagsFinal -version | grep -E "UseG1GC|UseSerialGC"
     bool UseG1GC     = true   {product} {ergonomic}
     bool UseSerialGC = false  {product} {default}

$ java -XX:ActiveProcessorCount=1 -Xmx200m -XX:+PrintFlagsFinal -version | grep -E "UseG1GC|UseSerialGC"
     bool UseG1GC     = false  {product} {default}
     bool UseSerialGC = true   {product} {ergonomic}

値の右側にある{ergonomic}が、その設定をJVMが実行時に判断したという印です。制限した側ではSerial GCが選ばれています。CPUを絞ったコンテナで動かすと開発マシンと違うGCで動く、という現象はここから起きます。本番と同じCPU・メモリ制限で-XX:+PrintFlagsFinalを1回流しておくと、前提の取り違えを防げます。

低遅延を狙うZGCも扱いが変わりました。JEP 474: ZGC: Generational Mode by DefaultによってJava 23で世代別モードが既定になり、JEP 490: ZGC: Remove the Non-Generational ModeによってJava 24で非世代別モードのコードは削除されました。同JEPはZGenerationalオプションを廃止扱い(obsolete)にしており、-XX:-ZGenerationalを付けても警告が出るだけで非世代別モードには戻せません。この廃止扱いのオプションは期限切れとしてJava 26で削除されており(OpenJDKの変更JDK-8369983: Remove expired ZGC flags for JDK 26、Fix Versionは26)、Java 26以降で指定するとJVMは認識せずに起動を拒否します。Java 24と25では警告で済み、Java 26で止まる、という段階の違いに注意してください。

各GCのアルゴリズムと選び分けはJavaのガベージコレクション(GC)とは|仕組み・種類・チューニングの基礎、言語をまたいだGCの考え方はガベージコレクション(GC)とは?仕組み・アルゴリズム・言語別の違いを解説、ZGCの詳細はZGCとは?Javaの低遅延ガベージコレクションの仕組みとG1GCとの違いで扱っています。

JITコンパイラとAOTキャッシュによる高速化

JVMは起動直後はインタプリタで実行し、実行回数が増えたメソッドをJITコンパイラでネイティブコードへ変換します。HotSpotのJITはC1とC2の2段構えで、TieredCompilationが既定で有効、TieredStopAtLevelの既定値は4です。C1が早めに中程度のコードを出し、さらに頻繁に呼ばれるものをC2が最適化する、という分担になります。

JITがどれだけ効いているかは、-XintでJITを止めた場合と比べると分かります。計測に使ったのは次のクラスで、fib(32)の再帰計算だけをSystem.nanoTime()で挟んでいます。

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");
    }
}

これをjavac Bench.javaでコンパイルし、それぞれ別プロセスで3回ずつ実行した結果が次です。

$ for i in 1 2 3; do java -cp . Bench; done
fib(32)=2178309 200ms
fib(32)=2178309 28ms
fib(32)=2178309 11ms

$ for i in 1 2 3; do java -Xint -cp . Bench; done
fib(32)=2178309 381ms
fib(32)=2178309 374ms
fib(32)=2178309 394ms

インタプリタのみの実行は374msから394msの範囲に収まり、実行ごとのばらつきがほとんどありません。一方JITを有効にした既定の実行は11msから200msまで振れています。速いのは当然として、ばらつきの大きさにも意味があります。JITのコンパイル進行は計測結果に影響しますが、この出力だけでは時間差の原因を特定できません。別プロセス間では通常のJITコンパイル状態は引き継がれず、OSの負荷や計測区間も確認が必要です。ベンチマークで1回だけ測った数字が当てにならないのは、この性質のためです。

起動時のこの遅さを減らす仕組みが、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 -XX:AOTCacheOutput=hello.aot -cp . Hello
Launching child process ... to assemble AOT cache hello.aot using configuration hello.aot.config
Reading AOTConfiguration hello.aot.config and writing AOTCache hello.aot
AOTCache creation is complete: hello.aot 9031680 bytes

$ java -XX:AOTCache=hello.aot -Xlog:aot=info -cp . Hello
[0.016s][info][aot] trying to map hello.aot
[0.016s][info][aot] Opened AOT cache hello.aot.

Java 25では-XX:AOTCacheOutputを付けて一度動かすだけでキャッシュが生成されます。上の例はクラス1個のプログラムでも9MB程度のキャッシュを作っており、効果が見込めるのは起動時に大量のクラスを読むフレームワーク上のアプリケーションです。JITの仕組み自体はJITコンパイラとは?仕組み・種類とAOT・インタプリタの違いをわかりやすく解説で詳しく扱っています。

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ガベージコレクタを搭載

ここで混同しやすいのが、配布物と実装の違いです。Eclipse Temurin、Amazon Corretto、Azul Zulu、Microsoft Build of OpenJDKはいずれもHotSpotを載せたOpenJDKのビルドであり、JVMの実装としては同じ系統です。ビルド元やサポート期間、商用利用の条件が違います。GraalVMとJVMの関係はGraalVMとは?JVMとの違いとNative Imageの作り方【2026年版】で整理しています。

言語の側も同様に、Javaに限りません。KotlinはAndroid開発とサーバーサイドの双方で使われ、Scalaは関数型の記法を持ち込み、Groovyはビルドスクリプトなどの動的な用途で使われ、Clojureはリスプ系の構文をJVM上に載せています。いずれもclassファイルを出力するため、既存のJavaライブラリをそのまま呼び出せます。JVM側の新機能も共通の基盤として効いており、たとえばJava 21で正式化された仮想スレッドはJava Virtual Thread(仮想スレッド)とProject Loomとは?作り方・仕組み・落とし穴を実務目線で扱っています。

JVMの導入とバージョン確認・チューニングの初手

JVMだけを取り出した配布物はありません。開発もするならJDK、実行だけでよいならJREを入れれば、どちらにもJVMが含まれています。macOSではEclipse TemurinなどのpkgかtarballをApple Silicon向け(aarch64)とIntel向け(x64)から選び、Homebrewを使うならbrew install --cask temurinでも入ります。Windowsはmsiインストーラー、Linuxはディストリビューションのパッケージか各ベンダーのtarballが一般的です。

導入後の確認はjava -versionです。前掲の出力で言えば、1行目がJavaのバージョンとLTSかどうか、2行目がランタイムのビルド元(この例ではTemurin)、3行目がVMの種別と実行モードを示します。64-Bit Server VMはサーバー向けの設定が効いた64ビット版という意味で、現在の一般的なJDKはこれです。

チューニングでは、設定変更より先にヒープ使用量と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%がそのまま反映されています。初期サイズは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の記録を開始できます。外部ツールを入れる前に、まずこの範囲で事実を集めます。

なお、どのJDKを本番で使うかは技術要件だけでなくライセンスの問題でもあります。Oracle Java SEとOpenJDKの無償条件の違いはOpenJDKは商用利用できる?無料の範囲とOracle JDKとの違い・ライセンス・サポート期限【2026年最新】で扱っています。

よくある質問

JVMは何の略ですか?

Java Virtual Machine の略で、日本語では「Java仮想マシン」と訳します。「Java VM」も同じものを指す表記です。古い文書では「JavaVM」と一語で書かれていることもあります。

JVMだけをインストールできますか?

JVM単体の配布物はありませんが、JDK全体の導入が必須というわけでもありません。JVMの実行には標準ライブラリなども必要で、開発ツールを含まないその組み合わせがJREです。Eclipse Temurinは実行専用のJREパッケージと、GUI関連ライブラリを除いたheadless JREパッケージの導入方法を公式に案内しています。また、自分のアプリケーション向けに実行環境をさらに小さくしたい場合はjlinkで必要なモジュールだけを含むランタイムイメージを作る方法が用意されています。

MacでJVMを使うにはどうすればよいですか?

Apple Silicon機ならaarch64版を選びます。Intel機では、macOS x64への対応があるJDKの版と配布元を選んでください。Temurin 27はmacOS x64版を提供しないため、Temurin 25などの対応状況を確認します。アーキテクチャを間違えるとRosetta 2経由の実行になったり、起動に失敗したりします。導入後はjava -versionの3行目でVMの種別を、/usr/libexec/java_home -Vでインストール済みのJDK一覧を確認できます。

JVMとDockerなどの仮想化は何が違いますか?

ハイパーバイザー型の仮想マシンはOSごと仮想化し、コンテナはOSのカーネルを共有してプロセスを隔離します。JVMが仮想化しているのはOSでもプロセス環境でもなく、命令セットです。層が違うため排他的な関係ではなく、コンテナの中でJVMを動かす構成が現在では一般的です。

Javaの最新バージョンはどれですか?

OpenJDKのJDKプロジェクトは半年ごとにフィーチャーリリースを出しており、プロジェクトページの一覧によればJava 27が2026年9月15日にGAを迎えています。OracleはLTSを2年ごとに提供する方針で、直近の対象はJava 25(2025年9月16日GA)です。実際のサポート期間や提供条件は配布元ごとに確認してください。OracleのJava SE Support Roadmapでは、非LTSのJava 27のPremier Supportが2027年3月までなのに対し、Java 25は2030年9月までとされています。本番環境の選定では、GAの新しさよりこの期間のほうが判断材料になります。

バイトコードの中身を見ることはできますか?

JDK同梱のjavapで確認できます。javap -cで逆アセンブルした命令列、javap -vで定数プールやメジャーバージョンを含む詳細が表示されます。生成されたバイトコードを読むと、文字列連結やラムダ式がどの命令に変換されているかが分かります。

関連記事

お気に入りに入れた記事の一覧

資料請求

RELATED POSTS 関連記事

目次