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│
│                                                             │
└─────────────────────────────────────────────────────────────┘

invokevirtualinvokeinterface 需要在執行時根據物件的實際類型查找方法,這稱為動態分派。而 invokestaticinvokespecial 在編譯期即可確定呼叫目標,屬於靜態分派

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:+EliminateLocks

3. 常數折疊(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:+PrintAssembly
PrintCompilation 輸出解讀
    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

# 產生的原生可執行檔
./myapp

AOT 優缺點:

優點缺點
極快的啟動時間編譯時間長
較小的記憶體佔用缺少執行時最佳化
無需 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

總結#

位元組碼與執行引擎的核心要點:

  1. 位元組碼結構:常數池、欄位表、方法表、屬性表
  2. 方法呼叫:五種 invoke 指令,靜態/動態分派
  3. 執行模式:解譯器快速啟動,JIT 長期高效
  4. 分層編譯:從 Level 0 到 Level 4,逐步最佳化
  5. 最佳化技術:內聯、逃逸分析、常數折疊、迴圈最佳化
  6. 實用工具:javap、ASM、JITWatch