Java 程式碼經過編譯後產生位元組碼(Bytecode),由 JVM 的執行引擎負責執行。理解位元組碼結構和執行機制,有助於深入理解 Java 的執行效率和效能最佳化。
位元組碼基礎#
Class 檔案結構#
┌─────────────────────────────────────────────────────────────┐
│ Class 檔案結構 │
├─────────────────────────────────────────────────────────────┤
│ 魔數 (Magic Number) │ 0xCAFEBABE │
│ 次版本號 (Minor Version) │ 0 │
│ 主版本號 (Major Version) │ 52 (Java 8) │
├─────────────────────────────────────────────────────────────┤
│ 常數池 (Constant Pool) │
│ ├── 字面量:文字字串、final 常數等 │
│ └── 符號參照:類別/介面名、欄位名、方法名 │
├─────────────────────────────────────────────────────────────┤
│ 存取旗標 (Access Flags) │ public, final, abstract...│
│ 本類別索引 (This Class) │
│ 父類別索引 (Super Class) │
│ 介面索引集合 (Interfaces) │
├─────────────────────────────────────────────────────────────┤
│ 欄位表 (Fields) │ 欄位資訊 │
│ 方法表 (Methods) │ 方法資訊 │
│ 屬性表 (Attributes) │ 附加資訊 │
└─────────────────────────────────────────────────────────────┘使用 javap 反組譯#
# 編譯
javac HelloWorld.java
# 反組譯,顯示位元組碼
javap -c HelloWorld.class
# 顯示詳細資訊(常數池、行號、區域變數等)
javap -v HelloWorld.class
# 顯示私有成員
javap -p HelloWorld.class位元組碼範例#
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}反組譯結果:
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1 // 載入參數 a 到運算元堆疊
1: iload_2 // 載入參數 b 到運算元堆疊
2: iadd // 相加,結果放回堆疊頂端
3: ireturn // 回傳 int 值
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this LCalculator;
0 4 1 a I
0 4 2 b I位元組碼執行過程圖解
執行 add(3, 5):
初始狀態:
區域變數表:[this, 3, 5] 運算元堆疊:[]
iload_1 (載入 slot 1):
區域變數表:[this, 3, 5] 運算元堆疊:[3]
iload_2 (載入 slot 2):
區域變數表:[this, 3, 5] 運算元堆疊:[3, 5]
iadd (相加):
區域變數表:[this, 3, 5] 運算元堆疊:[8]
ireturn (回傳):
回傳值:8常見位元組碼指令#
載入與儲存指令#
載入指令(從區域變數表到運算元堆疊):
├── iload - 載入 int
├── lload - 載入 long
├── fload - 載入 float
├── dload - 載入 double
├── aload - 載入 reference
└── iload_0 ~ iload_3 - 快捷指令
儲存指令(從運算元堆疊到區域變數表):
├── istore - 儲存 int
├── lstore - 儲存 long
├── fstore - 儲存 float
├── dstore - 儲存 double
└── astore - 儲存 reference運算指令#
// 加法
int result = a + b; // iadd
// 除法
int div = a / b; // idiv
// 取餘
int mod = a % b; // irem
// 自增
i++; // iinc 1 by 1型別轉換#
int i = 100;
long l = i; // i2l (int to long)
float f = l; // l2f (long to float)
double d = f; // f2d (float to double)
// 窄化轉換(可能損失精度)
int narrowed = (int) l; // l2i物件操作#
// 建立物件
Object obj = new Object();
// new, dup, invokespecial <init>
// 存取欄位
obj.field = value; // putfield
value = obj.field; // getfield
// 靜態欄位
MyClass.staticField = value; // putstatic
value = MyClass.staticField; // getstatic
// 型別檢查
if (obj instanceof String) { } // instanceof
String str = (String) obj; // checkcast方法呼叫指令#
JVM 提供五種方法呼叫指令:
┌─────────────────────────────────────────────────────────────────────┐
│ 方法呼叫指令 │
├────────────────┬────────────────────────────────────────────────────┤
│ 指令 │ 用途 │
├────────────────┼────────────────────────────────────────────────────┤
│ invokestatic │ 呼叫靜態方法 │
│ invokespecial │ 呼叫建構子、私有方法、super 方法 │
│ invokevirtual │ 呼叫實體方法(虛擬方法分派) │
│ invokeinterface│ 呼叫介面方法 │
│ invokedynamic │ 動態方法呼叫(Lambda、方法參照) │
└────────────────┴────────────────────────────────────────────────────┘方法呼叫範例#
public class MethodCalls {
public static void staticMethod() { } // invokestatic
private void privateMethod() { } // invokespecial
public void virtualMethod() { } // invokevirtual
public void demo() {
staticMethod(); // invokestatic
privateMethod(); // invokespecial
this.virtualMethod(); // invokevirtual
super.toString(); // invokespecial
}
}
interface MyInterface {
void interfaceMethod();
}
class Implementation implements MyInterface {
public void interfaceMethod() { }
}
// 呼叫介面方法
MyInterface mi = new Implementation();
mi.interfaceMethod(); // invokeinterface虛擬方法表(vtable)#
┌─────────────────────────────────────────────────────────────┐
│ 虛擬方法分派 │
├─────────────────────────────────────────────────────────────┤
│ │
│ class Parent { │
│ void method1() { } // vtable[0] │
│ void method2() { } // vtable[1] │
│ } │
│ │
│ class Child extends Parent { │
│ void method1() { } // 覆寫,vtable[0] 指向新實作 │
│ void method3() { } // 新增,vtable[2] │
│ } │
│ │
│ Parent p = new Child(); │
│ p.method1(); // invokevirtual -> 查 vtable[0] -> Child.method1│
│ │
└─────────────────────────────────────────────────────────────┘
invokevirtual和invokeinterface需要在執行時根據物件的實際類型查找方法,這稱為動態分派。而invokestatic和invokespecial在編譯期即可確定呼叫目標,屬於靜態分派。
invokedynamic 與 Lambda#
invokedynamic 原理#
// Lambda 表達式
Runnable r = () -> System.out.println("Hello");
// 編譯後使用 invokedynamic
// 首次執行時,引導方法(bootstrap method)被呼叫
// 引導方法回傳 CallSite,包含實際呼叫目標invokedynamic 執行流程:
1. 首次執行 invokedynamic
↓
2. 呼叫引導方法(Bootstrap Method)
LambdaMetafactory.metafactory()
↓
3. 動態生成實作類別
MethodHandles 綁定目標方法
↓
4. 回傳 CallSite
快取供後續使用
↓
5. 後續執行直接使用 CallSite
無需重複引導Lambda 實作機制#
public class LambdaDemo {
public static void main(String[] args) {
// Lambda 表達式
Runnable r = () -> System.out.println("Hello");
r.run();
}
}// javap -c -p LambdaDemo.class
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return
// Lambda 主體被編譯為私有靜態方法
private static void lambda$main$0();
Code:
0: getstatic #4 // Field java/lang/System.out
3: ldc #5 // String Hello
5: invokevirtual #6 // Method java/io/PrintStream.println
8: return執行引擎#
解譯執行 vs 編譯執行#
┌─────────────────────────────────────────────────────────────────────┐
│ HotSpot 混合執行模式 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ .class 位元組碼 │
│ │ │
│ ├─────────────────────────────────────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌────────────────┐ │
│ │ 解譯器 │ │ JIT 編譯器 │ │
│ │ Interpreter │ │ (C1 / C2) │ │
│ │ │ │ │ │
│ │ 逐條解譯執行 │ 熱點偵測 │ 編譯為機器碼 │ │
│ │ 啟動快 │ ──────────────────> │ 執行快 │ │
│ │ 執行慢 │ 方法呼叫計數器 │ 編譯慢 │ │
│ │ │ 回邊計數器 │ │ │
│ └──────────────┘ └────────────────┘ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 本機機器碼執行 │
│ │
└─────────────────────────────────────────────────────────────────────┘熱點偵測#
# 方法呼叫計數器閾值(Client 模式預設 1500,Server 模式預設 10000)
-XX:CompileThreshold=10000
# 回邊計數器閾值(用於偵測迴圈熱點)
-XX:OnStackReplacePercentage=140
# 禁用 C1 編譯器(只使用 C2)
-XX:-TieredCompilation
# 查看 JIT 編譯過程
-XX:+PrintCompilation分層編譯(Tiered Compilation)#
┌─────────────────────────────────────────────────────────────────────┐
│ 分層編譯層級 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Level 0: 解譯執行 │
│ │ 收集基本執行資訊 │
│ │ │
│ ▼ │
│ Level 1: C1 編譯(無 Profiling) │
│ │ 簡單最佳化,快速編譯 │
│ │ │
│ ▼ │
│ Level 2: C1 編譯(有限 Profiling) │
│ │ 方法呼叫和回邊計數 │
│ │ │
│ ▼ │
│ Level 3: C1 編譯(完整 Profiling) │
│ │ 收集分支、型別等詳細資訊 │
│ │ │
│ ▼ │
│ Level 4: C2 編譯(完全最佳化) │
│ 基於 Profiling 資訊進行激進最佳化 │
│ │
└─────────────────────────────────────────────────────────────────────┘JIT 編譯最佳化#
常見最佳化技術#
1. 方法內聯(Method Inlining)#
// 最佳化前
public int square(int x) {
return x * x;
}
public int calculate() {
return square(5);
}
// 內聯後
public int calculate() {
return 5 * 5; // 直接計算,消除方法呼叫開銷
}# 設定內聯閾值(位元組碼大小)
-XX:MaxInlineSize=35 # 熱點方法
-XX:FreqInlineSize=325 # 頻繁呼叫的方法2. 逃逸分析(Escape Analysis)#
public void example() {
// point 物件沒有逃逸出方法
Point point = new Point(1, 2);
int sum = point.x + point.y;
System.out.println(sum);
}
// 經過逃逸分析和最佳化後,可能變成:
public void example() {
// 標量替換:物件分解為基本型別
int x = 1;
int y = 2;
int sum = x + y;
System.out.println(sum);
}逃逸分析的最佳化:
- 標量替換:將物件分解為基本型別,在堆疊上分配
- 鎖消除:對於不會逃逸的物件,消除同步鎖
- 堆疊上分配:將不逃逸的物件直接分配在堆疊上
# 啟用逃逸分析(預設開啟)
-XX:+DoEscapeAnalysis
# 啟用標量替換
-XX:+EliminateAllocations
# 啟用鎖消除
-XX:+EliminateLocks3. 常數折疊(Constant Folding)#
// 最佳化前
int a = 1 + 2;
int b = 3 * 4;
int c = a + b;
// 最佳化後
int c = 15; // 編譯時計算完成4. 死碼消除(Dead Code Elimination)#
// 最佳化前
public void example() {
int unused = expensiveCalculation(); // 結果未使用
System.out.println("Hello");
}
// 最佳化後
public void example() {
System.out.println("Hello"); // expensiveCalculation 被消除
}5. 迴圈最佳化#
// 迴圈展開(Loop Unrolling)
// 最佳化前
for (int i = 0; i < 4; i++) {
sum += array[i];
}
// 最佳化後
sum += array[0];
sum += array[1];
sum += array[2];
sum += array[3];查看 JIT 編譯結果#
# 印出編譯過程
-XX:+PrintCompilation
# 輸出內聯決策
-XX:+PrintInlining
# 輸出組合語言程式碼(需要 hsdis 外掛)
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintAssemblyPrintCompilation 輸出解讀
169 1 3 java.lang.String::hashCode (55 bytes)
171 2 3 java.lang.String::equals (81 bytes)
172 3 n 0 java.lang.System::arraycopy (native)
173 4 3 java.lang.Object::<init> (1 bytes)格式說明:
- 第 1 欄:時間戳(毫秒)
- 第 2 欄:編譯 ID
- 第 3 欄:
n:native 方法s:同步方法!:有例外處理%:OSR(On Stack Replacement)編譯
- 第 4 欄:編譯層級(0-4)
- 後續:方法名和大小
Graal 編譯器#
Graal 簡介#
Graal 是用 Java 編寫的 JIT 編譯器,可以替代 HotSpot 的 C2 編譯器。它是 GraalVM 的核心組件,支援 AOT 編譯和多語言互操作。
# 啟用 Graal 編譯器(Java 10+)
-XX:+UnlockExperimentalVMOptions
-XX:+UseJVMCICompiler
# GraalVM 中預設啟用Graal 優勢#
- 更激進的最佳化:部分逃逸分析、推測最佳化
- 更好的內聯策略
- 模組化設計:易於擴展和實驗新最佳化
- AOT 編譯支援:Native Image
AOT 編譯(Ahead-of-Time)#
# 使用 GraalVM Native Image
native-image -jar myapp.jar
# 產生的原生可執行檔
./myappAOT 優缺點:
| 優點 | 缺點 |
|---|---|
| 極快的啟動時間 | 編譯時間長 |
| 較小的記憶體佔用 | 缺少執行時最佳化 |
| 無需 JVM 執行環境 | 部分動態特性受限 |
| 適合無伺服器/容器 | 反射需要組態 |
實用工具#
ASM 位元組碼操作#
// 使用 ASM 動態生成類別
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V11, ACC_PUBLIC, "GeneratedClass", null, "java/lang/Object", null);
// 產生建構子
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
cw.visitEnd();
byte[] bytecode = cw.toByteArray();JITWatch#
JITWatch 是分析 JIT 編譯行為的工具:
# 產生 JIT 日誌
java -XX:+UnlockDiagnosticVMOptions \
-XX:+TraceClassLoading \
-XX:+LogCompilation \
-XX:LogFile=jit.log \
-jar myapp.jar
# 使用 JITWatch 分析 jit.log總結#
位元組碼與執行引擎的核心要點:
- 位元組碼結構:常數池、欄位表、方法表、屬性表
- 方法呼叫:五種 invoke 指令,靜態/動態分派
- 執行模式:解譯器快速啟動,JIT 長期高效
- 分層編譯:從 Level 0 到 Level 4,逐步最佳化
- 最佳化技術:內聯、逃逸分析、常數折疊、迴圈最佳化
- 實用工具:javap、ASM、JITWatch