JVM 專題三:類載入子系統(一)類裝載器子系統
阿新 • • 發佈:2020-07-02
類裝載器子系統
1.1 什麼是類裝載子系統?
- 類裝載器子系統負責從檔案系統或者網路中載入Class檔案,Class檔案在檔案開頭有特定的檔案標識(魔數)。
- 類裝載器子系統(ClassLoader)只負責Class檔案的載入,至於它是否可以執行,則由Execution Engine決定。
| 你媽媽[ClassLoader]給你[Execution Engine]找到相親姑娘,是否能成得看你自己的本事
- 載入類的資訊存放於一塊稱為方法區的記憶體空間。除了類的資訊外,方法區中還會存放執行時常量池資訊,可能還包括字串字面量和數字常量(這部分常量資訊是Class檔案中常量池部分的記憶體對映)
| 執行時常量池:常量池在執行時載入到記憶體裡,就叫做執行時常量池
1.2 類裝載子系統架構圖
1.3 類的載入過程
1.3.1 載入
- 通過一個類的全限定名獲取定義此類的二進位制位元組流。
- 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口
| 載入.class檔案的方式
- 從本地系統直接載入
- 通過網路獲取,典型場景:Web Applet
- 從zip壓縮包中讀取,稱為日後jar、war格式的基礎
- 執行時計算生成,使用最多的是:動態代理技術(反射動態代理)
- 由其他檔案生成,典型場景:JSP應用
- 從專有資料庫中提取.class檔案,少見
- 從加密檔案中獲取,典型的防Class檔案被反編譯的保護措施
1.3.2 連結
- 驗證
- 目的在於確保Class檔案的位元組流中包含資訊符合當前虛擬機器要求,保證被載入類的正確性,不會危害虛擬機器自身安全。
- 主要包括四種驗證,檔案格式驗證,源資料驗證,位元組碼驗證,符號引用驗證。
- 準備
- 為類變數分配記憶體並且設定該類變數的預設初始值,即零值;
- 這裡不包含用final修飾的static,因為final在編譯的時候就會分配了,準備階段會顯式初始化;
- 這裡不會為例項變數分配初始化,類變數會分配在方法區中,而例項變數是會隨著物件一起分配到java堆中。
- 解析
- 將常量池內的符號引用轉換為直接引用的過程。
| 符號引用:一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《java虛擬機器規範》的Class檔案格式中。
| 直接引用:直接指向目標的指標、相對偏移量或一個間接定位到目標的控制代碼
- 事實上,解析操作往往會伴隨著jvm在執行完初始化之後再執行
- 解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別等。對應常量池中的
CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
等。
1.3.3 初始化:初始化模組,初始化階段就是執行類構造器方法<clinit>()的過程
- <clinit>即"class or interface initialization method",他並不是指構造器(構造器是虛擬機器視角下的<init>())。
- 此方法不需要定義,是javac編譯器自動收集類中的所有類變數的賦值動作和靜態程式碼塊中的語句合併而來。
| 沒有定義<clinit>(),但是在二進位制檔案中<clinit>()方法存在
| 如果沒有給變數賦值,也沒有靜態程式碼塊,就不會有<clinit>()方法
| 前向引用
- 若該類具有父類,JVM會先呼叫父類的<clinit>方法,再呼叫子類的<clinit>方法
- 虛擬機器必須保證一個類的<clinit>方法在多執行緒下被同步加鎖
| 一個類只會被載入一次,也就是說只會呼叫一次<clinit>方法,類第一次被載入之後,會在記憶體中快取起來,下次再使用這個類,直接從記憶體中取。
| 舉例說明:假設現在有兩個執行緒th1,th2都想要載入類TestClass,如果執行緒th1搶到載入類的語句TestClass t = new TestClass(),th1就去載入類,因為會被同步加鎖,th2就進不去載入類的方法,所以只會有一個執行緒可以成功列印初始化語句
1.4 類載入器分類
1.4.1 引導類載入器:BootStrap ClassLoader
- 由C/C++語言實現,巢狀在JVM內部。
- java核心類庫都是使用引導類載入器BootStrapClassLoader載入的。
- 並不繼承自ClassLoader,沒有父載入器。
- 載入擴充套件類載入器和應用類載入器,並指定他們的父類載入器,即ClassLoader。
- 處於安全考慮,Bootstrap啟動類載入器只加載包名為java、javax、sun等開頭的包
1.4.2 自定義類載入器:
- 由Java實現。
- Java虛擬機器規範將所有派生於抽象類ClassLoader的類載入器都劃分為自定義類載入器,從下圖可以看到擴充套件類載入器ExtClassLoader,應用類載入器System ClassLoader間接繼承ClassLoader,所以Extension Class Loader、System ClassLoader也是自定義類載入器。
- 注意上圖中的載入器劃分關係為包含關係,不是上下層,也不是繼承關係。(Java語言編寫的自定義類載入器不會繼承於C/C++實現的BootStrap ClassLoader,這種關係類似於等級制度)。
- 擴充套件類載入器(Extension Class Loader):JVM自帶
- java語言編寫 ,由sun.misc.Launcher$ExtClassLoader實現
- 派生於ClassLoader類
- 從java.ext.dirs系統屬性所指定的目錄中載入類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴充套件目錄)下載入類庫。如果使用者建立的JAR放在此目錄下,也會由拓展類載入器自動載入
- 應用程式類載入器(系統類載入器,System Class Loader):JVM自帶
- java語言編寫, 由sun.misc.Launcher$AppClassLoader實現。
- 派生於ClassLoader類
- 它負責載入環境變數classpath或系統屬性 java.class.path指定路徑下的類庫
- 該類載入器是程式中預設的類載入器,一般來說,java應用的類都是由它來完成載入
- 通過ClassLoader#getSystemClassLoader()方法可以獲取到該類載入器
- 程式設計師自定義類載入器
- 為什麼要自定義類載入器?
- 隔離載入類:確保框架jar包和中介軟體jar包不衝突
- 修改類載入的方式:在需要的時候動態載入
- 擴充套件載入源
- 防止原始碼洩露
- 如何自定義類載入器?
- 繼承ClassLoader類
- 把自定義的類載入邏輯及解除安裝findClass()方法中(JDK1.2之前需要重寫loaderClass()方法,JDK1.2之後不建議覆蓋loaderClass(),而是重寫findClass())
- 如果需求不復雜,也可以直接繼承URLClassLoader類,這樣可以避免自己去編寫findClass()方法及其獲取位元組碼流的方式,使自定義類載入器編寫更加簡潔
7.獲取不同類的載入器程式碼演示
/** * ClassLoader載入 */ public class ClassLoaderTest { public static void main(String[] args) { //獲取系統類載入器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //獲取其上層 擴充套件類載入器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 //獲取其上層 獲取不到引導類載入器 ClassLoader bootStrapClassLoader = extClassLoader.getParent(); System.out.println(bootStrapClassLoader);//null //對於使用者自定義類來說:使用系統類載入器進行載入 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //String 類使用引導類載入器進行載入的 -->java核心類庫都是使用引導類載入器載入的 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1);//null獲取不到間接證明了String 類使用引導類載入器進行載入的 } }View Code