メインコンテンツへスキップ
  1. Posts/

[Java] Javaはなぜ一度コンパイルすれば複数のOSで実行できるのか? JVM、GC、JIT、JDK/JRE

·10 分
NineKoo9
著者
NineKoo9
目次

このドキュメントは疑問に思っていた点をAIエージェントとの対話を通じて整理したメモです。JavaがプラットフォームIndependenceを得る方法と、JVM、GC、JIT、JDK/JREの役割を一つの流れで整理します。

Javaはなぜ一度コンパイルすれば複数のOSで実行できるのか?
#


1. JavaがJVMで得るメリット
#

“Write Once, Run Anywhere”
#

Javaのソースコードをコンパイルすると.classファイル(バイトコード)が生成される。
このバイトコードは特定のOS/CPUに依存しない中間言語だ。

Javaソースコード(.java)
    ↓ javacコンパイル
バイトコード(.class)  ← OSに依存しない中間言語
    ↓ JVM実行
各OSで動作

2. 「JVMもOSに合わせてインストールが必要じゃないの?」→ そうだ!
#

ここで核心となる概念が出てくる。

役割誰が負担する?
OS依存性の処理JVM(JVMがOS別に別々に存在する)
OSを意識する主体開発者 → JVMへ移転

つまり、開発者がOSを意識していた負担をJVMの開発会社(Oracleなど)が代わりに引き受ける構造だ。

[Windows JVM]  ┐
[Mac JVM]      ├──── 同じ.classバイトコードを実行
[Linux JVM]    ┘

シンプルな例のレベルでは.classファイル1つを持ち歩くと考えてもよい。
ただし実際のデプロイでは互換Javaバージョン、依存ライブラリ、ネイティブモジュールの有無も合わせる必要がある。
つまり「OS独立性」は大きく得られるが、「どんな環境でも必ず動く」まで意味するわけではない。


3. Python、Cは? そのまま実行できないの?
#

Cの場合 — プラットフォームごとに再コンパイルが必要
#

同じCコードでも:
  → Windowsでコンパイル → Windows実行ファイル(.exe)  → Macでは実行不可
  → Macでコンパイル    → Mac実行ファイル            → Windowsでは実行不可

Cはソースコードを各OS/CPU向けの機械語に直接変換するため、
デプロイ時にプラットフォームごとに別々にビルドする必要がある。
オープンソースプログラムで「Windowsバージョン / Macバージョン」を別々に提供する理由がこれだ。

Pythonの場合 — インタープリター/ランタイムが実行を担当
#

Pythonソースコード(.py)
    ↓ Pythonの実装(CPythonなど)
Pythonバイトコード(.pycなど、内部キャッシュ)
    ↓ インタープリター実行
各OSで動作

Pythonもプラットフォーム別のインタープリターをインストールする必要があるという点ではJavaと似ている。
しかし構造が「ほぼ同じ」とは言いにくい。

  • CPythonはソースを内部的にバイトコードにコンパイルしてから実行する。
  • このバイトコードはCPythonの実装とバージョンに依存する内部表現に近い。
  • 一方JavaのJVMバイトコードはJVM仕様によって標準化された配布フォーマットだ。

そのためPythonは通常ソースコード(.py)やwheelのようなパッケージで配布し、
Javaは.class/.jarのようなJVMバイトコードの成果物を配布することが多い。

言語別の比較まとめ
#

配布単位OS依存性の処理特徴
Cプラットフォーム別実行ファイル開発者が直接行う最も高速、移植性が低い
Javaバイトコード(.class)JVMが処理移植性が高い、JVMのインストールが必要
Pythonソースコード(.py)、パッケージインタープリター/ランタイムが処理便利だが実装ごとに差異がある、インタープリターのインストールが必要

結局**「OS依存性を誰が処理するか」の違い**だ。
Javaの革新は「開発者がプラットフォームを意識しなくてよいように、その責任をJVMに移したこと」だ。


4. JVMの革新
#

プラットフォーム独立性が最も有名だが、JVMがもたらした革新は複数ある。


革新1. プラットフォーム独立性(最も有名)
#

“Write Once, Run Anywhere”
上で説明したものと同じ。


革新2. 自動メモリ管理(Garbage Collection)
#

C/C++開発者が最も苦労していた一つがメモリを直接管理しなければならないことだった。

// Cでは開発者が直接解放する必要がある
int* arr = malloc(100);
// ... 使った後に
free(arr);  // これを忘れると? → メモリリーク(Memory Leak)

JVMは**GC(Garbage Collector)**が使われていないメモリを自動的に回収する。
開発者がfree()のようなものを意識する必要がなくなる。
生産性と安定性の面で非常に大きな革新だった。


革新3. バイトコード検証と歴史的なサンドボックスモデル
#

バイトコードはJVMの上でのみ実行されるため、JVMはロード/リンクの過程でclassファイルを検証できる。

classファイルのロード
    ↓ 形式チェック + バイトコード検証
型安全性/基本的な整合性の確認
    ↓ 実行

この検証プロセスは不正なバイトコード、不正な型の使用、スタックアンダーフロー/オーバーフローのような問題を減らす上で重要だった。
つまり、Javaセキュリティの一つの柱は「JVMがどんなバイトコードでもそのまま実行するわけではない」という点だ。

初期のJavaにはこれに加えてSecurity Managerベースのサンドボックスがあり、
Appletのようなリモートコードを制限された権限で実行できた。

ただしこれは歴史的な文脈として理解するのが正しい。

  • Java 9(2017): Applet API deprecated
  • JDK 11(2018): ブラウザプラグイン、Applet Viewer、Java Web Start削除
  • Java 17(2021): Applet API / Security Manager 削除予定(deprecated for removal)
  • JDK 24(2025): Security Manager 永続的に無効化

そのため今日のJavaを説明する際に**「サンドボックスが核心」と言うと、やや昔の説明に近くなる。
現在も重要なのは
バイトコード検証、型安全性、メモリ安全性**の方だ。


革新4. JITコンパイル(Just-In-Time)
#

初期のJVMはバイトコードを1行ずつ解釈していたため遅かったが、後にJITコンパイラーが導入された。

バイトコード
    ↓ インタープリターで実行 + プロファイリング
ホットコードを検出
    ↓ JITが機械語にコンパイル
その後より高速に実行

「頻繁に実行されるコードは実行中に機械語に最適化しよう」というアイデアだ。
現代のJVMはここにtiered compilationのような技法も使う。
おかげでJavaは多くのワークロードで高いパフォーマンスを出せるようになった。
つまり移植性 + ランタイム最適化を両立させた。


革新5. JVM自体がエコシステムになった
#

JVMという実行環境が標準化されたことで、Java以外の他の言語もJVM上で動くように作られ始めた。

Kotlin   ┐
Scala    ├──── すべてJVMの上で実行される(Javaのエコシステムをそのまま活用)
Groovy   ┘

特にKotlin/JVMはJavaとの高レベルな相互運用性を提供するため、Javaライブラリをそのまま活用できる。
JVMが一つの汎用実行プラットフォームになったわけだ。

ただしAndroidについては一点区別が必要だ。

  • GoogleはKotlinをAndroid開発で公式サポートしている。
  • しかしAndroidアプリの最終ランタイムは**標準JVMではなくART(Android Runtime)**だ。
  • ビルドの過程でJava/Kotlinの.classバイトコードはDEXバイトコードに変換される。

つまり「KotlinがAndroidでJVMの上でそのまま実行される」という言い方は厳密には正確ではない。
それでもJavaとの相互運用性 + 既存エコシステムの再利用性が非常に大きいことは確かだ。


革新の優先度まとめ
#

順位革新一行サマリー
プラットフォーム独立性開発者がOSを意識しなくてよくなった
GC(自動メモリ管理)メモリバグから解放
JVMのエコシステム化Kotlinなど他の言語もJavaの資産を活用
バイトコード検証/歴史的サンドボックス型安全性と制限実行モデル
JITコンパイル移植性とランタイムパフォーマンスの最適化

プラットフォーム独立性が最も有名で象徴的な革新なのは確かだが、
実際の業務でJavaを選ぶ理由はGC + 成熟したエコシステム + JITパフォーマンスが複合的に作用する。


5. JVMのインストール方法
#

brew install openjdk@21をすると何がインストールされるか?
#

openjdk@21
└── JDK
    ├── JVM                       ← バイトコード実行エンジン
    ├── 標準クラスライブラリ/ランタイム構成要素
    └── 開発ツール                ← javac、javadoc、jdbなど

Homebrewのopenjdk@21JDK 21の配布版だ。
昔の資料のようにJVM / JRE / JDKを別々のボックスで分けて説明するのは、概念の区別としては有効だ。

しかし現在の基準では注意が必要だ。

  • JDKはJVMを含む上位概念だ。
  • JREはJVM + 実行用ライブラリという伝統的な概念だ。
  • しかしJDK 11(2018)以降は別途JREイメージがなくなった。

そのためopenjdk@21をインストールする際は
**「JDKの中にJVMと実行構成要素、開発ツールが一緒に入ってくる」**と理解する方が正確だ。

brewでインストールすること自体は正しい。
JVMはOSにデフォルトでインストールされておらず、通常はJDK配布版をインストールして一緒に取得する。


「OS別にインストールされる」という言葉の意味
#

brewでインストールする際に内部的に自分のOS/アーキテクチャに合ったバイナリをダウンロードする。

brew install openjdk@21
  → Mac(ARM)用OpenJDKバイナリをダウンロード
  → Linuxなら Linux用バイナリをダウンロード

JVM自体がOS別に異なって作られており、ユーザーは自分のOSに合ったものをインストールする。
このプロセスをbrewが自動的に処理してくれる。

ただし、ここでも表現を正確にする必要がある。

  • HomebrewはJavaでは公式にmacOSとLinuxで使うパッケージマネージャーだ。
  • そのためこの文脈でのbrew install openjdk@21Mac/Linux基準の説明だ。
  • Windowsは通常別のインストール経路を使う。

全体フローのまとめ
#

[Oracle / OpenJDKの開発チーム]
  → Mac用JVMのビルド
  → Windows用JVMのビルド       ← JVM自体はOS別に存在する
  → Linux用JVMのビルド

[開発者]
  → brew install openjdk@21  ← Mac/Linuxで自分の環境に合ったJDKをインストール
  → javac Hello.java          ← バイトコードを生成
  → java Hello                ← インストールされたJVMが実行

**「JVMがOS依存性を処理してくれる」**という言葉は、
JVMの開発チームがOS別にJVMを別々に作って配布し、その手間を開発者の代わりに引き受けていることを意味する。
開発者はその実装をインストールした後、共通のバイトコードを乗せて実行するということだ。

つまり以降は.class/.jarOSごとに再コンパイルされる必要はないが
引き続き**Javaバージョンの互換性、ライブラリの依存関係、デプロイ方式(jar、jlink runtime、containerなど)**は別途意識する必要がある。

参考資料
#