1、類加載子系統在記憶體結構中所處的位置
通過記憶體結構圖,我們先知道類加載子系統所處的位置,做到心中有圖。

2、類加載器作用
- 類加載器子系統負責從檔案系統或者網絡中加載Class檔案,class檔案在檔案開頭有特定的檔案辨別。
- ClassLoader隻負責class檔案的加載,至于它是否可以運作,則由Execution Engine(執行引擎)決定。
- 加載的類資訊存放于一塊稱為方法區的記憶體空間。除了類的資訊外,方法區中還會存放運作時常量池資訊,可能還包括字元串字面量和數字常量(這部分常量資訊是Class檔案中常量池部分的記憶體映射)
3、類加載過程
/**
*示例代碼
*/
public class HelloLoader {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
用流程圖表示上述示例代碼:
4、類加載階段
4.1、連接配接階段
4.1.1、驗證(Verify)
- 目的在于確定Class檔案的位元組流中包含資訊符合目前虛拟機要求,保證被加載類的正确性,不會危害虛拟機自身安全。
- 主要包括四種驗證,檔案格式驗證,中繼資料驗證,位元組碼驗證,符号引用驗證。
4.1.2、準備(Prepare)
- 為類變量配置設定記憶體并且設定該類變量的預設初始值,即零值。
- 這裡不包含用final修飾的static,因為final在編譯的時候就會配置設定了,準備階段會顯式初始化;
- 這裡不會為執行個體變量配置設定初始化,類變量會配置設定在方法區中,而執行個體變量是會随着對象一起配置設定到Java堆中。
4.1.3、解析(Resolve)
- 将常量池内的符号引用轉換為直接引用的過程。
- 事實上,解析操作往往會伴随着JVM在執行完初始化之後再執行。
- 符号引用就是一組符号來描述所引用的目标。符号引用的字面量形式明确定義在《java虛拟機規範》的Class檔案格式中。直接引用就是直接指向目标的指針、相對偏移量或一個間接定位到目标的句柄。
- 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
4.2、初始化階段
- 初始化階段就是執行類構造器方法<clinit>()的過程。
此方法不需定義,是javac編譯器自動收集類中的所有類變量的指派動作和靜态代碼塊中的語句合并而來。
- 構造器方法中指令按語句在源檔案中出現的順序執行。
- <clinit>()不同于類的構造器。(關聯:構造器是虛拟機視角下的<init>())
- 若該類具有父類,JVM會保證子類的<clinit>()執行前,父類的<clinit>()已經執行完畢。
- 虛拟機必須保證一個類的<clinit>()方法在多線程下被同步加鎖。
5、類加載器分類
JVM支援兩種類型的類加載器 。分别為引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)。
從概念上來講,自定義類加載器一般指的是程式中由開發人員自定義的一類類加載器,但是Java虛拟機規範卻沒有這麼定義,而是将所有派生于抽象類ClassLoader的類加載器都劃分為自定義類加載器。
無論類加載器的類型如何劃分,在程式中我們最常見的類加載器始終隻有3個,如下所示:
這裡的四者之間的關系是包含關系。不是上層下層,也不是子父類的繼承關系。
6、JVM自帶的加載器
6.1、啟動類加載器(引導類加載器,Bootstrap ClassLoader)
- 這個類加載使用C/C++語言實作的,嵌套在JVM内部。
- 它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路徑下的内容),用于提供JVM自身需要的類
- 并不繼承自ava.lang.ClassLoader,沒有父加載器。
- 加載擴充類和應用程式類加載器,并指定為他們的父類加載器。
- 出于安全考慮,Bootstrap啟動類加載器隻加載包名為java、javax、sun等開頭的類
6.2、擴充類加載器(Extension ClassLoader)
- Java語言編寫,由sun.misc.Launcher$ExtClassLoader實作。
- 派生于ClassLoader類
- 父類加載器為啟動類加載器
- 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/1ib/ext子目錄(擴充目錄)下加載類庫。如果使用者建立的JAR放在此目錄下,也會自動由擴充類加載器加載。
6.3、應用程式類加載器(系統類加載器,AppClassLoader)
- java語言編寫,由sun.misc.LaunchersAppClassLoader實作
- 派生于ClassLoader類
- 父類加載器為擴充類加載器
- 它負責加載環境變量classpath或系統屬性java.class.path指定路徑下的類庫
- 該類加載是程式中預設的類加載器,一般來說,Java應用的類都是由它來完成加載
- 通過ClassLoader#getSystemclassLoader() 方法可以擷取到該類加載器
7、自定義類加載器
在Java的日常應用程式開發中,類的加載幾乎是由上述3種類加載器互相配合執行的,在必要時,我們還可以自定義類加載器,來定制類的加載方式。 為什麼要自定義類加載器?
- 隔離加載類
- 修改類加載的方式
- 擴充加載源
- 防止源碼洩漏
使用者自定義類加載器實作步驟:
- 開發人員可以通過繼承抽象類ava.lang.ClassLoader類的方式,實作自己的類加載器,以滿足一些特殊的需求
- 在JDK1.2之前,在自定義類加載器時,總會去繼承ClassLoader類并重寫loadClass() 方法,進而實作自定義的類加載類,但是在JDK1.2之後已不再建議使用者去覆寫loadclass() 方法,而是建議把自定義的類加載邏輯寫在findClass()方法中
- 在編寫自定義類加載器時,如果沒有太過于複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass() 方法及其擷取位元組碼流的方式,使自定義類加載器編寫更加簡潔。
8、ClassLoader的使用說明
ClassLoader類是一個抽象類,其後所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)
sun.misc.Launcher 它是一個java虛拟機的入口應用
擷取ClassLoader的途徑
#方式一:擷取目前ClassLoader
clazz.getClassLoader()
#方式二:擷取目前線程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
#方式三:擷取系統的ClassLoader
ClassLoader.getSystemClassLoader()
#方式四:擷取調用者的ClassLoader
DriverManager.getCallerClassLoader()
9、雙親委派機制
Java虛拟機對class檔案采用的是按需加載的方式,也就是說當需要使用該類時才會将它的class檔案加載到記憶體生成class對象。而且加載某個類的class檔案時,Java虛拟機采用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式。
工作原理
- 如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執行;
- 如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終将到達頂層的啟動類加載器;
- 如果父類加載器可以完成類加載任務,就成功傳回,倘若父類加載器無法完成此加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模式。
舉例
當我們加載jdbc.jar 用于實作資料庫連接配接的時候,首先我們需要知道的是 jdbc.jar是基于SPI接口進行實作的,是以在加載的時候,會進行雙親委派,最終從根加載器中加載 SPI核心類,然後在加載SPI接口類,接着在進行反向委派,通過線程上下文類加載器進行實作類jdbc.jar的加載。
優勢
- 避免類的重複加載
- 保護程式安全,防止核心API被随意篡改
- 自定義類:java.lang.String
- 自定義類:java.lang.ShkStart(報錯:阻止建立 java.lang開頭的類)
沙箱安全機制
自定義String類,但是在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先加載jdk自帶的檔案(rt.jar包中java\lang\String.class),報錯資訊說沒有main方法,就是因為加載的是rt.jar包中的string類。這樣可以保證對java核心源代碼的保護,這就是沙箱安全機制。
10、其他
如何判斷兩個class對象是否相同
在JVM中表示兩個class對象是否為同一個類存在兩個必要條件:
- 類的完整類名必須一緻,包括包名。
- 加載這個類的ClassLoader(指ClassLoader執行個體對象)必須相同。
換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class檔案,被同一個虛拟機所加載,但隻要加載它們的ClassLoader執行個體對象不同,那麼這兩個類對象也是不相等的。
對類加載器的引用
JVM必須知道一個類型是由啟動加載器加載的還是由使用者類加載器加載的。如果一個類型是由使用者類加載器加載的,那麼JVM會将這個類加載器的一個引用作為類型資訊的一部分儲存在方法區中。當解析一個類型到另一個類型的引用的時候,JVM需要保證這兩個類型的類加載器是相同的。
類的主動使用和被動使用
Java程式對類的使用方式分為:主動使用和被動使用。
主動使用,又分為七種情況:
- 建立類的執行個體
- 通路某個類或接口的靜态變量,或者對該靜态變量指派
- 調用類的靜态方法
- 反射(比如:Class.forName("com.atguigu.Test"))
- 初始化一個類的子類
- Java虛拟機啟動時被标明為啟動類的類
JDK 7 開始提供的動态語言支援:java.lang.invoke.MethodHandle執行個體的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化
除了以上七種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導緻類的初始化。