???一、從 JDK 到 JVM:Java 運行環境的基石??
????????在 Java 開發領域,JDK(Java Development Kit)是開發者的核心工具包。它不僅包含了編譯 Java 代碼的工具(如 javac),還內置了 JRE(Java Runtime Environment)—— 即 Java 程序的運行時環境。而 JVM(Java Virtual Machine)則是 JRE 的核心,它如同一個 “翻譯官”,將 Java 字節碼轉換為不同操作系統能理解的機器指令,實現了 “一次編寫,到處運行” 的跨平臺特性。
1.JVM 作用
Java 虛擬機負責裝載字節碼到其內部,解釋/編譯為對應平臺上的機器碼指令執行。
現在的 JVM 不僅可以執行 java 字節碼文件,還可以執行其他語言編譯后的字節碼文件,是一個跨語言平臺.?
?
程序在執行之前先要把 java 代碼轉換成字節碼(class 文件),jvm首先需要把字節碼通過一定的方式 類加載器(ClassLoader)把文件加載到內存中的運行時數據區(Runtime Data Area) ,而字節碼文件是jvm的一套指令集規范,并不能直接交個底層操作系統去執行,因此需要特定的命令解析器 執行引擎(Execution Engine) 將字節碼翻譯成底層系統指令再交由CPU 去執行,而這個過程中需要調用其他語言的接口本地庫接口(NativeInterface) 來實現整個程序的功能,這就是這 4 個主要組成部分的職責與功能。?
? ? ? ? 比如,當我們運行java HelloWorld
時,JVM 首先通過類加載器找到HelloWorld.class
文件,將二進制數據讀入內存,創建HelloWorld.class
對象。
二、JVM 模塊劃分與核心功能
JVM 的架構可分為四大模塊:類加載器子系統、運行時數據區、執行引擎和本地方法接口。
1. 類加載器子系統
類加載器負責將字節碼文件加載到 JVM 中。根據職責不同,JVM 提供了三種類加載器:
- 啟動類加載器(Bootstrap ClassLoader):用 C/C++ 實現,加載 Java 核心類庫(如
rt.jar
),位于%JAVA_HOME%/lib
目錄。- 擴展類加載器(Extension ClassLoader):加載
%JAVA_HOME%/jre/lib/ext
目錄或java.ext.dirs
指定路徑的類庫。- 應用程序類加載器(Application ClassLoader):加載用戶類路徑(classpath)下的類,是程序默認的類加載器。
????????類加載器采用雙親委派機制:當一個類加載器收到加載請求時,會先委托父類加載器處理,只有父類無法加載時才嘗試自己加載。這一機制確保了核心類的安全性和唯一性。例如,當嘗試加載java.lang.String
時,啟動類加載器會優先加載核心庫中的 String 類,避免用戶自定義類覆蓋核心類。
2. 運行時數據區
運行時數據區是 JVM 在執行程序時分配的內存區域,包含以下部分:
- 程序計數器:記錄當前線程執行的字節碼指令地址,是線程私有的最小內存空間。
- Java 虛擬機棧:每個線程創建時生成,保存方法調用的棧幀(包含局部變量表、操作數棧、方法返回地址等),線程私有,可能出現棧溢出(StackOverflowError)。
- 本地方法棧:管理本地方法(如 C/C++ 實現的方法)的調用。
- 堆內存:存儲對象實例,是 GC(垃圾回收)的主要區域,分為新生代(Eden 和 Survivor 區)和老年代,通過分代收集算法優化回收效率。
- 方法區:存儲類的元數據(如字節碼、靜態變量、常量池),JDK8 后稱為元空間(Metaspace),邏輯上獨立于堆。
3. 執行引擎
執行引擎是 JVM 的 “大腦”,負責將字節碼轉換為機器指令。它包含:
- 解釋器:逐行解釋執行字節碼,啟動快但效率較低。
- JIT 編譯器:將頻繁執行的 “熱點代碼” 編譯為本地機器碼,存儲在方法區的 JIT 緩存中,提升執行效率。
- 垃圾回收器:自動回收不再使用的對象,主要針對堆內存,采用標記 - 復制、標記 - 清除、標記 - 壓縮等算法。
4. 本地方法接口
????????本地方法接口允許 Java 調用非 Java 代碼(如 C/C++),通過本地方法庫實現與操作系統或硬件的交互,例如文件操作、網絡通信等。
三、類加載的核心機制與實踐
1. 類加載的作用與過程
類加載的核心任務是將字節碼文件轉換為 JVM 可識別的 Class 對象。這一過程分為五個階段:
- 加載(Loading):通過類的全限定名獲取二進制字節流,生成 Class 對象。
- 驗證(Verification):檢查字節碼的安全性和合規性,防止惡意代碼攻擊。
- 準備(Preparation):為靜態變量分配內存并設置默認初始值(如 int 初始化為 0)。
- 解析(Resolution):將符號引用轉換為直接引用(如將類名轉換為內存地址)。
- 初始化(Initialization):執行靜態代碼塊和靜態變量賦值,這是類加載的最后一步。
2. 類加載的觸發時機
????????類加載遵循 “按需加載” 原則,只有在需要使用類時才會觸發。根據 JVM 規范,以下情況會強制加載類(主動引用):
假設我們有一個 Hello 類
package com.ffyc.classload;/*** 問題:什么時候類會被加載?**/
public class Hello {// 作為靜態成員時,類會被加載static {System.out.println("類被加載了......");}//作為main方法時,類也會被加載public static void main(String[] args) {System.out.println("1111111");}}
TestHello 類?
package com.ffyc.classload;public class TestHello {public static void main(String[] args) throws ClassNotFoundException {new Hello(); // 觸發Hello類的初始化,并加載Hello類Class.forName("com.ffyc.classload.Hello");//反射方式加載Hello類}}
3. 類加載的典型案例
(1) 主動引用(必定觸發加載)
new
?實例化對象MyClass obj = new MyClass(); // 首次創建對象時加載
- 訪問類的靜態變量或靜態方法
int value = MyClass.staticField; // 訪問靜態字段 MyClass.staticMethod(); // 調用靜態方法
- 反射調用(
Class.forName()
)Class.forName("com.example.MyClass"); // 通過反射強制加載
- 初始化子類時(父類優先加載)
class Parent {} class Child extends Parent {} // 首次使用 Child 時,會先加載 Parent
- 作為程序入口的主類(
main
?方法所在類)public class Main { public static void main(String[] args) {} // JVM 啟動時加載 }
(2) 被動引用(不會觸發加載)
- 通過子類引用父類的靜態字段
class Parent { static int value = 10; } class Child extends Parent {} System.out.println(Child.value); // 僅加載 Parent,不加載 Child
- 通過數組定義類
MyClass[] arr = new MyClass[10]; // 不會加載 MyClass
- 引用常量(常量在編譯期優化)
class MyClass { final static int VALUE = 10; } System.out.println(MyClass.VALUE); // 不觸發加載(常量池直接訪問)
四、類加載器分類
站在 java 開發人員的角度來看,類加載器就應當劃分得更細致一些. 保持者三層類加載器.
1. 啟動類加載器 (BootStrap ClassLoader)
????????這個類加載器使用 C/C++語言實現,也叫引導類加載器,嵌套在 JVM 內部.它用來加載java 核心類庫.負責加載擴展類加載器和應用類加載器,并為他們指定父類加載器. 出于安全考慮,啟動類加載器只加載存放在\lib 目錄,或者被-Xbootclasspath 參數鎖指定的路徑中存儲放的類.
2.?擴展類加載器(Extension ClassLoader)
????????Java 語言編寫的,由 sun.misc.Launcher$ExtClassLoader 實現. 派生于 ClassLoader 類. 從 java.ext.dirs 系統屬性所指定的目錄中加載類庫,或從JDK 系統安裝目錄的jre/lib/ext 子目錄(擴展目錄)下加載類庫.如果用戶創建的jar 放在此目錄下,也會自動由擴展類加載器加載.
3. 應用程序類加載器(系統類加載器 Application ClassLoader)
????????Java 語言編寫的,由 sun.misc.Launcher$AppClassLoader 實現.派生于 ClassLoader 類. 加載我們自己定義的類,用于加載用戶類路徑(classpath)上所有的類. 該類加載器是程序中默認的類加載器.ClassLoader 類 , 它 是 一 個 抽 象 類 , 其 后 所 有的類加載器都繼承自ClassLoader(不包括啟動類加載器)
?五、雙親委派機制
????????Java 虛擬機對 class 文件采用的是按需加載的方式,也就是說當需要該類時才會將它的 class 文件加載到內存中生成 class 對象.而且加載某個類的class 文件時,Java 虛擬機采用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式.
工作原理:
1. 如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執行.
2. 如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終將到達頂層的啟動類加載器.
3. 如果父類加載器可以完成類的加載任務,就成功返回,倘若父類加載器無法完成加載任務,子加載器才會嘗試自己去加載,這就是雙親委派機制. 如果均加載失敗,就會拋出 ClassNotFoundException 異常。
那么思考一下,如果我們自定義一個 String 類,會被加載嗎?
直接上示例去驗證你的答案
?
package java.lang;/*** 測試雙親委派機制*/
public class String {static {System.out.println("自定義 String 類被加載!");}
}
package com.ffyc.classload;public class TestHello {public static void main(String[] args) {new Hello(); // 觸發Hello類的初始化,并加載Hello類try {Class.forName("com.ffyc.classload.Hello");//反射方式加載Hello類Class.forName("java.lang.String");} catch (ClassNotFoundException e) {throw new RuntimeException(e);}System.out.println("String類加載器為:"+ String.class.getClassLoader()+"所以屬于啟動類加載器");System.out.println("Hello類加載器為:"+Hello.class.getClassLoader()+"所以屬于系統類加載器");}}
輸出結果為,并沒有看到??"自定義 String 類被加載!"? 這句話
????????可以看到,自定義的String 類雖然和jdk的String類同包同名,但還是沒有被加載,這就是雙親委派機制,那么問題來了,我就想讓它加載我自己定義的String類,該怎么做?
六、如何打破雙親委派機制
????????Java 虛擬機的類加載器本身可以滿足加載的要求,但是也允許開發者自定義類加載器。 在 ClassLoader 類中涉及類加載的方法有兩個,loadClass(String name), findClass(String name),這兩個方法并沒有被 final 修飾,也就表示其他子類可以重寫. 重寫 findClass 方法 我們可以通過自定義類加載重寫方法打破雙親委派機制, 再例如 tomcat 等都有自己定義的類加載器.
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;public class CustomClassLoader extends ClassLoader {private final String classPath;public CustomClassLoader(String classPath) {this.classPath = classPath;}@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 檢查是否已加載過該類Class<?> loadedClass = findLoadedClass(name);if (loadedClass != null) {return loadedClass;}// 打破雙親委派:先嘗試自己加載,再委托父類try {// 自定義加載邏輯(例如從指定路徑加載類)byte[] classBytes = loadClassBytes(name);if (classBytes != null) {return defineClass(name, classBytes, 0, classBytes.length);}} catch (IOException e) {// 加載失敗,繼續委托父類加載器}// 委托父類加載器(保留原有機制的兜底)return super.loadClass(name, resolve);}}private byte[] loadClassBytes(String className) throws IOException {// 將類名轉換為文件路徑(例如com.example.MyClass → /path/com/example/MyClass.class)String path = classPath + File.separator + className.replace('.', File.separatorChar) + ".class";File file = new File(path);if (!file.exists()) {return null;}try (FileInputStream fis = new FileInputStream(file)) {byte[] bytes = new byte[(int) file.length()];fis.read(bytes);return bytes;}}
}
public class Main {public static void main(String[] args) throws Exception {// 創建自定義類加載器,指定加載路徑CustomClassLoader loader = new CustomClassLoader("/path/to/classes");// 加載自定義類(優先從指定路徑加載)Class<?> clazz = loader.loadClass("com.example.MyClass");Object instance = clazz.newInstance();System.out.println(instance.getClass().getClassLoader()); // 輸出:CustomClassLoader}
}
七、總結
????????JVM 的類加載機制是 Java 程序運行的基石,它通過類加載器、運行時數據區和執行引擎的協同工作,確保了程序的跨平臺性和高效執行。理解類加載的過程、時機和類加載器的工作原理,不僅能幫助開發者優化程序性能,還能深入排查類沖突、內存泄漏等問題。無論是日常開發還是高級調優,掌握 JVM 類加載機制都是成為優秀 Java 工程師的必經之路。