Overview of Content
JVM 會為 Java 程式提供執行的環境,而 JVM 其中一個重要的任務就是 管理類別、物件的生命週期,這個本文則針對 類(Class
)說明
Java虛擬機(JVM)是 Java 程序的核心引擎,負責管理類別和物件的生命週期。本篇將深入探討 Java 類別的生命週期,以及 ClassLoader 在類別加載過程中的作用
- JVM 與類加載:探索 JVM 的生命週期,從類別載入到初始化,深入了解 JVM 與類之間的關係。
- 類的生命週期:從載入階段到初始化階段,逐一解析類別的生命週期過程。
- 類的加載時機:探討類別的加載時機,包括啟動類別、物件創建和靜態元素初始化等。
- Java ClassLoader 特點:了解 Java ClassLoader 的特性,包括 JVM 內建類加載器和父委託機制等。
- 類加載測試:深入測試類別的加載與卸載過程,驗證命名空間和父委託機制的作用。
無論你是對 Java 類別生命週期感興趣,還是想深入了解 ClassLoader 的工作原理,本篇都將為你提供深入且全面的解說。
寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀
JVM 與類加載
類的生命週期如下圖;重點是 JVM 加載 class 文件加載過後 1. 二進制放置方法區、2. 創建一個(唯一)JVM 堆區
JVM 生命週期
● JVM 生命週期如下
● JVM 啟動:JVM 的啟動是隨應用程式的啟動
當我們透過 Java 命令執行 Java 應用後,就會啟動一個 JVM(一個應用對應一個虛擬機的實體)
● JVM 結束:JVM 的結束通常有以下方式
● 程式的正常結束
● 程式發生異常錯誤導致例外中止
● 執行 System#exit()
方法,主動退出
● JVM 運行的 OS 平台,但平台發生意外,導致應用關閉,JVM 也會結束
● JVM 的生命週期會隨應用的啟動、關閉共同運行
類的生命週期
這裡說的是「類」!不是「物件」唷~
物件是運行時創建的實體instance
,類更像是物件的模板
● 類加載入 JVM 至 卸載出 JVM 的過程稱為類的生命週期,而類的生命週期有 5 個階段
A. 類加載:找查加載 Class 文檔 (Class 文檔轉為 Class類)
● 根據特定名稱找到類 or 界面的二進制字節流
● 將該二進制字節流代表的靜態儲存結構 轉化到方法區 的運行時數據結構
● 在內存中生成一個代表這個類的 java.lang.Class 對象,放置在方法區 (唯一一個 class),做為這個類的各種數據訪問入口
B. 類連結 (分三個階段)
● 驗證 : 確保引入的類型的正確性
● 準備 : 類的 static 字段(static 區塊、static Field)分配,並初始化
● 解析 : VM 將 常量池 內的符號引用替換為直接引用
C. 類初始化:將類的變量初始化為正確值
D. 類使用
E. 類卸載
● 類的加載時機?
而一個類的加載時機又有分幾種(之後小節會提到),但有一個原則就是 Java 應用 首次主動使用 這個類的時候,該類就會加載
類別:載入階段
● 類(Class 文件)的載入是指:
A. 方法區資料:將 .class
檔中的二進位資料讀取到記憶體中,並存放在 JVM 執行時期的方法區中!
而載入類的方式並非只有直接透過檔案,只要載入 Class 文件格式的資料,使用其他方式也可以,像是:
● 本地 .class
文件
● 網路傳輸進來的 2 進位 .class
文件
● 從 ZIP
, JAR
... 或其他壓縮檔提取的.class
文件
● 從記憶體中動態創建 .class
文件並加載
B. 堆區資料:在堆區建立該類相對應的 java.lang.Class
物件

● 而將「類」加載進 JVM 的工具是啥?就是 類加載器(之後說明)
類別:連結階段
● 連結就是將已經讀入記憶體的類別資訊(2 進位)合併到虛擬機的運行時環境;我們可以知道連結有 3 個步驟:驗證、初始化準備、解析
A. 驗證(Verifucation
):
保證被載入的類別有正確的 Class 資料結構!這個步驟可以確認以下事項
● Class 資料結構:確保內容格式是正確的
像是檢查 Class 魔數 就可以知道該文件是否是 Class 文檔
● 語意檢查:以 Java 語言的規範角度去檢查是否語意正確
final 類別不會有繼承、final 方法不會被覆蓋
● ByteCode 驗證:ByteCode 以方法來說,會透過 操作碼的指令 組成方法,在這個步驟會檢查操作碼的合法性
● 二進位相容性驗證:確保相互參考的類別之間的 協調一致
就像是加載的類如果沒有呼叫類別需要的方法時就會拋出
NoSuchMethodError
錯誤

B. 初始化準備(Prepartion
):
● 記憶體空間:為類的靜態變數、靜態區塊分配對應的記憶體空間
● 初始化:賦予靜態變數初始值;這裡的初始化是針對記憶體的初始化
類似使用 C 語言的
memset
函數,將該記憶體區塊全部清為 0
public class PrepareStatic {
// 初始化為 0
private static int apple;
static {
// 初始化為 0,尚未真正初始化!
int banana = 10;
}
}
C. 解析(Resolution
):
JVM 會將類別(.class
)的二進位資料中的 符號參考替換為直接參考值;所謂的 符號是我們程式中撰寫的程式的參考(reference)
● 也就是將「符號引用」轉為「直接引用」(類似 C/C++ 中的重新定位過程)
class Person {
// helloWorld 就是符號,同時也是參考
private final HelloWorld helloWorld = new HelloWorld();
void firstSay() {
// say 也是符號
// 解析階段會將 HelloWorld#say
// 轉換為 HelloWorld#say 放在方法區的位置的指標!
helloWorld.say();
}
}
class HelloWorld {
void say() {
// do somthing...
}
}
類別:初始化階段
這裡的初始化與 連結時期 的初始化不同,這裡的初始化尚未到建構函數(在堆區建構物件時才會呼叫到),這裡針對類(方法區)做初始化
● 按照順序 執行初始化
public class PrepareStatic {
private static int apple;
static {
apple = 10;
}
// 由於按照順序,最後賦予值的設定將為最終設定
static {
apple = 200;
}
public static void main(String[] args) {
System.out.println(PrepareStatic.apple);
}
}
● JVM 初始化一個類時基礎規則為:
● 按順序初始化:如上所述
● 類別自身尚未加載:如果目標類別尚未加載,那會先將該類加載(載入、連結、驗證)再初始化
class SubClz {
static int tmp = 200;
static {
System.out.println("SubClz static block: " + tmp);
}
}
public class PrepareStatic {
public static void main(String[] args) {
System.out.println("Sub Clz tmp: " + SubClz.tmp);
}
}
● 父類尚未加載:如果目標類有父類,並父類也尚未加載:那會先加載父類進方法區並建立堆區的 Class 訊息再初始化,再加載子類(目標類)最後出初始化子類
class ParentClz {
static int tmp = 10;
static {
System.out.println("ParentClz static block: " + tmp);
}
}
// 有父類
class SubClz extends ParentClz {
static int tmp = 200;
static {
System.out.println("SubClz static block: " + tmp);
}
}
public class PrepareStatic {
public static void main(String[] args) {
System.out.println("Sub Clz tmp: " + SubClz.tmp);
}
}
類的加載時機
我們前面有說到,類的加載是在我們 初次 主動 使用類別時,而這個初次主動 主動又有分 的,並不是有使用到就算主動;
主動使用到有分如下行為,這些行為 JVM 才會幫我們進行類加載
啟動類別
● 啟動類別,也就是擁有 Main 函數的類別,在呼叫 java
指令呼叫該類時,JVM 會先將其初始化
public class EntryClz {
// 我們知道類加載後連結(初始化階段)會呼叫靜態區塊
// 以此,我們來就可以確認 EntryClz 是否被加載
static {
System.out.println("Entry Clz has loaded.");
}
public static void main(String[] args) {
System.out.println("Hello Entry Clz");
}
}
● 測試目標:java 命令呼叫 EntryClz
類,讓 JVM 加載該類,並執行內部的 main
方法
A. 使用 javac
將 EntryClz.java
編譯成 EntryClz.class
檔案(編譯指令如下)
javac EntryClz.java
B. 使用 java
執行 EntryClz.class
檔案(執行指令如下)
java EntryClz
創建物件
● 建立類的 instance 物件 也會觸發 JVM 加載一個類
最常見的就是使用
new
關鍵字 創建一個物件,而其中也 包含透過反射創建一個類的 instacnce
A. new
關鍵字 創建物件
public class NewInstance {
public static void main(String[] args) {
// 引用不會觸發類加載
MyClz myClz;
System.out.println("Before create instance");
myClz = new MyClz();
System.out.println("After create instance");
}
}
class MyClz {
static {
System.out.println("MyClz has loaded.");
}
}
● 從結果可以發現:「引用」不會導致類的加載、初始化 (可以從 static 區塊,發現類是否有被加載)
B. 反射 創建物件
public class NewInstance {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
System.out.println("Before create instance");
MyClz.class.newInstance();
System.out.println("After create instance");
}
}
class MyClz {
static {
System.out.println("MyClz has loaded.");
}
}
靜態元素
● 靜態元素 包括如下兩種
● 呼叫 靜態方法
class LoadStaticMethod {
static {
System.out.println("LoadStaticMethod loaded");
}
// 呼叫會觸發加載
static void showMsg() {
System.out.println("HelloWorld");
}
}
● 存取類別的 靜態 變數
class LoadStaticField {
// 存取會觸發加載
static int commonValue = 0;
static {
System.out.println("LoadStaticField loaded");
}
}
● 編譯時期常數
final static
常數,這種常數又稱為 編譯時期常數;會在編譯期間直接做優化,將該常數在使用到的地方直接替代class ConstField { static { System.out.println("ConstField loaded"); } // 呼叫這個 Const Field 不會觸發 JVM 加載該 Class static final boolean IS_DEBUG = true; } public class LoadByStatic { public static void main(String[] args) { System.out.println("Is debug: " + ConstField.IS_DEBUG); } }
訪問
Const Field
不會觸發 JVM 加載該類我們也可以透過 javac、javap 指令來查看 常量得值,會被編譯器直接取代,不會呼叫該類(這是一種編譯器的優化)
● JVM 加載 編譯時期常數 不會為它在方法區創建一塊空間(編譯時期常數分配在其他區域)
編譯時期常數分配在 Java「常數池」(Constant Pool)中,這裡的訪問效率更高
反射呼叫方法
● 透過 Java 反射相關 API 操作類的方法(因為反射會分析堆區中的 Class 物件、方法區的二進制碼,所以必須先載入類)
class ReflectionMethod {
void showMsg() {
System.out.println("Hello World, I am method.");
}
}
public class LoadByReflection {
public static void main(String[] args) throws InstantiationException, IllegalAccessException {
ReflectionMethod.class.newInstance().showMsg();
}
}
● 這裡要提早提及一個有關於 ClassLoader 的蓋念,使用 ClassLoader 加載並不會導致類的初始化(只載入到方法區,尚未載入堆區)
A. 透過以下範例,可以觀察到,如果 單純使用 ClassLoader 並不會導致類的初始化
class HelloWorld {
static {
System.out.println("Hello World class has loaded.");
}
}
public class LoadByClassLoader {
public static void main(String[] args) throws ClassNotFoundException,
InstantiationException, IllegalAccessException {
ClassLoader sysClzLoader = ClassLoader.getSystemClassLoader();
Class<?> clz = sysClzLoader.loadClass("HelloWorld");
System.out.println("Loader class success? " + (clz != null) + "\n");
System.out.println("Load class finish");
clz.newInstance();
}
}
B. 再看看以下範例,我們使用 Class#forName
函數來加載一個類,這時會發現 Class#forName
會觸發類的初始化!
class HelloWorld {
static {
System.out.println("Hello World class has loaded.");
}
}
public class LoadByReflection {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("HelloWorld");
}
}
● 從上面兩個範例可以知道,JVM 加載類不等於類的初始化,這是兩個概念!只有 類被使用之時才會進行初始化
ClassLoader 加載進入 JVM 後不會立即進行初始化。類的初始化只有在需要時才會發生,即當你使用該類的實例、靜態方法、靜態變數等時
ClassLoader 負責類生命週期的前兩個步驟,不一定會有類初始化
初始化子類 & 介面
● 初始化一個類別的子類,那父類也會一起被加載進來(並且先於子類);這裡說的初始化包含創建、呼叫 Static Field、Static Method
● 接下來我們來看幾種狀況
A. 創建子類:會觸發父類加載,再加載子類
class ParentClz {
static int tmp = 10;
static {
System.out.println("ParentClz static block: " + tmp);
}
}
class SubClz extends ParentClz {
static int tmp = 200;
static {
System.out.println("SubClz static block: " + tmp);
}
}
public class PrepareStatic {
public static void main(String[] args) {
System.out.println("Before instance Clz");
new SubClz();
System.out.println("After instance Clz");
}
}
B. 創建父類:如果 只單獨創建父類,那並不會觸發 JVM 加載子類
class ParentClz {
static int tmp = 10;
static {
System.out.println("ParentClz static block: " + tmp);
}
}
class SubClz extends ParentClz {
static int tmp = 200;
static {
System.out.println("SubClz static block: " + tmp);
}
}
public class PrepareStatic {
public static void main(String[] args) {
System.out.println("Before instance Clz");
new ParentClz();
System.out.println("After instance Clz");
}
}
C. 類只會加載一次:如果父類已經被加載,那 JVM 就不會再次加載
class ParentClz {
static int tmp = 10;
static {
System.out.println("ParentClz static block: " + tmp);
}
}
class SubClz extends ParentClz {
static {
System.out.println("First SubClz static block\n");
}
}
class SubClz2 extends ParentClz {
static {
System.out.println("Second static block\n");
}
}
public class PrepareStatic {
public static void main(String[] args) {
System.out.println("Before instance Clz");
new SubClz();
new SubClz2();
System.out.println("After instance Clz");
}
}
● 在 JVM 規範中,JVM 初始化一個類時會要求同時初始化父類,但 不會要求初始化介面
● 在初始化一個類別時,不會先初始化它所實作的介面
● 在初始化一個介面時,不會初始化它的父介面
● 只有當實際使用到介面的成員(如方法、字段)時,相關的介面才會被加載。
這可以降低複雜性、提高類加載的效率
Java ClassLoader 特點
類是透過哪個工具載入的呢?類別是透過 ClassLoader 加載,主要有幾中加載器
● 內建類加載器(JVM)
● 啟動類別加載器
● 擴展類別載入器
● 系統類載入器
● JVM 允許類別載入器(
ClassLoader
) 預料 某個類的載入,而順便載入,如果載入失敗,則拋出錯誤
JVM 內建類加載器
● JVM 有預設幾個內建類加載器:
類加載器 | 特色 | 載入類 | 來源 |
Bootstrap | 沒有父加載器,實現依賴於底層作業系統,並且 不繼承 ClassLoader 類 | 載入虛機的核心類別庫 (java.lang.* … 像是 Object 類) | 系統屬性 sun.boot.class.path |
Extension | 父加載器為 Bootstrap | 放在 jre/lib/ext 目錄下的 jar 檔,會被自動加載 | 系統屬性 java.ext.dirs |
System | 又稱為應用類別載入器,父加載器為 Extension | 載入應用的類 | 系統屬性 java.class.path 或是 classpath 屬性 |
加載順序如下
Custom
是使用者可自訂的類加載器
● 加載器驗證測試範例:
● Bootstrap 加載器:會加載 java 自訂的核心類,像是 Object 類
public static void main(String[] args) throws ClassNotFoundException {
Class<?> objClz = Class.forName("java.lang.Object");
System.out.println("Object classLoader: " + objClz.getClassLoader());
}
● Bootstrap 返回的是 null
從結果看來,Bootstrap 返回的是 null,這可以安全的保護 Bootstrap 不讓其他使用者使用
父委託機制
● 父委託機制(也稱為 雙親委託機制),這是類加載實現的,類加載的核心概念入下:
A. 由使用者自己的類加載器,開始載入
B. 但實做載入的並非是一定是自己,而是由父加載器先嘗試加載
C. 父加載器無法加載,再由子類加載
上面可以看出來,這是 類加載的方式是一種經典的 遞歸結構(加載器、加載器之間是包裝關係,不是 繼承關係)
● 所有加載器都無法加載成功的話?
直到最後如果 Bootstrap 加載器仍無法加載類時,就像呼叫者拋出
ClassNotFoundException
異常
● 這種設計與安全性有啥關係?
透過父加載器先行加載的好處是可以避免使用者(或是惡意程式),載入一個非法的類(或惡意的類),來替換調原先正常的類!
加載器特點:命名空間、執行時套件
● ClassLoader 類加載器有 兩個重要特點
A. 命名空間:有幾個重點概念
● 每個類別載入器都有自己的命名空間!
由命名空間由該載入器、所有的父類載入器共同組成
● 在同一個命名空間中不會有相同的類被再次加載
● 如果是由不同命名空間來加載相同類,那該類就可能初始化多次(被載入多次)
B. 執行時套件:
● 由同個類別載入器載入的屬於相同套件的類別,組成執行時套件
● 決定兩個類是否屬於同一個執行時套件,需要 比較 套件名稱、類別載入器,兩者要都相同才算屬於同執行套件
可以通過兩個 ClassLoader 加載 同一個 Class 文件,而文件可以被加載 2 次(因為不同類加載器,內部的緩存會分開)
● 這套件區分有什麼效果?
只有相同套件中的類可以存取(預設存取級別
Package
)的屬性、函數;這可以限制類的存取
類加載測試
● 準備被加載的類:1. Sample.java
、2. Apple.java
A. Sample.java
類
package classLoader;
public class Sample {
static {
System.out.println("Sample class was loaded by " + Sample.class.getClassLoader());
}
public Sample() {
System.out.println("Sample class was initialize by " + this.getClass().getClassLoader());
new Apple();
}
}
B. Apple.java
類
package classLoader;
public class Apple {
static {
System.out.println("Apple class was loaded by " + Apple.class.getClassLoader());
}
public Apple() {
System.out.println("Apple class was initialize by " + this.getClass().getClassLoader());
}
}
● 自訂類加載器:
我們來自定義一個類加載器,它的功能是讀取指定的 .class
檔,並透過 ClassLoader#defineClass
加載進記憶體中
以下假設我的
.class
檔案會存在/Users/Hy-KylePan/IdeaProjects/JavaTest
下的三個目錄之下(代表了不同 Package)A.
lib_server
目錄B.
lib_client
目錄C.
lib_other
目錄
public class DefineClassLoader extends ClassLoader {
private final String name;
private static final String basePath = "/Users/Hy-KylePan/IdeaProjects/JavaTest";
private String subPath;
public DefineClassLoader(String name) {
super();
this.name = name;
}
public DefineClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
public void setSubPath(String subPath) {
this.subPath = subPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassAsBytes(name);
// Converts an array of bytes into an instance of class `Class`
return defineClass(name, data, 0, data.length);
}
private byte[] loadClassAsBytes(String name) throws ClassNotFoundException {
String totalPath = basePath + subPath + "/" + name;
String fixPath = totalPath.replace(".", "/");
File file = new File(fixPath + ".class");
try(FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int index;
while ((index = fis.read()) != -1) {
baos.write(index);
}
return baos.toByteArray();
} catch (Exception e) {
throw new ClassNotFoundException();
}
}
@Override
public String toString() {
return name;
}
public static void main(String[] args) throws ClassNotFoundException,
InstantiationException, IllegalAccessException {
// 加載器 1
DefineClassLoader serverLoader = new DefineClassLoader("serverLoader");
serverLoader.setSubPath("/lib_server");
serverLoader.loadClass("classLoader.Sample").newInstance();
System.out.println();
// 加載器 2
// 設定 parent 加載器為 serverLoader
DefineClassLoader clientLoader = new DefineClassLoader(serverLoader, "clientLoader");
clientLoader.setSubPath("/lib_client");
clientLoader.loadClass("classLoader.Sample").newInstance();
System.out.println();
// 加載器 3
// 設定 parent 加載器為 null(其實代表了 BootClassLoader)
DefineClassLoader otherLoader = new DefineClassLoader(null, "otherLoader");
otherLoader.setSubPath("/lib_other");
otherLoader.loadClass("classLoader.Sample").newInstance();
System.out.println();
}
}
待加載類 與 類加載器 之間的關係如下圖
驗證:命名空間、父委託
● 在創建完自己的類加載器後,我們首先來驗證類加載器 & 命名空間是否有關;
我們使用以下步驟
A. 編譯多個被加載類:將 Sample.java
、Apple.java
兩個 Java 檔分別編譯到 lib_server
、lib_client
、lib_other
三個資料夾之下…
讓其都存有對應的 .class
檔案
## 各別指令編譯
javac -sourcepath ./src/testClassLoader ./src/classLoader/Apple.java ./src/classLoader/Sample.java -d lib_server
javac -sourcepath ./src/testClassLoader ./src/classLoader/Apple.java ./src/classLoader/Sample.java -d lib_client
javac -sourcepath ./src/testClassLoader ./src/classLoader/Apple.java ./src/classLoader/Sample.java -d lib_other
● 其中的
.class
檔案內容都相同,只次儲存的位置不同這可以用來驗證相同的 Class 會不會應為加載器的不同,而進行多次加載,或是只進行一次加載
B. 編譯自定義的類加載器:將 DefineClassLoader.java
編譯
javac src/classLoader/DefineClassLoader.java
C. 用 java
運行 DefineClassLoader
類
java -classpath ./src classLoader.DefineClassLoader
● 從結果看… 了解 JVM ClassLoader 機制的細節
● JVM 方法區
● 存在兩組相同的 Sample
, Apple
類,但由於是 不同類加載進行加載,所有就算相同類,也會載入 2 次
serverLoader
&otherLoader
會加載類
A. serverLoader
加載:由於委託 BootClassLoader 後仍無法加載目標類,所以再退回 serverLoader
讓它加載,所以緩存會在 serverLoader
這個類加載器中!
BootClass 只加載系統類
B. clientLoader
不會加載,因為 serverLoader
已經加載過,所以會使用 serverLoader
的緩存

C. otherLoader
加載:它的父類加載器為 BootClassLoader(但它無法加載目標類),所以退回到 otherLoader
加載;
BootClass 只加載系統類
● 而儘管加載的目標相同(
Sample
,Apple
類),但類加載器otherLoader
、serverLoader
是不同命名空間,所以 仍會加載兩次這會造成觸發兩次類的初始化
● 由於 clientLoader
加載時受委託機制限制,所以會先去尋找 severLoader
類加載器,而 severLoader
已經加載,所以不會再方法區再建構
● JVM 堆區
● 在讀取類資料方法區時,會同時在堆區建立對應的 Class 對象
● 這是否代表堆區中有兩個相同的 Class 呢?
不會!這是觀察角度問題,以堆區的角度來看 仍是只有一個 Class(也就是我們取得的 Class 物件);但如果以 類加載器來看則是不同 Class 所以會載入兩次
● 不同類別載入器的命名空間還有以下特點
A. 同一個命名空間中的類別是相互可見的
B. 子加載器可以看見父類別的命名空間! 但父載入器的類別,不能看見子類別載入的類別
● 因為子加載器別會有自己的緩存,而在檢查自己的緩存前會先請父加載器檢查它的緩存(反向卻不行);
● 所以可以當作子加載器可見父加載器的類別,又由於類的可見,所以可以確定父、子加載器是相同空間
驗證:執行套件
● 接著我們來驗證,假設 serverLoader
加載器加載成功後,otherLoader
是否可以使用它已經加載的類;
我們使用以下步驟
A. 手動移除 otherLoader
讀取 lib_other/classLoader
資料夾內的內容;移除後 otherLoader
就讀取不到指定類
rm -rf lib_other/classLoader/*
B. 用 java
運行 DefineClassLoader
類
java -classpath ./src classLoader.DefineClassLoader
● 從解果看來
就算是內容相同的 class
檔案,由不同 ClassLoader 加載,也不能在 JVM 中共享;所以會拋出 ClassNotFound
異常
破解命名空間:反射
● 我們知道命名空間可以隔離不同類加載器加載的類之間相互訪問,但是我們 可以使用反射來取得類的實體(取得堆區的 Class 物件)
● 反向搜尋
由 堆區 來找到 方法區,所以就可以忽略 ClassLoader 的加載問題!(但前提是類已經被加載進來)
類的卸載
回收時機:當類在堆區的 Class 不再被參考使用,並且 GC 的可達性分析也無法觸及時,Class 物件就會被回收,並結束類的生命週期;並且類結束生命週期後
● 堆區資料被回收
● 方法區資料也被回收
● 至於 ClassLoader 是否會被回收?
如果是內建(核心)的 ClassLoader 是隨 JVM 生命週期同生共死的(我們自己建立的 ClassLoader 則跟一般類相同)
測試:類加載器、類卸載
● 由於內建類加載器不會被卸載,所以我們這邊使用上面自定義的類加載器(DefineClassLoader
),並透過設置、主動 GC 來觸發回收,查看類是否在方法區被卸載並重新創建
public static void main(String[] args) throws ClassNotFoundException,
InstantiationException, IllegalAccessException, InterruptedException {
// 創建第一個類加載
DefineClassLoader serverLoader = new DefineClassLoader("serverLoader");
serverLoader.setSubPath("/lib_server");
Class<?> sClz = serverLoader.loadClass("classLoader.Sample");
Object obj = sClz.newInstance();
// 查看類在方法區的位置
System.out.println("ClassLoader, Sample clz hashcode :" + obj.getClass().hashCode());
// 將原有的強引用設置為 null,讓其離開可達性分析
serverLoader = null;
sClz = null;
obj = null;
// 手動觸發 GC
System.gc();
Thread.sleep(1000);
System.out.println();
// 創建第二個類加載
serverLoader = new DefineClassLoader("serverLoader");
serverLoader.setSubPath("/lib_server");
sClz = serverLoader.loadClass("classLoader.Sample");
obj = sClz.newInstance();
// 查看類在方法區的位置
System.out.println("ClassLoader recreate, and Sample clz hashcode :" + obj.getClass().hashCode());
}
從結果看來,類確實類重新加載到堆區了!(Class 物件的 hashcode 不同)
補充
URLClassLoader
● 在 JDK 中的 java.net
套件中,有提供一個 URLClassLoader
,它可以從網路上下載一個類,並將其加載進方法區、堆區;以下寫一個使用範例
● 我們可以將一個 class 上傳到雲空間,這裡將上面的
Apple.class
傳到 github 空間
public class UseUrlClassLoader {
public static void main(String[] args) throws MalformedURLException,
ClassNotFoundException, InstantiationException, IllegalAccessException {
URL url = new URL("https://github.com/KylePanHy/MyTest/raw/main/Apple.class");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{
url
});
Class<?> clz = urlClassLoader.loadClass("classLoader.Apple");
Object obj = clz.newInstance();
System.out.println("Class from url, clz name: " + obj.getClass().getName() + ", hashCode: " + obj.hashCode());
}
}
更多的 Java 語言相關文章
Java 語言深入
● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識
此外,我們還涵蓋了物件創建、函數式編程、註解應用以及泛型的深入分析,幫助您提升在實際開發中的技能和效率
點擊以下連結,開始你的學習之旅~
● 深入探索 Java 基礎類型、編碼、浮點數、參考類型和變數作用域 | 探討細節
● 深入了解 Java 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構
● 深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較
● 深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用
● 認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用
Java IO 相關文章
● 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
深入 Java 多執行緒
● 這一系列文章將帶你深入了解 Java 多執行緒技術的各個方面,從基礎知識到進階應用,涵蓋了多執行緒編程的核心概念與實踐
深入 Java 物件導向
● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!