我是小C同學編寫得一個java檔案,如何實現我的功能呢?需要去JVM(Java Virtual Machine)這個地方旅行。
我高高興興的來到JVM,想要開始JVM之旅,它確說:「現在的我還不能進去,需要做一次轉換,生成class檔案才行」。為什麼這樣呢?
JVM不能直接載入java檔案的原因:
知道原因後,我又問JVM,我怎麼才能變成class檔案呢,JVM告訴我可以通過javac命令。
javac 是 Java 編譯器命令,用於將 Java 原始碼檔案編譯成位元組碼檔案(.class 檔案)。
javac [options] [source files]
.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 中的類載入器是按照其載入類的特點進行分類的,主要有以下幾種型別:
程式碼:
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());
}
}
執行結果:
自定義類載入器主要包括兩種型別:
程式碼:
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);
}
}
程式碼:
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核心庫的型別安全和穩定性,並防止惡意程式碼的自行佈置。如果一個類沒有在父類別載入器中被發現,子類載入器才會嘗試載入該類。這種類載入器之間的父子關係被稱為「雙親委派模型」.
如圖:
為什麼通過雙親委派進行載入呢?
載入過後,我是否就可以被使用了呢?答案是否定的,我還要經歷Lingking 階段,包括Verification、Preparation 和 Resolution。
在驗證階段,Java虛擬機器器會進行語法與語意的檢查,以保證class檔案的完整性和正確性,同時保證被載入的class與虛擬機器器的版本相容。主要的檢查內容包括檔案格式、位元組碼語意、符號參照等。
在準備階段,Java虛擬機器器會為類變數分配記憶體,並且賦予初始值。如果類變數包含有靜態變數,那麼這時也會初始化靜態變數。因此,在這個階段,類變數所使用的空間已經被分配,將其設定為預設初始值即可。
在解析階段,將類或介面中的符號參照轉化為直接參照的過程。在 Java 虛擬機器器載入類時,符號參照是一種指向常數池中某個符號的參照,而直接參照則是指向記憶體中某個位置的直接指標。解析階段可以理解為是在解決類之間的依賴關係,使各個類之間可以像使用自身成員一樣使用別的類中的成員。
在驗證、準備和解析後,我還要經過初始化,才能被使用。
初始化是指在類載入過程的最後一步,JVM要對類進行一些初始化的操作,確保類可以安全地使用。在這個階段,往往包括靜態變數顯式賦值和靜態程式碼塊執行。
當類載入器完成類的載入、驗證、準備後,在初始化階段,JVM對類的靜態變數進行顯式賦值。如果類定義了多個靜態變數,JVM會按照程式碼中宣告的順序進行初始化,並且若發現此過程需要存取到其他未初始化的類,JVM會先完成這些類的初始化。
除了靜態變數的顯式賦值,類的靜態程式碼塊也會在初始化階段執行。當JVM執行類載入的Initializing階段時,會執行類中所有靜態程式碼塊的內容,如果類中沒有定義靜態程式碼塊,則不執行。這個過程一般用於在使用之前對類進行初始化。
當一個類在初始化時,如果發現其父類別還未進行初始化,JVM會先對其父類別進行初始化。如果該類實現了介面,也會對這個介面進行初始化操作,介面的初始化過程和類一樣,都會進行靜態變數顯式賦值及靜態程式碼塊執行,同時還會檢查介面中的所有靜態方法。
初始化之後,我才真正的進入JVM中,其它小夥伴需要我的時候,只需要建立我的範例,就可以使用我的功能了,得到我幫助得小夥伴都很感謝我。
在JVM中我過得很開心,也留下了很多足跡。在我走後,如何讓我得足跡不對其他小夥伴有影響呢?GC可以幫我解決這個問題。
GC(Garbage Collection)是JVM提供的垃圾回收機制。在Java中,物件是動態分配的,記憶體是由JVM自動管理,而不是由程式設計師手動分配和釋放。當一個物件不再被程式參照時,就應該由垃圾回收器回收其佔用的記憶體,這樣可以防止記憶體漏失和提高記憶體的。
通過我的旅行,你知道JVM是怎麼載入一個類的了麼?我們通過載入、Linking、初始化和使用等各個階段,將Java類完整地載入記憶體並執行其中定義的方法和變數。這個過程中,每個階段都扮演著不同的角色,併為類的正常執行提供了必要的支援。
作者:京東物流 陳昌浩
來源:京東雲開發者社群 自猿其說Tech 轉載請註明來源