はじめに
祝! Java 25 リリース
先日(2025年9月16日),Java 25 がリリースされたことは,Java を愛する皆々様ならご存知のことかと思います。
特に,このバージョンは Java 21 に続く 2 年ぶりの長期サポート(LTS;Long-Term Support)版であり,企業利用においても安心して使えるバージョンとして今後の普及が見込まれています。
前バージョンとの比較や注目の点に関しては,Qiita の方に良い記事がありましたので,そちらをご参照ください。
Java の言語仕様と JVM の仕組みは公開されている
ところで,Java のあらゆる言語仕様や Java 仮想マシン(JVM) の仕組みは,JEP(JDK Enhancement Proposal)や JSR(Java Specification Request)などの提案を通じて議論され,最終的に OpenJDK のプロジェクトに取り込まれます。
これらの提案や議論は,OpenJDK の JDK Wiki や JEP Index で広く公開されており,誰でも閲覧できます。
さらに,Java の言語仕様や JVM の仕様は Oracle の The Java® Language Specification や The Java® Virtual Machine Specification で詳細に定義されており,またこれも誰でも閲覧できるようになっています。 つまり,Java の言語仕様や JVM の仕組みは,オープンなプロセスを通じて進化しており,誰でもその詳細を学んで理解できるようになっているのです。
その理解度を指し示す最も有効な指標の一つが,JVM を自作できるかどうかです。 もし,あなたが Java の言語仕様や JVM の仕組みを深く理解しているならば,JVM を自作することも可能でしょう。 しかしながら,実際に JVM を自作しようとすると,その膨大な情報量と複雑な内容に圧倒されてしまうことでしょう。
自作 JVM は未だに敷居が高い
Java の言語仕様や JVM の仕組みを,これら仕様書から深く理解しようとすると,その膨大な情報量と複雑な内容に圧倒されてしまうことでしょう。 特に,JVM の仕様書は非常に長大で難解な内容が多く含まれており,初心者にとっては大変敷居が高いものとなっています。
しかも,Java の言語仕様や JVM の仕組みを学ぶためのリソースは非常に限られており,またそれらも必ずしも初心者にとって分かりやすいものではありません。例えば,C コンパイラや LLVM を自作するためのチュートリアルや健忘録は,軽く調べただけでも大量に見つかりますが,JVM を自作しようという人はそう多くはないようです。
そのため,私はこれまで JVM を読む というシリーズで,JVM の仕様書を読み解くためのガイドを提供してきました(現在も連載中です)。しかしながら,いざ自分で JVM を実装しようとすると,仕様書を読むだけでは不十分であり,実際に手を動かして試行錯誤することが不可欠だということに気がつきました。
私自身も JVM を自作する過程で多くの困難に直面しました。
この記事では,私のように JVM を自作したい・自作している方々に向けて,JVM の自作に役立つ知識やリソースを共有したいと思います。
JVM を自作する理由
この記事を読む方々が,必ずしも JVM を自作したいと思っているわけではないかもしれません。しかしながら,やはり,やってみると JVM の自作は大変たのしいもので,そのうえ多くの学びをついでに得られるという,一石二鳥どころか一石三鳥・四鳥のような経験ができることは間違いありません。
なお,この記事は JVM を自作するためのチュートリアルではなく,あくまで JVM を自作することの意義や,そのために必要な知識やリソースを紹介することを目的としています。 そのため,具体的な実装方法やコード例については,(極力紹介しますが)あまり詳しくは触れません。
30 億のデバイスで走る JVM
Oracle は 2010年頃から,Java の宣伝文句として「30 億のデバイスで走る Java」などと謳っています。Java と言っていますが,これはもちろん JVM のことを指します。
この 30 億台のデバイスには,もちろん皆様のコンピュータや Android デバイスも含まれていますが,実はそれ以外にも,家電製品や組み込みシステム,IoT デバイスなども含まれています。
例えば,皆様がお持ちでしょうか,マイナンバー・カードや住民基本台帳カード(一部),エストニア国民 IDカードなどは,Java Card という,JVM のサブセットを実装したスマートカードで動作する Java プラットフォームを利用しています。 これらのカードは,セキュリティが非常に重要なデバイスであり,Java Card はそのために特別に設計された JVM の一種です。
さらに,かつて存在した Java Ring という,指輪型のウェアラブル・デバイスも,JVM を搭載していました。
あらゆるデバイスで走る JVM
地球上に存在するあらゆるチューリング完全なデバイスは,理論的に JVM を実装できる可能性があります。もし,将来あなたがハッキーな謎デバイスで DOOM の代わりに Minecraft を動かしたいと思ったとき,JVM を自作できるスキルがあれば,それを実現するための大きな一歩を踏み出せることでしょう。
特に,近年の DOOM 移植ブームにおいては,最初にそのデバイスで RISC-V アーキテクチャを実装し,RISC-V 上で動作する Linux を移植したあとに,DOOM を動作させるという手順が一般的です。 そのため,JVM を自作できるスキルは,Minecraft の移植にもきっと役立つことだと思います。
学びの多い JVM
真面目な話をすると,JVM を自作することは,コンピュータ・サイエンスの多くの重要な概念を学ぶための素晴らしい方法です。 例えば,メモリ管理,ガベージ・コレクション,スレッドと同期,バイトコードの解釈と最適化などなど,JVM の実装には多くの興味深いトピックが含まれています。 これらのトピックは,現代のソフトウェア開発において非常に重要なものであり,JVM を自作することでそれらを深く理解できることは,もはや言うまでもありません。
JVM は「仮想マシン」と呼ばれている通り,JVM は一種の小さなコンピュータなのです。 JVM を自作することを,例えば CPU のエミュレータを自作することと同じように捉えると,その楽しさが少しは伝わるかもしれません。命令や概念もユニークなものが多く,例えば,フレームやクラス・ローダ,モニタ機能などは,通常の CPU には存在しない JVM 独自の概念です。もし,あなたが CPU をエミュレートしたあとに,なにかか新しいことをしたいと思ったとき,JVM の自作はその次のステップとして最適な選択肢となるでしょう。
さらに,前述の通り JVM にはスレッドを用いたスケジューリング機能やヒープの管理機能があります。もし,あなたがいつか OS を自作したいと思ったとき,JVM の自作はそのための良い準備運動なのです。
ただの CPU エミュレータではない JVM
JVM は,単なる CPU エミュレータとは異なり,独自の工夫が随所に盛り込まれています。
例えば, x86 の call
命令のオペランドは,呼び出す関数のアドレスを直接指定します。さらに,これはアドレスに対して直接ジャンプするのみで,引数や戻り値に関する手厚いサポートはありません。
一方で JVM の関数呼び出し命令は,invokevirtual
,invokespecial
,invokestatic
,invokeinterface
,invokedynamic
の 5 種類があり,それぞれ異なる方法で関数を呼び出します。さらに,これらには,標準で引数や戻り値の管理,オーバーロードの解決,動的バインディングなどの機能が組み込まれています。
オペランドには,呼び出す関数の名前とシグネチャ(引数と戻り値の型情報)が含まれており,JVM はこれらを使って適切な関数を見つけ出します。
このように,JVM は単なる CPU エミュレータとは異なり,関数呼び出しやメモリ管理などの高レベルな機能を提供しています。 そのため,JVM を自作することは,単なる CPU エミュレータを自作することよりも,さらに多くの学びと挑戦を提供してくれることでしょう。
最初に当たるは究極の選択
JVM を自作するにあたって,最初に選択しなければならないのは,どのレベルで JVM を実装するかということです。
何も知らずに JVM を自作し始めてしまうとと,中盤で後述の理由によって挫折してしまうことでしょう(私のように)。 そのため,自分が目指すべきゴールを最初に明確にすることが,自作 JVM の成功の鍵となります。
JVM の実装レベルには,大きく分けて以下の 4 つの選択肢があります。 これらの選択肢にはそれぞれ利点と欠点があり,また実装の難易度も大きく異なります。
- 命令を解釈して実行する超簡単な JVM:緑
- 基本的な機能を備えたミニマルな JVM:黄
- あらゆる Java プログラムを完全にサポートするフル機能の JVM:紫
- JIT コンパイラなどの高度な機能を備えた高性能な JVM:赤
なお,これらの区分は私が独自に定義したものであり,厳密なものではありません。 また,これらのレベルは互いに排他的なものではなく,例えばミニマルな JVM に JIT コンパイラを追加することも可能です。特に,これらを1から順に段階的に実装していくことをお勧めします。
1. 命令を解釈して実行する超簡単な JVM
最も基本的なレベルの JVM は,JVM バイト・コードを解釈して実行するだけのシンプルな実装です。 このレベルの JVM は,JVM の命令セットを理解し,それらを逐次実行するためのインタプリタを実装することに焦点を当てます。
このレベルの JVM の目標は,Java の基本的な算術演算や変数の読み書きなどの基本的な命令をサポートし,簡単な JVM バイト・コードを実行できるようにすることです。
JVM は,Java のソース・コードを直接実行しているわけではありません。Java のソース・コードは,まずコンパイラによって JVM バイト・コードに変換されます。 そして,JVM はこのバイト・コードを解釈して実行します。つまり,JVM は Java のソース・コードを直接実行するのではなく,それの化身であるバイト・コードを実行しているのです。
バイト・コードは,JVM の命令セットに従って設計された低レベルの命令群であり,Java のソース・コードを効率的に表現しています。 例えば,Java の int
型の変数を加算する場合,Java のソース・コードでは i + j
と書きますが,JVM バイト・コードでは iadd
という命令が使われます。
現在のJVM(Java 25)では,205 種類の命令が定義されていますが,この実装レベルでは,いくつかの必要最低限の命令だけをサポートすれば十分です。
さらに,JVM はスタックベースの仮想マシンであり,命令の実行にはオペランド・スタックというデータ構造を使用します。 例えば,iadd
命令は,オペランド・スタックの上位 2 つの int
型の値を取り出し,それらを加算して,結果を再びスタックに積みます。
簡単な JVM の実装を通して,スタックの実装や操作,命令のデコードと実行などの基本的な概念を学べます。
このレベルの JVM は,Java の基本的な機能を理解するための良い出発点となりますが,実際の Java プログラムを実行するには,まだまだ多くの機能が不足しています。
この段階を乗り越えると,次のような Java プログラムを実行できるようになります。
1 + 2;
3 * 10;
(1 + 2) ^ 3 >> 2;
詳しくは,小記事である「【簡単な】 Java 仮想マシンを自作したい全ての方々へ」をご参照ください。
2. 基本的な機能を備えたミニマルな JVM
基本的な機能を備えたミニマルな JVM は,JVM の命令セットのすべてをサポートし,さらにメソッドの呼び出しや例外処理,少量の VM ネイティブ・メソッドなどの基本的な機能を実装します。
このレベルの JVM は,Java の基本的な機能を理解し,簡単な Java プログラムを実行するための良い基盤となります。
このレベルの JVM の目標は,JVM の命令セットをすべて実装して,メソッドの呼び出しや例外処理などの基本的な機能をサポートすることです。
JVM の命令セットをすべて実装することは,長い JVM の実装の中ではさほど難しいことではありません。命令一つ一つは適度に抽象化されており,あまり複雑ではありません。数も 205 種類と,それほど多くはありません。
x86 や ARM のような CPU アーキテクチャの命令セットと比較すると,JVM の命令セットは非常にシンプルであり,実装も容易です。
しかしながら,JVM の命令セットをすべて実装したとしても,それだけでは実用的な(長く遊べるような) JVM にはなりません。次に考えるべきは,メソッドの呼び出しです。
JVM はメソッド呼び出しを「フレーム」(Frame)というデータ構造を使って管理しています。フレーム内には,ローカル変数やオペランド・スタック,メソッドの引数・戻り値などが格納されます。
JVMは,メソッドが呼び出される度に新しいフレームを作成して,「JVM スタック」(Java Virtual Machine Stack) というスタック構造に積み上げて管理します。各メソッドが呼び出されるたびに新しいフレームがスタックに積まれ,メソッドの実行が(成功・失敗にかかわらず)終了するときにそのフレームがスタックから取り除かれます。
さらに,もしあなたが標準出力を用いてコンソールにメッセージを表示したい場合,System.out.println
メソッドを実装しなければなりません。このメソッドは FileDescriptor
などの標準ライブラリのクラス,ひいてはファイル操作に関するネイティブ・メソッドを呼び出します。そのため,実装すべきメソッドが芋づる式に増えていきます。しかしながら,この段階からそれをすべて実装しようとすると,途方もない労力が必要となってしまい,挫折してしまうかもしれません。
そこで,ここでは必要最低限の VM ネイティブ・メソッドだけを実装することをお勧めします。例えば,System.out.println
メソッドを呼び出すときに,その中での呼び出しを通常の Java メソッドではなく,直接ホスト OS の標準出力に文字列を書き込むようにします(モックするようなイメージです)。これにより,標準ライブラリの多くの機能を実装することなく,簡単にコンソールへの出力を実現できます。
このレベルの JVM を実装することで,Java の基本的な機能を理解し,簡単な Java プログラムを実行できるようになります。
この段階を乗り越えると,次のような Java プログラムを実行できるようになることでしょう。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
詳しくは,小記事である「【ミニマルな】 Java 仮想マシンを自作したい全ての方々へ」をご参照ください。
3. 標準ライブラリを完全にサポートするフル機能の JVM
フル機能の JVM は,あらゆる Java プログラムを完全にサポートするために,JVM の仕様で定義されているすべての機能を実装します。これには,ガベージ・コレクションやスレッド管理,標準ライブラリの完全なサポートなどが含まれます。
このレベルの JVM の目標は,標準ライブラリの完全なサポートを実現し,System.initPhase1()
~ System.initPhase3()
メソッドをエラーなく実行できるようにすることです。
現在の Java プログラムは,OpenJDK の標準ライブラリに大きく依存しています。 例えば,java.util
パッケージのコレクション・フレームワークや,java.io
パッケージの入出力機能,java.net
パッケージのネットワーク機能などが広く使われています。これらの標準ライブラリを完全にサポートすることは,実用的な JVM を実装するために不可欠です。
標準ライブラリの完全なサポートと聞くと,大変な労力が必要に思えるかもしれませんが,実際には VM が提供するべきであるネイティブ・メソッドをすべて実装するだけに過ぎません。
例えば,java.lang.Object
クラスの wait
や notify
メソッド,Field
クラスの get
や set
メソッド,Thread
クラスのスレッド管理メソッドなどが含まれます。 これらのネイティブ・メソッドを実装することで,標準ライブラリの多くの機能を利用できるようになります。
ここで忘れてはいけないのが,System.initPhase1()
~ System.initPhase3()
メソッドの呼び出しです。これらのメソッドは,標準ライブラリの立ち上げに必要な初期化処理を行います。例えば,System
クラスの out
フィールドの初期化や,java.lang.ClassLoader
クラスの初期化などが含まれます。さらには,オブジェクトのファイナライザ・スレッドの起動なども行います。
これらのメソッドを呼び出すことで,標準ライブラリの多くの機能が正しく動作するようになります。
このレベルの JVM では,これらのメソッドがエラーなく実行できるようにすることが目標となるでしょう。
この段階を乗り越えると,あらゆる(ネイティブ・メソッドを必要としないような)Java プログラムを実行できるようになります。
4. JIT コンパイラなどの高度な機能を備えた高性能な JVM
高性能な JVM は,JIT(Just-In-Time)コンパイラやガベージ・コレクション,スレッド管理などの高度な機能を備えています。さらに,ネイティブ・メソッドによる JNI(Java Native Interface)のサポートなども含まれます。
これらの高度な機能を実装することで,Java プログラムのパフォーマンスを大幅に向上させるだけではなく,OpenJDK が提供する JVM と同等の機能を実現できます。
このレベルの JVM の目標は,JIT コンパイラやガベージ・コレクションなどの高度な機能を実装して,レベル 3 の JVM をさらに強化することです。
JIT コンパイラは,JVM バイト・コードをネイティブコードに変換して実行することで,プログラムの実行速度を向上させます。 これにより,Java プログラムはネイティブ・アプリケーションと同等のパフォーマンスを発揮できるようになります。 ガベージ・コレクションは,メモリ管理を自動化することで,メモリリークやダングリング・ポインタなどの問題を防ぎます。 これにより,Java プログラムは安定して動作し,メモリの効率的な利用が可能となります。
前述の System.initPhase1()
メソッドは,ただ愚直に実装しただけでは,その実行に約 5 秒~20秒ほどかかります。 そういった場合には,別途 JIT コンパイラや CDS(Class Data Sharing)などの高度な機能を実装することで,この初期化時間を数百ミリ秒にまで短縮できます。 これにより,Java プログラムの起動時間が大幅に改善されます。
さらに,ネイティブ・メソッドによる JNI のサポートは,C/C++ などのネイティブコードと連携するために不可欠です。System.loadLibrary
メソッドを実装することで,Java プログラムからネイティブ・ライブラリを動的にロードし,その中の関数を呼び出せるようになります。
これには,JEP 454(Foreign Function & Memory API)などの提案を参考にしながら,JNI の仕様を理解し,適切に実装することが求められます。
このレベルの JVM を実装することで,OpenJDK と同等の機能を備えた高性能な JVM を実現できます。
JVM の自作に役立つ知識とリソース
JVM を自作するためには,以下のような知識とリソースが役立ちます。
なお,以下に挙げたものは,私が実際に JVM を自作する過程で役立ったものを中心に選んでいます。 これらのリソースは,JVM の自作に必要な知識を体系的に学ぶための良い出発点となるでしょう。
1. 公式の仕様書・ドキュメント
-
The Java® Virtual Machine Specification:JVM の公式仕様書です。
これは,JVM を自作するための最も重要なリソースの一つです。
バージョンごとに刷新されるため,各バージョンごとの変遷や違いを理解すると,より多彩な実装が可能となります。 -
The Java® Language Specification:Java 言語の公式仕様書です。
一見 Java 言語に限られた内容に思えますが,標準ライブラリに関する記述や,JVM の動作に関する記述も含まれています。
そのため,JVM を自作する際にも役立つことが多々ありました。
上記の公式ドキュメントは,JVM の自作に必要な知識を学ぶための最も重要なリソースです。これらは HTML 版と PDF 版の両方が提供されており,どちらも無料で閲覧できます。
画面で読む場合には HTML 版が便利ですが,紙に印刷して読む場合には PDF 版が適しています。私は後者を好むので,印刷してハンドブックのような形で使っています。
なお,JVM の仕様書は非常に長大であり,かつ英語で書かれています。特に(私のような)英検3級レベルの英語力しかない人間にとっては,読むだけでも一苦労です。
そこで,私は記事シリーズ「JVM を読む」を通じて,JVM の仕様書を読み解くためのガイドを提供しています。 この記事シリーズは,JVM の仕様書を段階的に読み進めるための良い出発点となることを願っています。
2. オープンソースの JVM 実装
-
OpenJDK:Oracle が主導するオープンソースの Java 開発キットです。
OpenJDK には,HotSpot のデファクト・スタンダード実装を含んでおり,そのソースコードは GitHub: openjdk/jdk で公開されています。
実装段階:3 以降を目指す場合には,標準ライブラリとその実装のソースコードを参考にすることが非常に役立ちます。特に,VM が実装するべきネイティブ・メソッド(Object.wait
やClass.forName
など)の詳細な挙動を理解するためには,実際のソース・コードを読んでみることが,一番の近道です。 -
Eclipse OpenJ9:IBM が主導するオープンソースの JVM 実装です。
こちらも,同様にソースコードが GitHub: eclipse/openj9 で公開されています。
OpenJ9 は,HotSpot とは異なる設計思想に基づいており,特に組み込みシステムやクラウド環境での利用を重視しています。 そのため,OpenJ9 のソースコードを読むことで,JVM の他の実装方法や最適化手法を学べます。
3. 有志による JVM 自作プロジェクトとその解説記事
-
Java VM 自作 方法 : igjit さんによる, R 言語での JVM の簡単な実装です。
この記事での分類では,実装段階:2(ミニマルな JVM)に相当します。
Note: Rが好きすぎてRでJVMを実装したnoteエンジニアに話を聞いてみた も参考になります。 -
gojvm: DQNEO さんによる,Go 言語での JVM の実装です。
こちらも,実装段階:2(ミニマルな JVM)に相当します。
特に,この実装はmain.go
という 1 つのファイルにすべてのコードが含まれており,非常に読みやすい構成となっていることが特徴です。 -
PHP で JVM を自作して,HelloWorld を出力してみる: memory(めもりー)さんによる,PHP での JVM の実装です。JVM 自作界隈では名の知れたお方です。皆様も,この記事に辿り着く前に見たことがあるかもしれません。
JVM の仕様書の読み方や,JVM の実装に関する多くの知見が大変詰まっています。もし,あなたが PHP で JVM を実装するつもりが無くても,このスライドは一読の価値があります。
4. The Java Compatibility Kit (JCK)
The Java Compatibility Kit (JCK) は,Java プラットフォームの互換性を検証するためのテスト・スイートです。 これは,JVM が Java SE の仕様に準拠した実装であることを確認するために使用されます。
JCK は,Java の各バージョンごとに提供されており,JVM の自作においても非常に役立ちます。 例えば,JVM の各機能が正しく動作しているかどうかを検証するために,TCK のテスト・ケースを実行してみるといったことが可能です。 これにより,JVM の実装の品質を向上させられます,
ただし,JCK の利用には OpenJDK コミュニティへの申請と承認が必要であり,また商用利用にはライセンス料が発生する場合があります。
個人で学習目的で利用する場合には,大変敷居が高いものとなっています。
おわりに
JVM を自作することは,Java の言語仕様や JVM の仕組みを深く理解するための素晴らしい方法です。 この記事では,JVM を自作する理由や,そのために必要な知識とリソースを紹介しました。 もし,あなたが JVM を自作したいと思っているならば,ぜひこの記事を参考にしてみてください。
そして,JVM を自作する過程で多くの学びと楽しさを得られることを願っています。
この記事が,JVM を自作したい・自作している方々にとって,少しでも役立つことを願っています。 もし,この記事に関して質問やフィードバックがありましたら,ぜひコメント欄でお知らせください。
では,よいバイト・コードライフを!
Views: 0