你可曾想過:當你在終端裏敲下 java,在 main 方法真正運行之前,JVM 為了“創造一個可運行你的程序的宇宙”,到底經歷了哪些步驟?從參數校驗、系統資源探測,到選擇垃圾回收器,再到類的加載、鏈接與初始化,這些看不見的過程決定了應用的啓動體驗與後續性能。本文用一個極簡的 HelloWorld 貫穿全程,結合詳細日誌,一步步洞察 JVM 的啓動機制,幫你在調試和性能優化時更有抓手。

1. 概覽

當我們運行一個 Java 應用時,JVM 在我們的代碼真正開始執行之前,會先完成一系列複雜步驟。本文將從執行 java 命令的那一刻開始,一直走到應用就緒。

我們以一個簡單的 HelloWorld 程序為例,拆解每一個階段。理解這些內部機制能顯著提升調試與性能調優的效果。

2. 從 java 命令到 JVM 啓動

在 JVM 執行任何代碼之前,它需要先啓動、校驗輸入並配置運行環境。下面按啓動順序走一遍早期流程:從調用 java 命令到初始化 JVM 運行時。

2.1. java 命令與初始調用

當我們運行 java 命令時,JVM 啓動序列會通過 JNI 方法 JNI_CreateJavaVM() 開始執行。該方法完成若干關鍵初始化任務,為執行 Java 應用準備環境。Java Native Interface(JNI)是 JVM 與原生系統庫之間的橋樑,使 Java 與平台特性可以雙向通信。

本文將使用詳細日誌觀察 JVM 的內部運作,例如:

java -Xlog:all=trace HelloWorldCopy

2.2. 校驗用户輸入

首先,JVM 會校驗我們傳入的參數:

[0.006s][info][arguments] VM Arguments:
[arguments] jvm_args: -Xlog:all=trace:file=helloworld.log 
[arguments] java_command: HelloWorld
[arguments] java_class_path (initial): .
[arguments] Launcher Type: SUN_STANDARD

JVM 會驗證目標可執行、類路徑以及任何 JVM 參數,確保它們在繼續執行前都是有效的。這個步驟能儘早捕獲很多常見配置錯誤,避免後續階段出現更難定位的問題。

2.3. 檢測系統資源

接着,JVM 會識別可用的系統資源,例如處理器數量、內存大小以及關鍵系統服務:

[0.007s][debug][os       ] Process is running in a job with 20 active processors.
[os       ] Initial active processor count set to 20
[os       ] Process is running in a job with 20 active processors.
[gc,heap  ]   Maximum heap size 4197875712
[gc,heap  ]   Initial heap size 262367232
[gc,heap  ]   Minimum heap size 6815736
[os       ] Host Windows OS automatically schedules threads across all processor groups.
[os       ] 20 logical processors found.

這些信息會影響 JVM 的一些內部決策,比如默認選擇哪個垃圾回收器。可用 CPU 數和總內存會直接影響 JVM 的啓發式選擇。不過,大多數設置都可以通過顯式的 JVM 參數進行覆蓋。在這個階段,JVM 還會檢查是否支持 Native Memory Tracking,並驗證它可能依賴的各類操作系統工具的可用性。

2.4. 環境準備

隨後,JVM 會生成 HotSpot 性能數據。這些數據會被 JConsole、VisualVM 等工具用於檢查和分析 JVM:

[perf,datacreation] name = sun.rt._sync_Inflations, dtype = 11, variability = 2, units = 4, dsize = 8, vlen = 0, pad_length = 4, size = 56, on_c_heap = FALSE, address = 0x000001f3085f0020, data address = 0x000001f3085f0050

這類性能數據通常存儲在系統的 /tmp 目錄下,並會在啓動階段的一段時間裏持續生成,與其他初始化任務並行進行。

3. 加載、鏈接與初始化

當 JVM 環境就緒後,它會開始為我們的程序執行做準備。

3.1. 選擇垃圾回收器

在 JVM 內部,一個關鍵步驟是選擇垃圾回收器(GC)。截至 JDK 23,默認情況下 JVM 會選擇 G1 GC,除非系統可用內存少於 1792MB 和/或僅有單處理器:

[gc               ] Using G1
[gc,heap,coops    ] Trying to allocate at address 0x0000000705c00000 heap of size 0xfa400000
[os               ] VirtualAlloc(0x0000000705c00000, 4198498304, 2000, 4) returned 0x0000000705c00000.
[os,map           ] Reserved [0x0000000705c00000 - 0x0000000800000000), (4198498304 bytes)
[gc,heap,coops    ] Heap address: 0x0000000705c00000, size: 4004 MB, Compressed Oops mode: Zero based, Oop shift amount: 3
[pagesize         ] Heap:  min=8M max=4004M base=0x0000000705c00000 size=4004M page_size=4K

當然,我們也可以選擇其它 GC:如 Parallel GC、ZGC 等,具體可用與默認策略依不同 JDK 版本與發行版而異。

3.2. 加載 CDS(類數據共享)

此時,JVM 會開始尋找進一步的優化機會。CDS 是一組已經經過預處理的類文件歸檔,可以改善 JVM 的啓動性能:

[cds] trying to map [Java home]/lib/server/classes.jsa
[cds] Opened archive [Java home]/lib/server/classes.jsa

不過,CDS 正在被 Project Leyden 中的 AOT(提前)機制逐步替代,後文會繼續討論。

3.3. 創建方法區

JVM 隨後會創建“方法區”,這是一個用於存儲類數據的特殊離堆內存區域。在 HotSpot 中,這一區域被稱為 metaspace。當關聯的類加載器不再可達時,存儲於此的類數據也會被移除:

[metaspace,map    ] Trying anywhere...
[metaspace,map    ] Mapped at 0x000001f32b000000

雖然方法區不在堆中,但它仍由 GC 管理。

3.4. 類加載

類加載包含三個步驟:定位二進制表示、根據其派生出類、並將其加載到方法區。正是這種動態加載能力,讓 Spring、Mockito 等框架可以在運行期按需生成並加載類。

類加載有兩種方式:引導類加載器(bootstrap class loader)或自定義類加載器。下面藉助一個簡單的 HelloWorld 類,看看 JVM 首先會做什麼:

public class HelloWorld extends Object {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

JVM 會優先加載 java.lang.Object 及其依賴。類在初次加載時大多處於“半隱藏”的狀態,以便進行必要的驗證與整理工作。

再看下 java.lang.Object 的方法:

public class Object {
    public final native Class<?> getClass()
    public String toString()
    public boolean equals(Object obj)
}

這些方法分別引用了 java.lang.Classjava.lang.String,因此它們也需要先行加載。JVM 採用“按需加載”的策略,僅在類被實際引用時才加載。不過,上述這些對 JVM 至關重要的類會被“搶先加載”。在一個簡單的 HelloWorld 程序裏,由 JNI_CreateJavaVM() 初始化的引導類加載器負責所有的類加載工作。

3.5. 類鏈接

類鏈接可以拆分為驗證(Verification)、準備(Preparation)與解析(Resolution),其發生順序並不固定:解析可能發生在驗證之前,也可能在類初始化之後。驗證確保類結構正確:

[class,init] Start class verification for: HelloWorld
[verification] Verifying class HelloWorld with new format
[verification] Verifying method HelloWorld.<init>()V

位於 CDS 中的類已經過驗證,因此會跳過該步驟,從而提升啓動性能。這是 CDS 的重要收益之一。在“準備”階段,JVM 會用默認值初始化靜態字段,沒有顯式初始化器的靜態變量會自動獲得默認值。

在“解析”階段,JVM 會解析常量池(Constant Pool)中的符號引用。常量池保存了類的所有符號引用,JVM 必須先將其解析為真實的內存引用,才能執行相關指令。

我們可以使用 javap 來觀察:

javap -verbose HelloWorldCopy

這將顯示常量池的內容:

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #7 = Fieldref           #8.#9          // java/lang/System.out:Ljava/io/PrintStream;
  #13 = String             #14            // Hello World

構造器的字節碼並不直接包含地址。它引用常量池中的符號項(例如 #1),這些條目描述了方法或字段。解析階段會將這些符號項轉為可執行的真實內存引用:

public HelloWorld();
  descriptor: ()V
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=1, locals=1, args_size=1
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  LineNumberTable:
    line 2: 0
    line 4: 4

第 1 行的 invokespecial 指令引用了常量池條目 #1,其中包含鏈接到 java.lang.Object 構造器所需的信息。<init> 表示這是由 javac 為每個構造器自動生成的特殊方法。JVM 採用“延遲解析”,只有在嘗試執行類中的某條指令時才觸發解析;並非所有已加載的類都會實際執行其指令。

3.6. 類初始化

類初始化會為靜態字段賦值並執行靜態初始化器,這與我們調用構造器的實例初始化不同。該過程由 javac 自動生成的特殊方法 clinit 負責。

4. 優化 JVM 啓動性能

儘管 JVM 的啓動已經很高效,但仍有提升空間。以下是一些方向。

4.1. 類加載的影響

我們可以使用系統的 time 工具來度量 JVM 啓動、加載類、鏈接並執行這個簡單程序的總耗時:

time java HelloWorldCopy

該工具會測量從 JVM 進程啓動到退出的掛鐘時間,包含類加載、鏈接、JIT 預熱與程序執行——不僅僅是用户代碼。對於 HelloWorld,JVM 在啓動期間通常會加載約 400~450 個類。在現代硬件上,即便開啓冗長日誌,整個過程也大約在 60 毫秒左右完成。

4.2. Project Leyden

Project Leyden 的目標是減少啓動時間、達到峯值性能的時間以及內存佔用。JDK 24 引入了 JEP 483:Ahead-of-Time Class Loading and Linking(提前類加載與鏈接),將這些操作從啓動時前移至 AOT 階段。

該特性會在“訓練運行”中記錄 JVM 的行為,將其存入緩存,並在後續啓動時從緩存加載。這將取代原先的 CDS 概念,並最終以 AOT 的更廣泛能力來統一表達。

4.3. JVM 參數與調優

雖然我們可以通過靜態字段與初始化器在某些場景中優化啓動性能,但應謹慎對待。為了將行為挪到類加載階段而進行重構,往往很難獲得可測量的收益——特別是考慮到運行時的大部分代碼來自依賴庫而非我們自己的應用。

5. 結論

本文從校驗用户輸入、檢測系統資源,到類的加載、鏈接與初始化,系統地梳理了 JVM 在啓動階段經歷的複雜流程。即便是一個簡單的 HelloWorld,JVM 也會在執行代碼之前構建起完整的運行環境,加載數百個類。

隨着 Project Leyden 等改進(例如 AOT)的到來,JVM 的啓動性能還將進一步提升。

更多相關技術乾貨分享可以關注這個Java專題