本博客主要參考周志明老師的《深入理解Java虛擬機》第三版
歡迎指出文章的不足之處;更多內容請點進愛敲代碼的小游子查看
虛擬機類加載機制目錄
- 概述
- 一、類加載的時機
- 1、被動參考例子一
- 2、被動參考例子二
- 3、被動參考的例子之三
- 4、介面的加載
- 二、類加載的程序
- 1、加載
- 1、在加載階段,Java虛擬機需要完成以下三件事情:
- 2、獲取定義類的二進制位元組流方式:
- 3、陣列類的加載
- 4、加載階段結束后
- 2、驗證
- 1.檔案格式驗證
- 2.元資料驗證
- 3.位元組碼驗證
- 4.符號參考驗證
- 3、準備
- 4、決議
- 1、符號參考與直接參考
- 1、符號參考:
- 2、直接參考:
- 2、決議階段發生的具體時間
- 3、決議動作
- 1.類或介面的決議
- 2.欄位決議
- 3.方法決議
- 4.介面方法決議
- 5、初始化
- 3、類加載器
- 1、類與類加載器
- 2、雙親委派模型
- 1)三層類加載器
- 1、啟動類加載器(Bootstrap Class Loader)
- 2、擴展類加載器(Extension Class Loader)
- 3、應用程式類加載器(Application Class Loader)
- 2)雙親委派
- 3)雙親委派模型的作業程序
- 4)雙親委派帶來的好處
- 5)雙親委派模型的實作
- 6)類加載的方式
- 3、破壞雙親委派模型
- 4、Java模塊化系統
- 1、模塊化的兼容性
- 1)模塊路徑
- 2)模塊化系統訪問路徑規則
- 3)它本身面臨的模塊間的管理和兼容性問題
- 2、模塊化的類加載器
- 1)模塊化下的類加載器變動
- 2)類加載的委派關系變動
- 3)三個類加載器歸屬關系
概述
Java虛擬機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換決議和初始化,最終形成可以被虛擬機直接使用的Java型別,這個程序被稱作虛擬機的類加載機制,在Java語言里面,型別的加載、連接和初始化程序都是在程式運行期間完成的Java天生可以動態擴展的語言特性就是依賴運行期動態加載和動態連接這個特點實作的,
一、類加載的時機
一個型別從被加載到虛擬機記憶體中開始,到卸載出記憶體為止,它的整個生命周期將會經歷加載 、驗證、準備、決議、初始化 、使用和卸載七個階段,其中驗證、準備、決議三個部分統稱為連接,

1)遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時,如果型別沒有進行過初始化,則需要先觸發其初始化階段,能夠生成這四條指令的典型Java代碼場景有:
- 使用new關鍵字實體化物件的時候,
- 讀取或設定一個型別的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候,
- 呼叫一個型別的靜態方法的時候,
2)使用java.lang.reflect包的方法對型別進行反射呼叫的時候,如果型別沒有進行過初始化,則需 要先觸發其初始化,
3)當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化,
4)當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先 初始化這個主類,
5)當使用JDK 7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實體最后的解 析結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種型別的方法句
柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化,
6)當一個介面中定義了JDK 8新加入的默認方法(被default關鍵字修飾的介面方法)時,如果有 這個介面的實作類發生了初始化,那該介面要在其之前被初始化,
這六種場景中的行為稱為對一個型別進行主動參考,除此之外,所有參考型別的方式都不會觸發初始化,稱為被動參考,
1、被動參考例子一
/**
*被動使用類欄位演示一:
*通過子類參考父類的靜態欄位,不會導致子類初始化
*/
public class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
/**
*SuperClass的子類
*/
public class SubClass extends SuperClass{
static {
System.out.println("SubClass init");
}
}
/** * 非主動使用類欄位演示 **/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
上述代碼運行之后,只會輸出“SuperClass init”,而不會輸出“SubClass init”,
- 對于靜態欄位, 只有直接定義這個欄位的類才會被初始化
- 因此通過其子類來參考父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化,
- 至于是否要觸發子類的加載和驗證階段取決于虛擬機的具體實作,
- 對于HotSpot虛擬機來說,可通過*-XX:+TraceClassLoading*引數觀察到此操作是會導致子類加載的,

2、被動參考例子二
/**
* 被動使用類欄位演示二: 通過陣列定義來參考類,不會觸發此類的初始化
* @param args
*/
public static void main(String[] args) {
SuperClass[] superClass = new SuperClass[10];
}
發現沒有輸出“SuperClass init”
-
說明并沒有觸發類com.yky.classLoadingTime.SuperClass的初始化階段,
-
但是這段代碼里面觸發了 另一個名為類的初始化階段,它是一個由虛擬機自動生成的、直接繼承于java.lang.Object的子類,創建動作由 位元組碼指令newarray觸發,
-
這個類代表了一個元素型別為com.yky.classLoadingTime.SuperClass的一維陣列,陣列中應有的屬性和方法(用戶可直接使用的只有被修飾為public的length屬性和clone()方法)都實作在這個類里,
-
Java語言中對陣列的訪問要比C/C++相對安全,很大程度上就是因為這個類包裝了陣列元素的訪問,而C/C++中則是直接翻譯為對陣列指標的移動,
-
在Java語言里,當檢查到發生陣列越界時會拋出java.lang.ArrayIndexOutOfBoundsException例外,避免了直接造成非法記憶體訪問,

3、被動參考的例子之三
public class ConstClass {
/**
* 被動使用類欄位演示三:
* 常量在編譯階段會存入呼叫類的常量池中,本質上沒有直接參考到定義常量的類,
* 因此不會觸發定義常量的類的初始化
* @param args
*/
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORLD = "hello world";
}
//非主動使用類欄位演示
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
上述代碼運行之后,只輸出“hello world”,沒有輸出“ConstClass init”
- 這是因為雖然在Java原始碼中確實參考了ConstClass類的常量HELLOWORLD,但其實在編譯階段通過常量傳播優化,已經將此常量的值“hello world”直接存盤在NotInitialization類的常量池中以后NotInitialization對常量 ConstClass.HELLOWORLD的參考,實際都被轉化為NotInitialization類對自身常量池的參考了,
- 也就是說,實際上NotInitialization的Class檔案之中并沒有ConstClass類的符號參考入口,這兩個類在編譯成Class檔案后就已不存在任何聯系了,
4、介面的加載
- 介面的加載程序與類加載程序稍有不同,針對介面需要做一些特殊說明:介面也有初始化程序, 這點與類是一致的,上面的代碼都是用靜態陳述句塊
static{}來輸出初始化資訊的介面中不能使用static{}陳述句塊,但編譯器仍然會為介面生成<clinit>()類構造器,用于初始化介面中所定義的成員變數, - 介面與類真正有所區別的是前面講述的六種“有且僅有”需要觸發初始化場景中的第三種: 當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個介面在初始化時,并不要求其父介面全部都完成了初始化,只有在真正使用到父介面的時候(如參考介面中定義的常量)才會初始化,
二、類加載的程序
1、加載

1、在加載階段,Java虛擬機需要完成以下三件事情:
- 通過一個類的全限定名來獲取定義此類的二進制位元組流,
- 將這個位元組流所代表的靜態存盤結構轉化為方法區的運行時資料結構,
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口,
2、獲取定義類的二進制位元組流方式:
- 從ZIP壓縮包中讀取
這很常見,最終成為日后JAR、EAR、WAR格式的基礎,
- 從網路中獲取
這種場景最典型的應用就是Web Applet,
- 運行時計算生成
這種場景使用得最多的就是動態代理技術,在java.lang.reflect.Proxy中,就是用
ProxyGenerator.generateProxyClass()來為特定介面生成形式為“*$Proxy”的代理類的二進制位元組流,
- 由其他檔案生成
典型場景是JSP應用,由JSP檔案生成對應的Class檔案,
- 從資料庫中讀取
有些中間件服務器(如SAP Netweaver)可以選擇把程式安裝到資料庫中來完成程式代碼在集群間的分發,
- 可以從加密檔案中獲取
這是典型的防Class檔案被反編譯的保護措施,通過加載時解密Class檔案來保障程式運行邏輯不被窺探,
3、陣列類的加載
陣列類本身不通過類加載器創建,它是由Java虛擬機直接在記憶體中動態構造出來的,但陣列類與類加載器仍然有很密切的關系,因為陣列類的元素型別最侄訓是要靠類加載器來完成加載
一個陣列類(簡稱為C)創建程序遵循以下規則:
- 如果陣列的組件型別(指的是陣列去掉一個維度的型別)是參考型別,那就遞回采用本節中定義的加載程序去加載這個組件型別,陣列C將被標識在加載該組件型別的類加載器的類名稱空間上(一個型別必須與類加載器一起確定唯一性),
- 如果陣列的組件型別不是參考型別(例如int[]陣列的組件型別為int),Java虛擬機將會把陣列C標記為與引導類加載器關聯,
- 陣列類的可訪問性與它的組件型別的可訪問性一致,如果組件型別不是參考型別,它的陣列類的可訪問性將默認為public,可被所有的類和介面訪問到,
4、加載階段結束后
Java虛擬機外部的二進制位元組流就按照虛擬機所設定的格式存盤在方法區之中了,方法區中的資料存盤格式完全由虛擬機實作自行定義,型別資料妥善安置在方法區之后,會在Java堆記憶體中實體化一個java.lang.Class類的物件, 這個物件將作為程式訪問方法區中的型別資料的外部介面,
加載階段與連接階段的部分動作(如一部分位元組碼檔案格式驗證動作)是交叉進行的,加載階段尚未完成,連接階段可能已經開始,但這些夾在加載階段之中進行的動作,仍然屬于連接階段的一部分,這兩個階段的開始時間仍然保持著固定的先后順序,
2、驗證
驗證是連接階段的第一步,這一階段的目的是確保Class檔案的位元組流中包含的資訊符合《Java虛 擬機規范》的全部約束要求,保證這些資訊被當作代碼運行后不會危害虛擬機自身的安全,

驗證階段分為四個階段的檢驗動作:檔案格式驗證、元資料驗證、位元組 碼驗證和符號參考驗證,
1.檔案格式驗證
第一階段要驗證位元組流是否符合Class檔案格式的規范,并且能被當前版本的虛擬機處理,
這一階段可能包括下面這些驗證點:
- 是否以魔數0xCAFEBABE開頭,
- 主、次版本號是否在當前Java虛擬機接受范圍之內,
- 常量池的常量中是否有不被支持的常量型別(檢查常量tag標志),
- 指向常量的各種索引值中是否有指向不存在的常量或不符合型別的常量,
- CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的資料,
- Class檔案中各個部分及檔案本身是否有被洗掉的或附加的其他資訊,
- 該驗證階段的主要目的是保證輸入的位元組流能正確地決議并存盤于方法區之內,格式上符合描述一個Java型別資訊的要求,
- 這階段的驗證是基于二進制位元組流進行的,只有通過了這個階段的驗證之后,這段位元組流才被允許進入Java虛擬機記憶體的方法區中進行存盤
- 后面的三個驗證階段全部是基于方法區的存盤結構上進行的,不會再直接讀取、操作位元組流了,
2.元資料驗證
第二階段的主要目的是對類的元資料資訊進行語意校驗,保證不存在與《Java語言規范》定義相悖的元資料資訊,
這個階段可能包括的驗證點如下:
- 這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類),
- 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類),
- 如果這個類不是抽象類,是否實作了其父類或介面之中要求實作的所有方法,
- 類中的欄位、方法是否與父類產生矛盾,
3.位元組碼驗證
第三階段是整個驗證程序中最復雜的一個階段,主要目的是通過資料流分析和控制流分析,確定程式語意是合法的、符合邏輯的,在第二階段對元資料資訊中的資料型別校驗完畢以后,這階段就要對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為
- 保證任意時刻運算元堆疊的資料型別與指令代碼序列都能配合作業,例如不會出現類似于“在操作 堆疊放置了一個int型別的資料,使用時卻按long型別來加載入本地變數表中”這樣的情況,
- 保證任何跳轉指令都不會跳轉到方法體以外的位元組碼指令上,
- 保證方法體中的型別轉換總是有效的,例如可以把一個子類物件賦值給父類資料型別,這是安全的,但是把父類物件賦值給子類資料型別,甚至把物件賦值給與它毫無繼承關系、完全不相干的一個資料型別,則是危險和不合法的,
4.符號參考驗證
最后一個階段的校驗行為發生在虛擬機將符號參考轉化為直接參考的時候,這個轉化動作將在連接的第三階段——決議階段中發生,
符號參考驗證可以看作是對類自身以外(常量池中的各種符號參考)的各類資訊進行匹配性校驗,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、欄位等資源,本階段通常需要校驗下列內容:
- 符號參考中通過字串描述的全限定名是否能找到對應的類,
- 在指定類中是否存在符合方法的欄位描述符及簡單名稱所描述的方法和欄位,
- 符號參考中的類、欄位、方法的可訪問性(
private、protected、public、<package>)是否可被當前類訪問,
符號參考驗證的主要目的是確保決議行為能正常執行,如果無法通過符號參考驗證,Java虛擬機 將會拋出一個java.lang.IncompatibleClassChangeError的子類例外,典型的如: java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等,
-Xverify:none引數可以關閉大部分的類驗證措施,以縮短虛擬機類加載的時間,
3、準備
正式為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體并設定類變數初始值的階段

- 進行記憶體分配的僅包括類變數,而不包括實體變數,實體變數將會在物件實體化時隨著物件一起分配在Java堆中,
- 這里所說的初始值“通常情況”下是資料型別的零值
public static int value = 123;
value在準備階段過后的初始值為0而不是123,因為這時尚未開始執行任何Java方法,而把 value賦值為123的putstatic指令是程式被編譯后,存放于類構造器()方法之中,所以把value賦值為123的動作要到類的初始化階段才會被執行,

- Java中所有基本資料型別的零值

- 如果類欄位 的欄位屬性表中存在ConstantValue屬性,那在準備階段變數值就會被初始化為ConstantValue屬性所指定的初始值
假設上面類變數value的定義修改為:
public static final int value = 123;
4、決議
決議階段是Java虛擬機將常量池內的符號參考替換為直接參考的程序

1、符號參考與直接參考
1、符號參考:
- 符號參考以一組符號來描述所參考的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可,
- 符號參考與虛擬機實作的記憶體布局無關,參考的目標并不一定是已經加載到虛擬機記憶體當中的內容,各種虛擬機實作的記憶體布局可以各不相同, 但是它們能接受的符號參考必須都是一致的,因為符號參考的字面量形式明確定義在《Java虛擬機規 范》的Class檔案格式中,
2、直接參考:
直接參考是可以直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的句柄,
- 直接參考是和虛擬機實作的記憶體布局直接相關的,同一個符號參考在不同虛擬機實體上翻譯出來的直接參考一般不會同,如果有了直接參考,那參考的目標必定已經在虛擬機的記憶體中存在,
2、決議階段發生的具體時間
- 虛擬機實作可以根據需要來自行判斷,到底是在類被加載器加載時就對常量池中的符號參考進行決議,還是等到一個符號參考將要被使用前才去決議它,
- 除invokedynamic指令以外,虛擬機實作可以對第一次決議的結果進行快取,譬如在運行時直接參考常量池中的記錄,并把常量標識為已決議狀態,從而避免決議動作重復進行,
- 當碰到某個前面已經由invokedynamic指令觸發過決議的符號參考時,并不意味著這個決議結果對于其他invokedynamic指令也同樣生效,因為invokedynamic指令對應的參考稱為動態呼叫點限定符
3、決議動作
決議動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法句柄和呼叫點限定符這7 類符號參考進行
1.類或介面的決議
假設當前代碼所處的類為D,如果要把一個從未決議過的符號參考N決議為一個類或介面C的直接參考
那虛擬機完成整個決議的程序需要包括以下3個步驟:
- 如果C不是一個陣列型別,那虛擬機將會把代表N的全限定名傳遞給D的類加載器去加載這個類C,在加載程序中,由于元資料驗證、位元組碼驗證的需要,又可能觸發其他相關類的加載動作,例如加載這個類的父類或實作的介面,一旦這個加載程序出現了任何例外,決議程序就將宣告失敗,
- 如果C是一個陣列型別,并且陣列的元素型別為物件,也就是N的描述符會是類似“[Ljava/lang/Integer”的形式,那將會按照第一點的規則加載陣列元素型別,如果N的描述符如前面所假設的形式,需要加載的元素型別就是“java.lang.Integer”,接著由虛擬機生成一個代表該陣列維度和元素的陣列物件,
- 如果上面兩步沒有出現任何例外,那么C在虛擬機中實際上已經成為一個有效的類或介面了,但在決議完成前還要進行符號參考驗證,確認D是否具備對C的訪問權限,如果發現不具備訪問權限,將拋出java.lang.IllegalAccessError例外,
-
JDK 9引入了模塊化以后,我們還必須檢查模塊間的訪問權限,
- 被訪問類C是public的,并且與訪問類D處于同一個模塊,
- 被訪問類C是public的,不與訪問類D處于同一個模塊,但是被訪問類C的模塊允許被訪問類D的模塊進行訪問,
- 被訪問類C不是public的,但是它與訪問類D處于同一個包中,
2.欄位決議
要決議一個未被決議過的欄位符號參考,首先將會對欄位表內class_index項中索引的CONSTANT_Class_info符號參考進行決議,也就是欄位所屬的類或介面的符號參考,
如果決議成功完成,那把這個欄位所屬的類或介面用C表示
對C進行后續欄位的搜索:
- 如果C本身就包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則回傳這個欄位的直接參考,查找結束,
- 否則,如果在C中實作了介面,將會按照繼承關系從下往上遞回搜索各個介面和它的父介面,如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則回傳這個欄位的直接參考,查找 結束,
- 否則,如果C不是java.lang.Object的話,將會按照繼承關系從下往上遞回搜索其父類,如果在父類中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則回傳這個欄位的直接參考,查找結束,
- 否則,查找失敗,拋出
java.lang.NoSuchFieldError例外,
如果查找程序成功回傳了參考,將會對這個欄位進行權限驗證,如果發現不具備對欄位的訪問權限,將拋出
java.lang.IllegalAccessError例外,

3.方法決議
方法決議的第一個步驟與欄位決議一樣,也是需要先決議出方法表的class_index項中索引的方法所屬的類或介面的符號參考,如果決議成功,那么我們依然用C表示這個類
接下來虛擬機將會按 照如下步驟進行后續的方法搜索:
- 由于Class檔案格式中類的方法和介面的方法符號參考的常量型別定義是分開的,如果在類的方法表中發現class_index中索引的C是個介面的話,那就直接拋出java.lang.IncompatibleClassChangeError 例外,
- 如果通過了第一步,在類C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束,
- 否則,在類C的父類中遞回查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束, 4
- 否則,在類C實作的介面串列及它們的父介面之中遞回查找是否有簡單名稱和描述符都與目標相匹配的方法,如果存在匹配的方法,說明類C是一個抽象類,這時候查找結束,拋出 java.lang.AbstractMethodError例外,
- 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError,
最后,如果查找程序成功回傳了直接參考,將會對這個方法進行權限驗證,如果發現不具備對此方法的訪問權限,將拋出
java.lang.IllegalAccessError例外,
4.介面方法決議
介面方法也是需要先決議出介面方法表的class_index項中索引的方法所屬的類或介面的符號引 用,如果決議成功,依然用C表示這個介面,接下來虛擬機將會按照如下步驟進行后續的介面方法搜索:
- 與類的方法決議相反,如果在介面方法表中發現class_index中的索引C是個類而不是介面,那么就直接拋出java.lang.IncompatibleClassChangeError例外,
- 否則,在介面C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束,
- 否則,在介面C的父介面中遞回查找,直到java.lang.Object類(介面方法的查找范圍也會包括Object類中的方法)為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方 法的直接參考,查找結束,
- 對于規則3,由于Java的介面允許多重繼承,如果C的不同父介面中存有多個簡單名稱和描述符都與目標相匹配的方法,那將會從這多個方法中回傳其中一個并結束查找,《Java虛擬機規范》中并沒有進一步規則約束應該回傳哪一個介面方法,但與之前欄位查找類似地,不同發行商實作的Javac編譯器有可能會按照更嚴格的約束拒絕編譯這種代碼來避免不確定性,
- 否則,宣告方法查找失敗,拋出java.lang.NoSuchMethodError例外,
從JDK 9起,介面方法的訪問也完全有可能因訪問權限控制而出現java.lang.IllegalAccessError例外
5、初始化
直到初始化階段,Java虛擬機才真正開始執行類中撰寫的Java程式代碼,將主導權移交給應用程 序,
在初始化階段,則會根據程式員通 程序式編碼制定的主觀計劃去初始化類變數和其他資源,我們也可以從另外一種更直接的形式來表達:初始化階段就是執行類構造器()方法的程序,
- ()方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態陳述句塊(static{}塊)中的陳述句合并產生的,編譯器收集的順序是由陳述句在源檔案中出現的順序決定的,靜態陳述句塊中只能訪問到定義在靜態陳述句塊之前的變數,定義在它之后的變數,在前面的靜態陳述句塊可以賦值,但是不能訪問

<clinit>()方法與類的建構式不同,它不需要顯式地呼叫父類構造器,Java虛擬機會保證在子類的<clinit>()方法執行前,父類的<clinit>()方法已經執行完畢,因此在Java虛擬機中第一個被執行的<clinit>()方法的型別肯定是java.lang.Object,- 由于父類的
<clinit>()方法先執行,也就意味著父類中定義的靜態陳述句塊要優先于子類的變數賦值操作
<clinit>()方法對于類或介面來說并不是必需的,如果一個類中沒有靜態陳述句塊,也沒有對變數的 賦值操作,那么編譯器可以不為這個類生成<clinit>()方法,- 介面中不能使用靜態陳述句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成()方法,但介面與類不同的是,執行介面的
<clinit>()方法不需要先執行父介面的<clinit>()方法,因為只有當父介面中定義的變數被使用時,父介面才會被初始化,此外,介面的實作類在初始化時也一樣不會執行介面的<clinit>()方法, - Java虛擬機必須保證一個類的
<clinit>()方法在多執行緒環境中被正確地加鎖同步,如果多個執行緒同時去初始化一個類,那么只會有其中一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行完畢<clinit>()方法,如果在一個類的<clinit>()方法中有耗時很長的操作,那就可能造成多個行程阻塞,在實際應用中這種阻塞往往是很隱蔽的,
static class DeadLoopClass {
static { // 如果不加上這個if陳述句,編譯器將提示“Initializer does not complete normally” 并拒絕編譯
if (true) {
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while (true) {
}
}
}
}
public static void main(String[] args) {
Runnable script = new Runnable() {
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + " run over");
}
};
Thread thread1 = new Thread(script);
Thread thread2 = new Thread(script);
thread1.start();
thread2.start();
}
D:\JAVA\develop\jdk-1.8\bin\java.exe -javaagent:C:\Users\ASUS\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\203.6682.168\lib\idea_rt.jar=63399:C:\Users\ASUS\AppData\Local\JetBrains\Toolbox\apps\IDEA-U\ch-0\203.6682.168\bin -Dfile.encoding=UTF-8 -classpath D:\JAVA\develop\jdk-1.8\jre\lib\charsets.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\deploy.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\access-bridge-64.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\cldrdata.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\dnsns.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\jaccess.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\jfxrt.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\localedata.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\nashorn.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\sunec.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\sunjce_provider.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\sunmscapi.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\sunpkcs11.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\ext\zipfs.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\javaws.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\jce.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\jfr.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\jfxswt.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\jsse.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\management-agent.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\plugin.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\resources.jar;
D:\JAVA\develop\jdk-1.8\jre\lib\rt.jar;
D:\springbootproject\jdk8demo\out\production\constantPool com.yky.classLoadingTime.NotInitialization
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]init DeadLoopClass
3、類加載器
Java虛擬機設計團隊有意把類加載階段中的“通過一個類的全限定名來獲取描述該類的二進制位元組 流”這個動作放到Java虛擬機外部去實作,以便讓應用程式自己決定如何去獲取所需的類,實作這個動作的代碼被稱為“類加載器”(Class Loader),

1、類與類加載器
對于任意一個類,都必須由加載它的類加載器和這個類本身一起共同確立其在Java虛擬機中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間,
- 比較兩個類是否“相 等”,只有在這兩個類是由同一個類加載器加載的前提下才有意義
public class NotInitialization {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.yky.classLoadingTime.NotInitialization").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.yky.classLoadingTime.NotInitialization);
}
}
運行結果:
原因:
這是因為Java虛擬機中同時存在了兩個ClassLoaderTest類,一個是由虛擬機的應用程式類加載器所加載的,另外一個是由我們自定義的類加載器加載的,雖然它們都來自同一 個Class檔案,但在Java虛擬機中仍然是兩個互相獨立的類,做物件所屬型別檢查時的結果自然為false,
2、雙親委派模型
三層類加載器、雙親委派的類加載架構
1)三層類加載器
1、啟動類加載器(Bootstrap Class Loader)
- 這個類加載器負責加載存放在 <JAVA_HOME>\lib目錄,或者被-Xbootclasspath引數所指定的路徑中存放的,而且是Java虛擬機能夠識別的(按照檔案名識別,如rt.jar、tools.jar,名字不符合的類別庫即使放在lib目錄中也不會被加載)類別庫加載到虛擬機的記憶體中,
- 啟動類加載器無法被Java程式直接參考,用戶在撰寫自定義類加載器時, 如果需要把加載請求委派給引導類加載器去處理,那直接使用null代替即可
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}
static void checkClassLoaderPermission(ClassLoader cl, Class<?> caller) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// caller can be null if the VM is requesting it
ClassLoader ccl = getClassLoader(caller);
if (needsClassLoaderPermissionCheck(ccl, cl)) {
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
2、擴展類加載器(Extension Class Loader)
這個類加載器是在類sun.misc.Launcher$ExtClassLoader 中以Java代碼的形式實作的,它負責加載<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中所有的類別庫,
3、應用程式類加載器(Application Class Loader)
- 這個類加載器由 sun.misc.Launcher$AppClassLoader來實作,由于應用程式類加載器是ClassLoader類中的getSystemClassLoader()方法的回傳值,所以有些場合中也稱它為“系統類加載器”,
- 它負責加載用戶類路徑 (ClassPath)上所有的類別庫,開發者同樣可以直接在代碼中使用這個類加載器,
- 如果應用程式中沒有自定義過自己的類加載器,一般情況下這個就是程式中默認的類加載器,
JDK 9之前的Java應用都是由這三種類加載器互相配合來完成加載的,還可以加入自定義的類加載器來進行拓展,典型的如增加除了磁盤位置之外的Class檔案來源,或者通過類加載器實作類的隔離、多載等功能,
2)雙親委派
圖中展示的各種類加載器之間的層次關系被稱為類加載器的“雙親委派模型(Parents Delegation Model)”,
- 雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載 器,
- 通常使用 組合(Composition)關系來復用父加載器的代碼,
3)雙親委派模型的作業程序
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加 載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到最頂層的啟動類加載中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去完成加載,
4)雙親委派帶來的好處
Java中的類隨著它的類加載器一起具備了一種帶有優先級的層次關系,
- 避免重復加載;例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,因此Object類 在程式的各種類加載器環境中都能夠保證是同一個類,
- 更安全;如果沒有使用雙親委派模型,都由各個類加載器自行去加載的話,如果用戶自己也撰寫了一個名為java.lang.Object的類,并放在程式的 ClassPath中,那系統中就會出現多個不同的Object類,Java型別體系中最基礎的行為也就無從保證,應用程式將會變得一片混亂,
5)雙親委派模型的實作
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 首先檢查這個類是否已經被加載過了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父類拋出ClassNotFoundException說明父類無法完成類加載請求
}
if (c == null) {
//如果父類加載器仍然無法加載時,按順序呼叫findClass繼續進行加載
long t1 = System.nanoTime();
c = findClass(name);
//這是定義類加載器;記錄統計資料
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
代碼的邏輯:
先檢查請求加載的型別是否已經被加載若沒有則呼叫父加載器的 loadClass()方法,若父加載器為空則默認使用啟動類加載器作為父加載器,假如父類加載器加載失敗,拋出ClassNotFoundException例外的話,才呼叫自己的findClass()方法嘗試進行加載,
6)類加載的方式
- 通過命令列啟動應用時由JVM初始化加載含有main()方法的主類,
- 通過Class.forName()方法動態加載,會默認執行初始化塊(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要執行初始化塊,
- 通過ClassLoader.loadClass()方法動態加載,不會執行初始化塊,
3、破壞雙親委派模型
雙親模型有個問題:父加載器無法向下識別子加載器加載的資源,
- 如下證明 JDBC 是啟動類加載器加載,但 mysql 驅動是應用類加載器,而 JDBC 運行時又需要去訪問子類加載器加載的驅動,就破壞了該模型,

JDK 自己為解決該問題,引入執行緒上下問類加載器,可以通過Thread的setContextClassLoader()進行設定
- 當為啟動類加載器時,使用當前實際加載驅動類的類加載器

- 熱替換
比如OSGI的模塊化熱部署,它的類加載器就不再是嚴格按照雙親委派模型,很多可能就在平級的類加載器中執行了,
OSGi實作模塊化熱部署的關鍵是它自定義的類加載器機制的實作,每一個程式模塊(OSGi中稱為 Bundle)都有一個自己的類加載器,當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實 現代碼的熱替換,在OSGi環境下,類加載器不再雙親委派模型推薦的樹狀結構,而是進一步發展為更加復雜的網狀結構,當收到類加載請求時,OSGi將按照下面的順序進行類搜索:
- 將以java.*開頭的類,委派給父類加載器加載
- 否則,將委派串列名單內的類,委派給父類加載器加載
- 否則,將Import串列中的類,委派給Export這個類的Bundle的類加載器加載
- 否則,查找當前Bundle的ClassPath,使用自己的類加載器加載
- 否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載
- 否則,查找Dynamic Import串列的Bundle,委派給對應Bundle的類加載器加載
- 否則,類查找失敗,
4、Java模塊化系統

JDK9開始引入,目的:為了能夠實作模塊化的關鍵目標:可配置的封裝隔離機制,
可配置的封裝隔離機制主要解決:
- 首先要解決JDK9之前基于類路徑(ClassPath)來查找依賴的可靠性問題;
- 還解決了原來類路徑上跨JAR檔案的public型別的可訪問性問題,
JDK 9的模塊不僅僅像之前的JAR包那樣只是簡單地充當代碼的容器,除了代碼外,Java的模塊定義還包含以下內容:
- 依賴其他模塊的串列,
- 匯出的包串列,即其他模塊可以使用的串列,
- 開放的包串列,即其他模塊可反射訪問模塊的串列,
- 使用的服務串列,
- 提供服務的實作串列,
1、模塊化的兼容性
1)模塊路徑
為了使可配置的封裝隔離機制能夠兼容傳統的類路徑查找機制,JDK 9提出了“模塊路徑”(ModulePath)的概念,
- 某個類別庫到底是模塊還是傳統的JAR包,只取決于它存放在哪種路徑上,
- 只要是放在類路徑上的JAR檔案,無論其中是否包含模塊化資訊(是否包含了module-info.class檔案),它都會被當作傳統的JAR包來對待;
- 相應地,只要放在模塊路徑上的JAR檔案,即使沒有使JMOD后綴,甚至說其中并不包含module-info.class檔案,它也仍然會被當作一個模塊來對待,
2)模塊化系統訪問路徑規則
模塊化系統將按照以下規則來保證使用傳統類路徑依賴的Java程式可以不經修改地直接運行在 JDK 9及以后的Java版本上:
- 模塊在模塊路徑的訪問規則:模塊路徑下的具名模塊(Named Module)只能訪問到它依賴定義中列明依賴的模塊和包,匿名模塊里所有的內容對具名模塊來說都是不可見的,即具名模塊看不見傳統JAR包的內容,
- JAR檔案在類路徑的訪問規則:所有類路徑下的JAR檔案及其他資源檔案,都被視為自動打包在一個匿名模塊(Unnamed Module)里,這個匿名模塊幾乎是沒有任何隔離的,它可以看到和使用類路徑上所有的包、JDK系統模塊中所有的匯出包,以及模塊路徑上所有模塊中匯出的包,
- JAR檔案在模塊路徑的訪問規則:如果把一個傳統的、不包含模塊定義的JAR檔案放置到模塊路徑中,它就會變成一個自動模塊(Automatic Module),盡管不包含module-info.class,但自動模塊將默認依賴于整個模塊路徑中的所有模塊,因此可以訪問到所有模塊匯出的包,自動模塊也默認匯出自己所有的包,
3)它本身面臨的模塊間的管理和兼容性問題
- 如果同一個模塊發行了多個不同的版本,那只能由開發者在編譯打包時人工選擇好正確版本的模塊來保證依賴的正確性,
- Java模塊化系統目前不支持在模塊定義中加入版本號來管理和約束依賴,本身也不支持多版本號的概念和版本選擇功能,
- 我們不論是在Java命令、Java類別庫的API抑或是《Java 虛擬機規范》定義的Class檔案格式里都能輕易地找到證據,表明模塊版本應是編譯、加載、運行期間都可以使用的,譬如輸入
java --list-modules,會得到明確帶著版本號的模塊串列:
java.base@13.0.2
java.compiler@13.0.2
java.datatransfer@13.0.2
java.desktop@13.0.2
java.instrument@13.0.2
java.logging@13.0.2
java.management@13.0.2
java.management.rmi@13.0.2
java.naming@13.0.2
java.net.http@13.0.2
......
OSGi – JPMS互操作性概念驗證
2、模塊化的類加載器
為了保證兼容性,JDK 9并沒有從根本上動搖從JDK 1.2以來運行了二十年之久的三層類加載器架構以及雙親委派模型,
1)模塊化下的類加載器變動
模塊化下的類加載器仍然發生了一些應該被注意到變動,主要包括以下幾個方面:
- 擴展類加載器(Extension Class Loader)被平臺類加載器(Platform Class Loader)取代
- 平臺類加載器和應用程式類加載器都不再派生自
java.net.URLClassLoader - 現在啟動類加載器、平臺類加載器、應用程式類加載器全都繼承于
jdk.internal.loader.BuiltinClassLoader
![]() | ![]() |
|---|
2)類加載的委派關系變動
JDK9中雖然仍然維持著三層類加載器和雙親委派的架構,但類加載的委派關系也發生了變動,
當平臺及應用程式類加載器收到類加載請求,在委派給父加載器加載前,要先判斷該類是否能夠歸屬到某一個系統模塊中,如果可以找到這樣的歸屬關系,就要優先委派給負責那個模塊的加載器完成加載,也許這可以算是對雙親委派的第四次破壞,
3)三個類加載器歸屬關系
在Java模塊化系統明確規定了三個類加載器負責各自加載的模塊,即前面所說的歸屬關系
- 啟動類加載器負責加載的模塊:
java.base java.datatransfer
java.desktop java.instrument
java.logging java.management
java.management.rmi java.naming
java.prefs java.rmi
java.security.sasl java.xml
jdk.httpserver jdk.internal.vm.ci
jdk.management jdk.management.agent
jdk.naming.rmi jdk.net
jdk.unsupported jdk.sctp
- 平臺類加載器負責加載的模塊:
java.activation* java.compiler*
java.scripting java.se
java.se.ee java.corba*
java.security.jgss java.smartcardio
java.sql.rowset java.xml.bind*
java.xml.crypto java.xml.ws*
java.xml.ws.annotation* java.transaction*
jdk.accessibility jdk.charsets
jdk.crypto.cryptoki jdk.crypto.ec
jdk.dynalink jdk.incubator.httpclient
jdk.internal.vm.compiler* jdk.jsobject
jdk.localedata jdk.naming.dns
jdk.scripting.nashorn jdk.security.auth
jdk.security.jgss jdk.xml.dom
jdk.zipfs java.sql
- 應用程式類加載器負責加載的模塊:
jdk.aot jdk.attach
jdk.compiler jdk.editpad
jdk.hotspot.agent jdk.internal.ed
jdk.internal.jvmstat jdk.internal.le
jdk.internal.opt jdk.jartool
jdk.javadoc jdk.jcmd
jdk.jconsole jdk.jdeps
jdk.jdi jdk.jdwp.agent
jdk.jlink jdk.jshell
jdk.jstatd jdk.pack
jdk.policytool jdk.rmic
jdk.scripting.nashorn.shell jdk.xml.bind*
jdk.xml.ws*
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/253147.html
標籤:其他


