準備 我是小C同學編寫得一個java文件,如何實現我的功能呢?需要去JVM(Java Virtual Machine)這個地方旅行。 變身 我高高興興的來到JVM,想要開始JVM之旅,它確說:“現在的我還不能進去,需要做一次轉換,生成class文件才行”。為什麼這樣呢? JVM不能直接載入java文 ...
準備
我是小C同學編寫得一個java文件,如何實現我的功能呢?需要去JVM(Java Virtual Machine)這個地方旅行。
變身
我高高興興的來到JVM,想要開始JVM之旅,它確說:“現在的我還不能進去,需要做一次轉換,生成class文件才行”。為什麼這樣呢?
JVM不能直接載入java文件的原因:
- Java源代碼中包含了許多高級語言特性和語法,比如類、繼承、多態、異常處理等等。這些高級特性在JVM中沒有直接對應的形式,只有通過編譯器的處理才能轉化為JVM可以理解的位元組碼指令。
- Java源代碼需要經過編譯器的編譯過程,才能生成相應的位元組碼文件,然後再由JVM載入、解釋執行。在編譯過程中,編譯器對源代碼進行語法分析、類型檢查、優化等操作,最終生成與目標平臺相容的Java位元組碼文件。
- JVM只能夠載入和運行符合Java虛擬機規範的.class位元組碼文件,而不能夠直接載入和運行Java源代碼文件。
編譯
知道原因後,我又問JVM,我怎麼才能變成class文件呢,JVM告訴我可以通過javac命令。
javac
javac 是 Java 編譯器命令,用於將 Java 源代碼文件編譯成位元組碼文件(.class 文件)。
命令格式
javac [options] [source files]
- options:為編譯選項,可以控制編譯器的行為,例如指定類路徑、生成調試信息、壓縮文件等。
- source files:為需要編譯的 Java 源代碼文件,可以指定多個文件,用空格隔開。如果不指定源代碼文件,則 `javac` 命令會在當前目錄查找所有擴展名為
.java
的文件進行編譯。
需要註意的是,`javac` 命令需要在正確配置 JDK 環境後才能使用。JDK(Java Development Kit)是 Java 開發工具包的縮寫,是 Java 應用程式開發的核心組件之一。
具體實現
編譯器在編譯源文件時,需要對源文件進行語法分析、語義分析和類型檢查等操作。
- 語法分析:
javac
命令首先將源文件讀入記憶體,然後進行詞法分析和語法分析。詞法分析器負責將源文件中的字元序列轉換成一個個單詞(Token),然後語法分析器將單片語合成可以被解釋執行的語法結構,形成抽象語法樹(AST)。 - 語義分析:
javac
命令在生成AST之後,進行語義分析。語義分析器主要是為了檢查程式中是否存在語義錯誤,例如變數未定義、類型不匹配等,如果發現語義錯誤,編譯器會輸出錯誤信息,並中止編譯過程,不會生成位元組碼文件。 - 類型檢查:
javac
命令在語義分析的基礎上,進行類型檢查。類型檢查器主要是檢查程式的類型是否匹配和相容,如果類型不匹配或不相容,編譯器會在編譯期間報告錯誤。 - 代碼生成:
javac
命令在生成抽象語法樹後,對其進行優化和轉化,最終生成位元組碼文件。編譯器會根據目標代碼的平臺和版本,生成適當的位元組碼文件。
執行
知道怎麼變身後,我立即通過javac命令,讓自己變成可以被JVM執行的class文件。
載入
變成class文件後,我怎麼能進入JVM內部呢,是走著去還是坐車去呢?JVM告訴我要通過類載入器進入。
類載入器
Java類載入器是Java虛擬機(JVM)中的一個重要組件,它負責將類文件(.class文件)載入到JVM中。
分類
Java 中的類載入器是按照其載入類的特點進行分類的,主要有以下幾種類型:
- 啟動類載入器(Bootstrap ClassLoader):負責載入 JRE/lib/rt.jar 中的核心 Java 類庫,是最頂層的類載入器,不是 Java 類(因為在 JVM 實現時就已經存在)。
- 擴展類載入器(Extension ClassLoader):負責載入 JRE/lib/ext 目錄下的擴展類庫,也是由 C++ 實現的類載入器。
- 應用程式類載入器(APP ClassLoader):負責載入應用程式的類,包括在 CLASSPATH 中指定的類庫或者目錄中的 Java 類。
- 自定義類載入器(Custom ClassLoader):繼承自 ClassLoader 類,實現自己的類載入器,主要用於載入一些自定義的類或者修改某些類的位元組碼。
查看使用的類載入器
代碼:
public class ClassLoaderTest {
public static void main(String[] args) {
//啟動類載入器
System.out.println(String.class.getClassLoader());
//擴展類載入器
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());
//應用程式類載入器
System.out.println(ClassLoaderTest.class.getClassLoader());
//擴展類載入器的父載入器
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getParent());
//應用程式類載入器的父載入器
System.out.println(ClassLoaderTest.class.getClassLoader().getParent());
}
}
執行結果:
自定義類載入器
自定義類載入器主要包括兩種類型:
- 獨立的自定義類載入器,通過重載 ClassLoader 類中的 findClass 方法來實現載入類文件的功能;
- 基於 URLClassLoader 類實現的自定義類載入器,使用 URL 的形式來指定類文件的位置。
重載ClassLoader
代碼:
public class CustomClassLoader extends ClassLoader {
private String basePath;
public CustomClassLoader(String basePath) {
this.basePath = basePath;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = getClassData(name);
if (data == null) {
throw new ClassNotFoundException();
} else {
// 使用 defineClass 方法將 byte 數組轉換為 Class 對象
return defineClass(name, data, 0, data.length);
}
}
private byte[] getClassData(String className) {
String path = basePath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try (InputStream inputStream = new FileInputStream(path);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
return outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
說明:
上述代碼繼承了ClassLoader
類,並重寫了其中的findClass()
方法,實現從指定目錄中載入類文件的功能。
在findClass()
方法中,首先通過getClassData()
方法讀取並返回類文件的位元組數組,如果獲取的位元組數組為空,則拋出ClassNotFoundException
異常;否則,使用defineClass()
方法將位元組數組轉換為 Class 對象,並返回該對象。
在getClassData()
方法中,根據傳入的類名生成類文件路徑,並使用FileInputStream
將類文件讀入位元組數組中。
使用:
public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
// 創建自定義類載入器,指定類文件所在的目錄
CustomClassLoader classLoader = new CustomClassLoader("F:\\classes");
// 使用自定義類載入器載入 Hello 類
Class<?> clazz = classLoader.loadClass("com.example.something.Hello");
Object obj = clazz.getDeclaredConstructor().newInstance();
System.out.println(obj);
}
}
基於 URLClassLoader
代碼:
public class CustomURLClassLoader extends URLClassLoader {
public CustomURLClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 調用父類 loadClass 方法進行委托載入
Class<?> clazz = super.findClass(name);
return clazz;
} catch (ClassNotFoundException e) {
// 如果父類無法載入,則嘗試在 URL 中載入
byte[] data = getClassData(name);
if (data == null) {
throw new ClassNotFoundException();
} else {
// 使用 defineClass 方法將 byte 數組轉換為 Class 對象
return defineClass(name, data, 0, data.length);
}
}
}
private byte[] getClassData(String className) {
String path = className.replace('.', '/') + ".class";
URL[] urls = getURLs();
for (URL url : urls) {
try {
URL classUrl = new URL(url, path);
// 使用 URLConnection 檢查類文件是否存在
try (InputStream is = classUrl.openStream();
ByteArrayOutputStream os = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) != -1) {
os.write(buffer, 0, length);
}
return os.toByteArray();
}
} catch (IOException e) {
// ignore and try next URL
}
}
return null;
}
}
說明:
上述代碼繼承了 `URLClassLoader` 類,並重寫了其中的 `findClass()` 方法,實現先嘗試使用父類載入器進行載入,如果無法載入,則嘗試使用 URL 載入類文件的功能。在 `getClassData()` 方法中,會遍歷 `URLClassLoader` 中定義的 URL,檢查類文件是否存在,並返回類文件的位元組數組,如果無法找到類文件,則返回 `null`。
使用:
public class CustomURLClassLoaderTest {
public static void main(String[] args) throws Exception {
// 創建 URL 數組,指定類文件所在的 URL
URL[] urls = { new URL("file:F:\\classes") };
// 創建父類載入器,使用系統類載入器
ClassLoader parent = ClassLoader.getSystemClassLoader();
// 創建自定義 URL 類載入器
CustomURLClassLoader classLoader = new CustomURLClassLoader(urls, parent);
// 使用自定義 URL 類載入器載入 Hello 類
Class<?> clazz = classLoader.loadClass("com.example.something.Hello");
Object obj = clazz.getDeclaredConstructor().newInstance();
System.out.println(obj);
}
}
雙親委派
載入器那麼多,我具體是哪個類進行載入得呢?雙親委派機制告訴我答案.
定義
雙親委派是一種Java類載入器的工作機制,它將類載入請求委派給父類載入器,直到頂級系統類載入器。基本思想是,除非有特殊需求,否則所有類的載入任務都應該由父類載入器完成,從而保證Java核心庫的類型安全和穩定性,並防止惡意代碼的自行佈置。如果一個類沒有在父類載入器中被髮現,子類載入器才會嘗試載入該類。這種類載入器之間的父子關係被稱為“雙親委派模型”.
如圖:
意義
為什麼通過雙親委派進行載入呢?
- 避免重覆載入
- 提高安全性
- 維護Java平臺的一致性
- 代碼優化
Linking
載入過後,我是否就可以被使用了呢?答案是否定的,我還要經歷Lingking 階段,包括Verification、Preparation 和 Resolution。
Verification(驗證)
在驗證階段,Java虛擬機會進行語法與語義的檢查,以保證class文件的完整性和正確性,同時保證被載入的class與虛擬機的版本相容。主要的檢查內容包括文件格式、位元組碼語義、符號引用等。
Preparation(準備)
在準備階段,Java虛擬機會為類變數分配記憶體,並且賦予初始值。如果類變數包含有靜態變數,那麼這時也會初始化靜態變數。因此,在這個階段,類變數所使用的空間已經被分配,將其設置為預設初始值即可。
Resolution(解析)
在解析階段,將類或介面中的符號引用轉化為直接引用的過程。在 Java 虛擬機載入類時,符號引用是一種指向常量池中某個符號的引用,而直接引用則是指向記憶體中某個位置的直接指針。解析階段可以理解為是在解決類之間的依賴關係,使各個類之間可以像使用自身成員一樣使用別的類中的成員。
初始化
在驗證、準備和解析後,我還要經過初始化,才能被使用。
定義
初始化是指在類載入過程的最後一步,JVM要對類進行一些初始化的操作,確保類可以安全地使用。在這個階段,往往包括靜態變數顯式賦值和靜態代碼塊執行。
內容
靜態變數顯式賦值
當類載入器完成類的載入、驗證、準備後,在初始化階段,JVM對類的靜態變數進行顯式賦值。如果類定義了多個靜態變數,JVM會按照代碼中聲明的順序進行初始化,並且若發現此過程需要訪問到其他未初始化的類,JVM會先完成這些類的初始化。
靜態代碼塊的執行
除了靜態變數的顯式賦值,類的靜態代碼塊也會在初始化階段執行。當JVM執行類載入的Initializing階段時,會執行類中所有靜態代碼塊的內容,如果類中沒有定義靜態代碼塊,則不執行。這個過程一般用於在使用之前對類進行初始化。
介面初始化
當一個類在初始化時,如果發現其父類還未進行初始化,JVM會先對其父類進行初始化。如果該類實現了介面,也會對這個介面進行初始化操作,介面的初始化過程和類一樣,都會進行靜態變數顯式賦值及靜態代碼塊執行,同時還會檢查介面中的所有靜態方法。
功能實現
初始化之後,我才真正的進入JVM中,其它小伙伴需要我的時候,只需要創建我的實例,就可以使用我的功能了,得到我幫助得小伙伴都很感謝我。
GC
在JVM中我過得很開心,也留下了很多足跡。在我走後,如何讓我得足跡不對其他小伙伴有影響呢?GC可以幫我解決這個問題。
定義
GC(Garbage Collection)是JVM提供的垃圾回收機制。在Java中,對象是動態分配的,記憶體是由JVM自動管理,而不是由程式員手動分配和釋放。當一個對象不再被程式引用時,就應該由垃圾回收器回收其占用的記憶體,這樣可以防止記憶體泄漏和提高記憶體的。
小結
通過我的旅行,你知道JVM是怎麼載入一個類的了麽?我們通過載入、Linking、初始化和使用等各個階段,將Java類完整地載入記憶體並執行其中定義的方法和變數。這個過程中,每個階段都扮演著不同的角色,併為類的正常運行提供了必要的支持。
作者:京東物流 陳昌浩
來源:京東雲開發者社區 自猿其說Tech 轉載請註明來源