類裝載器子系統,作為JVM最底層的一部分。
概述
用途:用來加載Class檔案到JVM的方法區,在方法區中建立一個
java.lang.Class
對象(後面簡稱類對象)通過此執行個體的
newInstance()
方法就可以建立出該類的一個對象。
比較兩個類是否相等,隻有當這兩個類由同一個加載器加載才有意義;否則即使同一個 class 檔案被不同的類加載器加載,那這兩個類必定不同,即通過類的 Class 對象的 equals 執行的結果必為false。
分類
JVM提供三種類加載器:
- 啟動類Bootstrap加載器:負責加載
中,或通過Java_Home\lib
參數指定路徑中的,且被虛拟機認可(按檔案名識别,如rt.jar)的class檔案。Java的核心類都由該ClassLoader加載。在Sun JDK中,這個類加載器是由C++實作的,并且在Java語言中無法獲得它的引用。-Xbootclasspath
- 擴充類Extension加載器:負責加載
目錄下,或通過Java_Home\lib\ext
系統變量指定路徑中的class檔案;java.ext.dirs
- 應用程式類Application加載器:負責加載使用者 classpath 下的 class 檔案。可通過
來擷取。ClassLoader.getSystemClassLoader()
可通過繼承
java.lang.ClassLoader
類的方式實作自定義類裝載器。ClassLoader源碼簡略版:
public abstract class ClassLoader {
// 傳回該類加載器的父類加載器
public final ClassLoader getParent();
// 加載名稱為name的類,傳回java.lang.Class類的執行個體
public Class<?> loadClass(String name);
// 查找名稱為name的類,傳回java.lang.Class類的執行個體
protected Class<?> findClass(String name);
// 查找名稱為name的已經被加載過的類,傳回java.lang.Class類的執行個體
protected final Class<?> findLoadedClass(String name);
// 把位元組數組b中的内容轉換成Java類,傳回java.lang.Class類的執行個體
protected final Class<?> defineClass(String name, byte[] b, int off, int len);
// 連結指定的Java類
protected final void resolveClass(Class<?> c);
}
雙親委派模型
Parent Delegation Model。
工作過程:如果一個類加載器收到加載類的請求,它首先将請求交由父類加載器加載;若父類加載器加載失敗,目前類加載器才會自己加載類。
作用:像
java.lang.Object
這些存放在
rt.jar
中的類,無論使用哪個類加載器加載,最終都會委派給最頂端的啟動類加載器加載,進而使得不同加載器加載的
Object
類都是同一個。
原理:雙親委派模型的代碼在
java.lang.ClassLoader.loadClass()
方法中實作:
- 首先檢查類是否被加載;
- 若未加載,則調用父類加載器的 loadClass 方法;
- 若該方法抛出 ClassNotFoundException 異常,則表示父類加載器無法加載,則目前類加載器調用 findClass 加載類;
- 若父類加載器可以加載,則直接傳回 Class 對象。
java.lang.ClassLoader.loadClass
源碼:
// 提供class類的二進制名稱,加載對應class,加載成功則傳回表示該類對應的Class<T> instance 執行個體
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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 thrown if class not found from the non-null parent class loader
}
// 父加載器加載失敗,并且沒有被引導類加載器加載,則嘗試該類加載器自己嘗試加載
if (c == null) {
// If still not found, then invoke findClass in order to find the class.
long t1 = System.nanoTime();
// 自己加載
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 是否解析類
if (resolve) {
resolveClass(c);
}
return c;
}
}
故加載過程:
- 查找
,初始化 JVMjvm.dll
- 産生 Bootstrap Loader,并加載
下的Java核心API類,都在JAVA_HOME/jre/lib
裡。rt.jar
- Bootstrap Loader 加載 Extended Loader,Extended Loader 加載
下的擴充類JAVA_HOME/jre/lib/ext
- Bootstrap Loader 加載 AppClass Loader,并将其父加載器設定為 Extended Loader
- AppClass Loader 加載 CLASSPATH 目錄下的 HelloWorld 類
雙親委派機制的優點:
- 避免重複加載,保證類的唯一性,将Java類與它的類加載器綁定到一起,當父類加載器加載完成後,子類加載器不會再次加載
- 安全性的考慮,如果使用者自己定義的類加載器加載JDK的核心類, 就可能對系統安全性造成破壞
任何以java.開頭的是核心API包,需要通路權限,強制加載會抛出異常:
Exception in thread "main" java.lang.SecurityException: Prohibited package name
涉及類相等的方法有:Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法以及instanceof對象所屬關系判定。
如果想要自己加載類,不使用雙親委派機制?可以用線程上下文加載器來加載這些類。
過程
類的整個生命周期包括:加載、驗證、準備、解析、初始化、使用和解除安裝7個階段。
其中,類加載過程:加載、連結(驗證、準備、解析)、初始化。
C/C++ 在運作前需要完成預處理、編譯、彙編、連結;在Java中,類加載 (加載、連接配接、初始化) 是在程式運作期間完成的。
在程式運作期間進行動态類加載會稍微增加程式的開銷,好處:提高程式的靈活性,可以節省記憶體空間、靈活地從網絡上加載類,可以通過命名空間的分隔來實作類的隔離,增強整個系統的安全性。
靈活性展現在它可以在運作期動态擴充,即在運作期間動态加載和動态連接配接。
其中加載、驗證、準備、初始化的開始順序是依次進行的,這些步驟開始之後的過程可能會有重疊。而解析過程會發生在初始化過程中。這是為了支援Java語言的運作時綁定(又動态綁定或晚期綁定)
加載
流程:
- 通過一個類的全限定名擷取描述此類的二進制位元組流;
- 将這個位元組流所代表的靜态存儲結構儲存為方法區的運作時資料結構;
- 在堆中生成代表這個類的類對象,作為通路方法區的入口;
虛拟機設計團隊把加載動作放到JVM外部實作,以便讓應用程式決定如何擷取所需的類。JVM提供3種類加載器。
加載的二進制位元組流的來源:
- 已經編譯好的本地class檔案(絕大多數情況)
- 壓縮包,如 Jar、War、Ear
- 其它檔案中動态生成,如從 JSP 檔案中生成 Class 類
- 資料庫中讀取,将二進制位元組流存儲至資料庫中,然後在加載時從資料庫中讀取。有些中間件會這麼做,用來實作代碼在叢集間分發
- 從網絡中,如Applet
加載過程的注意點:
-
JVM規範并未給出類在方法區中存放的資料結構;
類完成加載後,二進制位元組流就以特定的資料結構存儲在方法區中,但存儲的資料結構是由虛拟機自己定義的,JVM 規範并沒有指定。
-
JVM規範并沒有指定 Class 對象存放的位置;
在二進制位元組流以特定格式存儲在方法區後,JVM 會建立一個類對象,作為本類的外部接口。
既然是對象就應該存放在堆記憶體中,不過 JVM 規範并沒有給出限制,不同的虛拟機根據自己的需求存放這個對象。HotSpot 将 Class 對象存放在方法區。
-
加載階段和連接配接階段是交叉的;
類加載過程中每個步驟的開始順序都有嚴格限制,但每個步驟的結束順序沒有限制。也就是說,類加載過程中,必須按照如下順序開始:
,但結束順序無所謂,是以由于每個步驟處理時間的長短不一,就會導緻有些步驟會出現交叉。加載、連接配接、初始化
類和數組加載過程的差別?
數組也有類型,稱為數組類型,
String[] str = new String[10];
此數組類型是
Ljava.lang.String
,String隻是這個數組中元素的類型。
當程式在運作過程中遇到 new 關鍵字建立一個數組時,由 JVM 直接建立數組類,再由類加載器建立數組中的元素類。
而普通類的加載由類加載器完成。既可以使用系統提供的引導類加載器,也可以使用使用者自定義的類加載器。
驗證
為了確定Class檔案符合目前虛拟機要求,需要對其位元組流資料進行驗證,主要包括格式驗證、中繼資料驗證、位元組碼驗證和符号引用驗證。
-
格式驗證
驗證位元組流是否符合class檔案格式的規範,并且能被目前虛拟機處理,如是否以魔數0xCAFEBABE開頭、主次版本号是否在目前虛拟機處理範圍内、常量池是否有不支援的常量類型等。隻有經過格式驗證的位元組流,才會存儲到方法區的資料結構,剩餘3個驗證都基于方法區的資料進行。
-
中繼資料驗證
對位元組碼描述的資料進行語義分析,以保證符合Java語言規範,如是否繼承final修飾的類、是否實作父類的抽象方法、是否覆寫父類的final方法或final字段等。
-
位元組碼驗證
對類的方法體進行分析,確定在方法運作時不會有危害虛拟機的事件發生,如保證操作數棧的資料類型和指令代碼序列的比對、保證跳轉指令的正确性、保證類型轉換的有效性等。
-
符号引用驗證
為了確定後續的解析動作能夠正常執行,對符号引用進行驗證,如通過字元串描述的全限定名是都能找到對應的類、在指定類中是否存在符合方法的字段描述符等。
為什麼需要驗證?
編譯器和虛拟機是互相獨立的,虛拟機隻認二進制位元組流,不管獲得的二進制位元組流的來源。為防止位元組流中有安全問題,是以需要驗證。
驗證階段比較耗時,它非常重要但不一定必要,若所運作的代碼已經被反複使用和驗證過,可使用
-Xverify:none
參數關閉,以縮短類加載時間。
準備
在準備階段,為類變量(static修飾)在方法區中配置設定記憶體并設定初始值。如
private static int var = 100;
準備階段完成後,var 值為0,而不是100。在初始化階段,才會把100指派給val。
final,特殊情況:
private static final int VAL= 100;
在編譯階段會為VAL生成ConstantValue屬性,在準備階段虛拟機會根據ConstantValue屬性将VAL指派為100。
解析
解析階段是可選的,将常量池中的符号引用替換為直接引用的過程,兩者的不同之處:
- 符号引用,類似于OS中的邏輯位址,使用一組符号來描述所引用的目标,可以是任何形式的字面常量,定義在Class檔案格式中。
- 直接引用,類似于OS中的實體位址,可以是直接指向目标的指針、相對偏移量或能間接定位到目标的句柄。
初始化
初始化類中的靜态變量,并執行類中的static代碼、構造函數。類初始化的過程是不可逆的,如果中間一步出錯,則無法執行下一步。JVM規範嚴格定義何時需要對類進行初始化:
- 通過new關鍵字、反射、clone、反序列化機制執行個體化對象時
- 調用類的靜态方法時
- 使用類的靜态字段或對其指派時
- 通過反射調用類的方法時
- 初始化該類的子類時(初始化子類前其父類必須已經被初始化)
- JVM啟動時被标記為啟動類的類(簡單了解為具有main方法的類)
初始化階段就是執行類構造器
clinit()
的過程。
clinit()
方法由編譯器自動産生,收集類中
static{}
代碼塊中的類變量指派語句和類中靜态成員變量的指派語句。
在準備階段,類中靜态成員變量已經完成預設初始化,而在初始化階段,
clinit()
方法對靜态成員變量進行顯示初始化。
該過程的注意點:
-
方法中靜态成員變量的指派順序是根據 Java 代碼中成員變量的出現的順序決定的;clinit()
- 靜态代碼塊能通路出現在靜态代碼塊之前的靜态成員變量,無法通路出現在靜态代碼塊之後的成員變量;
- 靜态代碼塊能給出現在靜态代碼塊之後的靜态成員變量指派;
- 構造函數
需顯示調用父類構造函數,而類的構造函數init()
不需要調用父類的類構造函數,因為虛拟機會確定子類的clinit()
方法執行前已經執行父類的clinit()
方法;clinit()
- 如果一個類/接口中沒有靜态代碼塊,也沒有靜态成員變量的指派操作,編譯器不會生成
方法;clinit()
- 接口也需要通過
方法為接口中定義的靜态成員變量顯示初始化;clinit()
- 接口中不能使用靜态代碼塊;
- 接口在執行
方法前,虛拟機不會確定其父接口的clinit()
方法被執行,隻有當父接口中的靜态成員變量被使用到時才會執行父接口的clinit()
方法;clinit()
- 虛拟機會給
方法加鎖,是以當多條線程同時執行某一個類的clinit()
方法時,隻有一個方法會被執行,其它的方法都被阻塞。隻要有一個clinit()
方法執行完,其它的clinit()
方法就不會再被執行。是以在同一個類加載器下,同一個類隻會被初始化一次。clinit()
初始化開始時機
JVM 規範中隻定義類加載過程中初始化過程開始的時機,加載、連接配接過程都應該在初始化之前開始 (解析除外),這些過程具體在何時開始,JVM 規範并沒有定義,不同的虛拟機可以根據具體的需求自定義。
在運作過程中遇到如下位元組碼指令時,如果類尚未初始化,那就要進行初始化:new、getstatic、putstatic、invokestatic。這四個指令對應的 Java 代碼場景是:
- 通過 new 建立對象;
- 讀取、設定一個類的靜态成員變量(不包括final修飾的靜态變量);
- 調用一個類的靜态成員函數。
- 使用
進行反射調用時,如果類沒有初始化,那就需要初始化。java.lang.reflect
當初始化一個類的時候,若其父類尚未初始化,那就先要讓其父類初始化,然後再初始化本類;當虛拟機啟動時,虛拟機會首先初始化帶有main方法的類,即主類。
JVM 規範中要求在程式運作過程中,當且僅當出現上述4個條件之一的情況才會初始化一個類。如果間接滿足上述初始化條件是不會初始化類的。直接滿足上述初始化條件的情況叫做主動使用;間接滿足上述初始化過程的情況叫做被動使用。
引用
類加載過程的最後一個階段,到初始化階段,才真正開始執行類中的Java程式代碼。
主動引用
隻有這四種情況才會觸發類的初始化,稱為對一個類進行主動引用,除此之外所有引用類的方式都不會觸發其初始化,稱為被動引用。
VM規範嚴格規定有且隻有四種情況必須立即對類進行初始化:
- 遇到new、getstatic、putstatic、invokestatic這四條位元組碼指令時,如果類還沒有進行過初始化,則需要先觸發其初始化。生成這四條指令最常見的Java代碼場景是:使用new關鍵字執行個體化對象時、讀取或設定一個類的靜态字段(static)時(被static修飾又被final修飾的,已在編譯期把結果放入常量池的靜态字段除外)、以及調用一個類的靜态方法時。
- 使用Java.lang.refect包的方法對類進行反射調用時,如果類還沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
- 當VM啟動時,使用者需要指定一個要執行的主類,VM會先執行該主類。
被動引用
- 通過子類引用父類中的靜态字段,這時對子類的引用為被動引用,是以不會初始化子類,隻會初始化父類;
- 常量在編譯階段會存入調用它的類的常量池中,本質上沒有直接引用到定義該常量的類,是以不會觸發定義常量的類的初始化;
- 通過數組定義來引用類,不會觸發類的初始化
使用
解除安裝
拓展
類加載的用途:類層次劃分、OSGI、熱部署、代碼加密
綁定
把一個方法的調用與方法所在的類 (方法主體)關聯起來,對Java來說,綁定分為靜态綁定和動态綁定:
- 靜态綁定:即前期綁定。在程式執行前方法已經被綁定,此時由編譯器或其它連接配接程式實作。針對Java,可以簡單的了解為程式編譯期的綁定。Java當中的方法隻有final,static,private和構造方法是前期綁定的。
- 動态綁定:即晚期綁定,運作時綁定。在運作時根據具體對象的類型進行綁定。Java中幾乎所有的方法都是後期綁定的。
Launcher & ExtClassLoader & AppClassLoader
ExtClassLoader與AppClassLoader,都在
sun.misc.Launcher
類内部定義,是其靜态内部類。JVM負責調用已經加載在方法區的類
sun.misc.Launcher
的靜态方法
getLauncher()
,擷取
sun.misc.Launcher
執行個體,且
sun.misc.Launcher
使用單例設計模式,保證一個JVM内隻有一個Launcher執行個體。
線程上下文加載器
線程上下文類加載器是從線程的角度來看待類的加載,為每一個線程綁定一個類加載器,可以将類的加載從單純的雙親加載模型解放出來,進而實作特定的加載需求。
Class.forName 和 ClassLoader.loadClass差別
一個 Java類加載到 JVM 中會經過三個步驟,裝載(查找和導入類或接口的二進制資料)、連結(校驗:檢查導入類或接口的二進制資料的正确性;準備:給類的靜态變量配置設定并初始化存儲空間;解析:将符号引用轉成直接引用)、初始化(激活類的靜态變量的初始化 Java 代碼和靜态 Java 代碼塊)。
Class.forName(className)
方法,内部實際調用方法
Class.forName(className, true, classloader);
// name:要加載 Class 的名字,initialize:是否要初始化,loader:指定的 classLoader
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)
ClassLoader.loadClass(className)
方法,内部實際調用方法
ClassLoader.loadClass(className, false);
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
/**
兩個參數的含義分别為:
name:class 的名字
resolve:是否要進行連結
*/
通過傳入的參數可知
Class.forName
方法執行之後已經對被加載類的靜态變量配置設定完存儲空間,而
ClassLoader.loadClass
方法并沒有一定執行完連結這一步;當想動态加載一個類,且這個類又存在靜态代碼塊或者靜态變量,而你在加載時就想同時初始化這些靜态代碼塊,則應偏向于使用
Class.forName
方法。