Java 入門與 JVM:一次編寫,到處執行
從 javac 與 java 兩道指令出發,看懂位元碼、JVM 與靜態強型別,理解 Java 為何介於 C++ 與 Python 之間、又為何被企業愛用三十年。
把同一份程式丟進筆電、伺服器、手機,全都跑得起來
想像一個情境:你寫了一支計算成績平均的小工具,組員用 Windows、助教用 Mac、學校伺服器跑 Linux。如果是 C++,你得在三種平台各自重新編譯一次,還要祈禱沒踩到平台差異;如果是 Python,得確保每台機器都裝對版本的直譯器和套件。而 Java 給了一個讓企業愛了三十年的承諾:Write Once, Run Anywhere(一次編寫,到處執行)——你只編譯一次,產出的成品丟到任何裝有 Java 的機器都能跑。
這篇文章不打算把 Python 篇的內容換個語法重講一遍。如果你已經讀過本專區的 Python 篇與計算機概論概念篇,這裡會聚焦在 Java 真正不一樣的地方:那顆叫做 JVM 的虛擬機器、嚴格的靜態強型別、以類別(class)為核心的設計,以及為什麼銀行、電信、電商後台清一色用 Java。我們會一路寫程式、編譯、執行,親手感受這些差異。

從一份原始碼到能跑的程式:多了一道「編譯」
Python 是直譯式語言:你寫好 .py,直接 python hello.py,直譯器一行一行邊讀邊跑。Java 不一樣,它在「寫程式」和「執行」之間,多插了一道編譯(compile)步驟。
先看最小的一支 Java 程式。請開一個檔案,檔名一定要是 Hello.java(稍後會解釋為什麼):
public class Hello {
public static void main(String[] args) {
System.out.println("你好,Java!");
}
}
接著在終端機做兩件事:
javac Hello.java # 編譯:產出 Hello.class
java Hello # 執行:跑 Hello.class
執行後會看到:
你好,Java!
注意這裡出現了兩個指令,這正是 Java 與 Python 在工具鏈上的根本差異:
javac(Java Compiler,Java 編譯器):把人讀的原始碼Hello.java翻譯成一個Hello.class檔。java(Java Launcher,啟動器):載入.class並實際執行。
那個 Hello.class 不是給人看的,它裝的是位元碼(bytecode)。如果你好奇它長什麼樣,可以用 javap -c Hello 反組譯看看,會出現一堆像 getstatic、ldc、invokevirtual 的指令。這就是 Java 的中間產物。
位元碼與 JVM:「一次編寫,到處執行」的真相
這裡是 Java 最關鍵的設計,也是它和 C++ 分道揚鑣的地方。
C++ 的編譯器會把原始碼直接翻成特定 CPU 與作業系統的機器碼。在 Windows x86 編出來的執行檔,搬到 Mac 的 ARM 晶片上完全跑不動,因為機器碼是綁死平台的。
Java 多繞了一層。javac 不直接產生機器碼,而是產生位元碼——一種介於原始碼與機器碼之間、不綁任何特定 CPU 的「中間語言」。真正負責把位元碼變成當下這台機器能跑的動作的,是 JVM(Java Virtual Machine,Java 虛擬機器)。
Hello.java
│ javac(編譯,只做一次)
▼
Hello.class(位元碼,跨平台)
│ java(啟動 JVM)
▼
┌─────────────────────────────┐
│ JVM(每個平台各有一套) │
│ Windows JVM / Mac JVM / ... │
└─────────────────────────────┘
│
▼
各平台的機器碼 → CPU 執行
關鍵在於:位元碼只有一份,JVM 有很多種。Oracle、各家廠商針對 Windows、macOS、Linux、x86、ARM 各自實作了對應的 JVM。同一個 Hello.class,丟到任何裝有 JVM 的機器,由那台機器的 JVM 負責「翻譯落地」。所以「一次編寫,到處執行」這句話完整來說是:一次編譯成位元碼,靠各平台的 JVM 到處執行。
對照三種語言的定位,差異就清楚了:
| C++ | Python | Java | |
|---|---|---|---|
| 執行方式 | 編譯成機器碼 | 直譯器逐行跑 | 編譯成位元碼,JVM 執行 |
| 跨平台 | 每平台重新編譯 | 靠直譯器抹平 | 位元碼一份,JVM 抹平 |
| 速度 | 最快(貼近硬體) | 較慢 | 接近 C++(靠 JIT,下面會談) |
| 型別檢查 | 編譯期 | 執行期 | 編譯期(強型別) |
可以說 Java 站在 C++ 與 Python 中間:它要 C++ 等級的執行效率與型別安全,又要 Python 等級的跨平台便利。代價是多了一道編譯,以及需要先裝 JVM。
class 與 main:一切都得包在類別裡
回頭看那支 Hello,逐行拆解,會看到 Java 與 Python 在「程式長相」上的第二個大差異。
public class Hello { // 一切都得放進某個 class
public static void main(String[] args) { // 程式的進入點
System.out.println("你好,Java!");
}
}
public class Hello:Java 是 OOP-first(物件導向優先)的語言。在 Python 你可以寫一個沒有任何 class 的腳本;但在 Java,所有程式碼都必須住在某個 class 裡,連最簡單的 Hello World 也不例外。- 檔名規定:一個
.java檔裡的public類別,名稱必須與檔名完全一致。public class Hello就一定要存成Hello.java,連大小寫都不能錯。這是初學者第一個常踩的雷。 public static void main(String[] args):這是程式的進入點(entry point)。當你執行java Hello,JVM 會去找這個固定簽章的方法開始跑。這串字每個字都有意義:public(外界可呼叫)、static(不需先建立物件就能呼叫)、void(不回傳值)、main(固定名稱)、String[] args(接收命令列參數)。簽章寫錯一個字,程式就找不到進入點。System.out.println(...):印出一行字。相當於 Python 的print(),但你看得出它是「呼叫System裡out物件的println方法」——非常物件導向的長相。
相較於 Python 三行就能跑的腳本,Java 的「儀式感」確實重。但這份結構性,正是它在大型團隊協作時的優勢來源。
靜態強型別:在還沒執行前就抓出錯誤
Java 與 Python 第三個根本差異是型別系統。Python 是動態型別,變數的型別等到執行時才確定;Java 是靜態強型別(statically strongly typed),每個變數宣告時就要寫清楚型別,編譯器會在執行前替你檢查。
public class Types {
public static void main(String[] args) {
int age = 20; // 整數,必須宣告型別
double score = 87.5; // 浮點數
String name = "小明"; // 字串
boolean passed = true; // 布林值
// age = "二十"; // ← 這行若解除註解,編譯期就直接報錯!
System.out.println(name + " 今年 " + age + " 歲,分數 " + score);
}
}
輸出:
小明 今年 20 歲,分數 87.5
注意被註解掉的 age = "二十";:在 Python 裡你可以隨時把字串塞進原本放整數的變數,直到執行到那行炸掉才知道錯了。但在 Java,這種型別不合的指派根本通不過 javac——程式連產出 .class 的機會都沒有。
這就是靜態型別的價值:很多錯誤被攔在編譯期,而不是上線後才在使用者面前爆炸。對一個十人團隊維護的數十萬行系統來說,這道「編譯期防線」省下的除錯時間是巨大的。代價是你得多打很多字、彈性比 Python 低。這是一種刻意的取捨。
動手寫一段:成績分級小工具
把前面學到的東西串起來,寫一支完整可執行的程式:讀入一組分數,算平均,並判斷等第。請存成 Grades.java:
public class Grades {
public static void main(String[] args) {
int[] scores = {88, 72, 95, 60, 45}; // 陣列,型別固定為 int
int sum = 0;
for (int s : scores) { // for-each:走訪每個元素
sum += s;
}
double average = (double) sum / scores.length; // 強制轉型避免整數除法
System.out.println("共有 " + scores.length + " 筆成績");
System.out.println("平均:" + average);
for (int s : scores) {
System.out.println(s + " 分 → " + grade(s));
}
}
// 自訂方法:把分數轉成等第
static String grade(int score) {
if (score >= 90) return "A";
else if (score >= 80) return "B";
else if (score >= 70) return "C";
else if (score >= 60) return "D";
else return "F";
}
}
執行 javac Grades.java 後 java Grades,預期輸出:
// 輸出:
// 共有 5 筆成績
// 平均:72.0
// 88 分 → B
// 72 分 → C
// 95 分 → A
// 60 分 → D
// 45 分 → F
這支程式裡藏了一個很多人會踩的雷:(double) sum / scores.length。如果寫成 sum / scores.length,兩個都是 int,Java 會做整數除法,$360 / 5 = 72$ 看起來剛好沒事,但若是 $361 / 5$ 就會得到 72 而非 72.2——小數直接被無條件捨去。先用 (double) 把其中一個轉成浮點數,整個運算才會走浮點除法。這類「型別決定運算行為」的細節,正是靜態型別語言要特別留意的地方。
自動記憶體回收:不用像 C++ 那樣手動還記憶體
Java 還繼承了一個讓它比 C++ 好寫的特性:自動記憶體回收(Garbage Collection,GC)。
在 C++,你 new 出來的物件得自己記得 delete,忘了就記憶體洩漏,重複刪除或用到已釋放的記憶體則直接當掉。Java 把這件苦差事交給 JVM:你只管 new 出物件,當沒有任何變數再指向它時,垃圾回收器會自動把記憶體收回,你完全不必手動釋放。
public class Memory {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
String temp = "暫時物件 " + i; // 每圈都 new 一個新字串
System.out.println(temp);
}
// 迴圈結束後,那些 temp 物件沒人引用了,
// JVM 的 GC 會在適當時機自動回收,不需要你手動處理。
}
}
這一點上 Java 和 Python 站在同一邊(Python 也有自動記憶體管理),都比 C++ 友善。差別在於 Java 的 GC 是高度工程化的,有多種演算法可選,能在企業級的大流量服務下穩定運作——這也是下一節的伏筆。
為什麼企業愛用 Java
把上面的特性收攏起來,就能理解為什麼銀行核心系統、電信帳務、電商後台、Android App 大量採用 Java:
- 跨平台部署省心:開發機、測試機、正式機平台不同也不必擔心,位元碼一份到處跑,JVM 抹平差異。
- 強型別 + 編譯期檢查:大型團隊、長壽命專案最怕「改一處壞十處」。靜態型別讓 IDE 能精準重構、提早抓錯,維護成本大幅下降。
- 穩定與向後相容:二十年前的 Java 程式,很多到今天仍能跑。企業系統動輒維護十幾年,這種穩定性是金錢買不到的安全感。
- 成熟的生態系:Spring(後端框架)、Maven/Gradle(建置工具)、龐大的函式庫與工具鏈,幾乎任何企業需求都有現成方案。
- 效能足夠且可預測:靠 JIT(下節詳述),執行效率接近 C++,又有成熟的 GC 與監控工具,能撐住高併發。
- 人才庫龐大:Java 教學資源、會 Java 的工程師都非常多,企業招募與交接相對容易。
簡言之,Java 不見得是寫起來最快、最炫的語言,但它是最適合「很多人、寫很久、不能出事」的軟體的語言。這正是企業情境的核心需求。
常見錯誤(初學者最容易踩的雷)
- 檔名與 public 類別名不一致:
public class Hello就必須存成Hello.java,大小寫也要一致,否則javac直接報錯。這是新手最常見的第一個坑。 - 忘了先
javac就java:Java 不像 Python 可以直接執行原始碼。你必須先javac Foo.java產出.class,才能java Foo。而且java後面接的是類別名Foo,不是檔名Foo.class。 - 整數除法吃掉小數:
int / int的結果仍是int。算平均、比例時記得至少把一邊轉成double。 main簽章打錯:public static void main(String[] args)少一個字、型別寫錯,JVM 就找不到進入點,會丟出找不到 main 方法的錯誤。- 以為 Java 可以沒有 class:所有程式碼都得包在某個 class 裡,這跟 Python 能寫純腳本不同。
重點回顧
- Java 在「寫程式」與「執行」之間多了編譯成位元碼這一步:
javac編譯、java執行。 - 位元碼跨平台、JVM 各平台一套,這才是「一次編寫,到處執行」的完整含義。
- Java 是 OOP-first、靜態強型別、自動記憶體回收,定位介於 C++(快、貼硬體)與 Python(彈性、好寫)之間。
- 企業愛用的根本原因:跨平台、編譯期防線、長期穩定、生態成熟——適合「多人、長期、高可靠」的系統。
深入探討(研究所視角)
到目前為止我們把 JVM 當黑盒子。研究所層級該打開它,看看那層「虛擬機器」內部到底發生什麼事。
JVM 架構:類別載入、執行引擎與記憶體區域
JVM 不是單純的「翻譯機」,它是一個完整的執行環境,大致由三部分組成:
-
類別載入子系統(Class Loader Subsystem):負責在執行期把
.class載入記憶體,並完成載入(loading)→ 連結(linking)→ 初始化(initialization)三階段。連結階段又細分為驗證(verification)、準備(preparation)、解析(resolution)。值得注意的是 Java 採用雙親委派模型(parent delegation model):類別載入請求會先往上委派給父載入器,避免核心類別(如java.lang.String)被惡意覆寫。 -
執行期資料區(Runtime Data Areas):JVM 把記憶體切成幾塊。其中堆積(Heap)存放所有物件實例,是 GC 的主要作用範圍,通常再分為新生代(Young Generation)與老年代(Old Generation),以配合分代回收(generational GC)假設「多數物件很快就死」。JVM 堆疊(Stack)則是每個執行緒一份,存放方法呼叫的區域變數與運算過程。理解這個劃分,才能解釋
StackOverflowError(堆疊爆掉,常因無窮遞迴)與OutOfMemoryError(堆積爆掉)的本質差異。 -
執行引擎(Execution Engine):真正執行位元碼的核心,內含直譯器、JIT 編譯器與垃圾回收器。
位元碼驗證:在執行前確保安全
前面提到連結階段有個驗證(bytecode verification)步驟,這是 JVM 安全模型的基石,也是 C++ 沒有的環節。
位元碼可能來自不可信來源(例如網路下載),如果直接執行,惡意或損壞的位元碼可能讓堆疊溢位、跳到非法位址、把整數當指標用,破壞 JVM 的記憶體安全。驗證器會在類別載入時,對位元碼做一連串靜態檢查:型別是否一致、運算元堆疊不會上溢或下溢、跳躍目標合法、區域變數使用前已初始化等等。實作上常以資料流分析(data-flow analysis)推導每個指令處運算元堆疊與區域變數的型別狀態,確認在所有控制流路徑上都自洽。通過驗證的位元碼才被允許執行——這讓「執行不可信程式碼」(如早年的 Applet、今日的沙箱環境)在型別安全上有了保障。
JIT 即時編譯:為什麼 Java 能跑得幾乎和 C++ 一樣快
如果 JVM 只是逐條直譯位元碼,效能會明顯輸給 C++。Java 之所以能逼近原生效能,靠的是 JIT(Just-In-Time,即時編譯)。
JVM 採用混合執行策略。程式剛啟動時,執行引擎先用直譯器逐條跑位元碼,啟動快但執行慢。同時 JVM 會剖析(profiling)程式的執行行為,統計哪些方法、哪些迴圈被反覆執行——這些就是所謂的熱點(hotspot)(HotSpot 也正是主流 JVM 的名字)。當某段程式碼夠熱,JIT 就把它的位元碼即時編譯成當下這台機器的原生機器碼並快取起來,之後再呼叫就直接跑原生碼,速度大幅提升。
JIT 還能做出靜態編譯器難以企及的剖析導向最佳化(profile-guided optimization),因為它握有真實的執行期資訊:
- 方法內聯(inlining):把頻繁呼叫的小方法直接展開到呼叫處,省去呼叫開銷。
- 去虛擬化(devirtualization):根據執行期觀察,把多型呼叫推測性地特化為直接呼叫。
- 逃逸分析(escape analysis):若分析出某物件不會逃出方法範圍,可改在堆疊配置甚至完全消除配置,減輕 GC 壓力。
這帶來一個 Java 特有的現象:程式剛啟動時較慢(仍在直譯與剖析),跑一陣子「熱起來」後才達到巔峰效能。這也是為什麼 Java 服務常被詬病「冷啟動慢」,以及近年 GraalVM 的 AOT(Ahead-of-Time,提前編譯)原生映像會成為微服務領域的熱門解法——它用犧牲部分峰值最佳化的代價,換取近乎瞬間的啟動。
把這三件事疊起來看,JVM 的設計哲學就完整了:位元碼驗證守住安全、類別載入與記憶體區域管理生命週期、JIT 加上 GC 在執行期動態壓榨效能。Java 不是用「貼近硬體」去換速度(那是 C++ 的路),而是用一層精密的虛擬機器,在跨平台、安全、效能與可維護性之間取得了那個讓企業買單三十年的平衡點。