寫在前面:我是「雲祁」,一枚熱愛技術、會寫詩的大數據開發猿。暱稱來源於王安石詩中一句
[ 雲之祁祁,或雨於淵 ]
,甚是喜歡。
寫博客一方面是對自己學習的一點點總結及記錄,另一方面則是希望能夠幫助更多對大數據感興趣的朋友。如果你也對數據中臺、數據建模、數據分析以及Flink/Spark/Hadoop/數倉開發
感興趣,可以關注我的動態 https://blog.csdn.net/BeiisBei ,讓我們一起挖掘數據的價值~
每天都要進步一點點,生命不是要超越別人,而是要超越自己! (ง •_•)ง
一、內存結構概述
複雜版的詳細圖
本文針對Class Loader SubSystem這一塊展開講解類加載子系統的工作流程。
如果自己手寫一個Java虛擬機的話,主要考慮到哪些結構呢?
類加載器和執行引擎。
二、類加載器和類的加載過程
2.1 類加載子系統作用
-
類加載子系統負責從文件系統或者網絡中加載class文件,class文件在文件開頭有特定的文件標識即16進制CA FE BA BE;
-
加載後的Class類信息存放於一塊成爲 方法區 的內存空間。除了類信息之外,方法區還會存放運行時常量池信息,可能還包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)
-
ClassLoader只負責class文件的加載,至於它是否可以運行,則由Execution Engine決定
-
如果調用構造器實例化對象,則其實例存放在堆區
2.2 類加載器ClassLoader角色
- class file 存在於本地硬盤上,可以理解爲設計師畫在紙上的模板,而最終這個模板在執行的時候要加載到JVM當中來根據這個文件實例化出n個一模一樣的實例。
- class file 加載到JVM中,被稱爲DNA元數據模板,放在方法區。
- 在.class文件 —> JVM —> 最終成爲元數據模板,此過程就要一個運輸工具(類裝載器 Class Loader),扮演一個快遞員的角色。
2.3 類的加載過程
2.3.1 加載
-
通過一個類的全限定名獲取定義此類的二進制字節流;
-
將這個字節流所代表的的靜態存儲結構轉化爲方法區的運行時數據;
-
在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口
2.3.2 鏈接(即驗證、準備、解析)
驗證
-
目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全。
-
主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
準備
-
爲類變量分配內存並且設置該類變量的默認初始值,即零值;
-
這裏不包含用final修飾的static,因爲final在編譯的時候就會分配了,準備階段會顯式初始化;
-
之類不會爲實例變量分配初始化,類變量會分配在方法去中,而實例變量是會隨着對象一起分配到java堆中。
解析
-
將常量池內的符號引用轉換爲直接引用的過程。
-
事實上,解析操作網晚會伴隨着jvm在執行完初始化之後再執行
-
符號引用就是一組符號來描述所引用的目標。符號應用的字面量形式明確定義在《java虛擬機規範》的class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄
-
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
2.3.3 初始化
-
clinit()即“class or interface initialization method”,注意他並不是指構造器init(),初始化階段就是執行類構造器方法
<clinit>()
的過程 -
此方法不需要定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態代碼塊中的語句合併而來。
-
我們注意到如果沒有靜態變量c,那麼字節碼文件中就不會有clinit方法
構造器方法clinit()中指令按語句在源文件中出現的順序執行
虛擬機必須保證一個類的clinit()方法在多線程下被同步加鎖。
即一個類只需被clinit一次,之後該類的內部信息就被存儲在方法區。
可以看到線程2並不會重複執行初始化操作。
三、類加載器分類
-
JVM支持兩種類型的加載器,分別爲引導類加載器C/C++實現(BootStrap ClassLoader)和自定義類加載器由Java實現
-
從概念上來講,自定義類加載器一般指的是程序中由開發人員自定義的一類類加載器,但是java虛擬機規範卻沒有這麼定義,而是將所有派生於抽象類ClassLoader的類加載器都劃分爲自定義類加載器。
-
這裏的四者之間的關係是包含關係。不是上層下層,也不是子父類的繼承關係。
-
按照這樣的加載器的類型劃分,在程序中我們最常見的類加載器是:引導類加載器BootStrapClassLoader、自定義類加載器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)
3.1 自定義類與核心類庫的加載器
-
對於用戶自定義類來說:將使用系統類System Class Loader加載器中的AppClassLoader進行加載
-
Java核心類庫都是使用引導類加載器BootStrapClassLoader加載的
/**
* 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 類使用引導類加載器進行加載的
}
}
3.2 虛擬機自帶的加載器
啓動類加載器(引導類加載器,BootStrap ClassLoader)
-
這個類加載使用C/C++語言實現的,嵌套在JVM內部
-
它用來加載java的核心庫(JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path路徑下的內容),用於提供JVM自身需要的類
-
並不繼承自java.lang.ClassLoader,沒有父加載器
-
加載拓展類和應用程序類加載器,並指定爲他們的父加載器,即ClassLoader
-
出於安全考慮,BootStrap啓動類加載器只加載包名爲java、javax、sun等開頭的類
拓展類加載器(Extension ClassLoader)
-
java語言編寫 ,由sun.misc.Launcher$ExtClassLoader實現。
-
派生於ClassLoader類
-
從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。如果用戶創建的JAR放在此目錄下,也會由拓展類加載器自動加載。
應用程序類加載器(系統類加載器,AppClassLoader)
-
java語言編寫, 由sun.misc.Launcher$AppClassLoader實現。
-
派生於ClassLoader類
-
它負責加載環境變量classpath或系統屬性 java.class.path指定路徑下的類庫
-
該類加載器是程序中默認的類加載器,一般來說,java應用的類都是由它來完成加載
-
通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器
/**
* 虛擬機自帶加載器
*/
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("********啓動類加載器*********");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
//獲取BootStrapClassLoader能夠加載的api路徑
for (URL e:urls){
System.out.println(e.toExternalForm());
}
//從上面的路徑中隨意選擇一個類 看看他的類加載器是什麼
//Provider位於 /jdk1.8.0_171.jdk/Contents/Home/jre/lib/jsse.jar 下,引導類加載器加載它
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);//null
System.out.println("********拓展類加載器********");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")){
System.out.println(path);
}
//從上面的路徑中隨意選擇一個類 看看他的類加載器是什麼:拓展類加載器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@4dc63996
}
}
知識擴展:啓動類加載器BootStrapClassLoader能夠加載的api路徑有
最近看java.util.concurrent包的內容,發現java.time.、java.util.、java.nio.、java.lang.、java.text.、java.sql.、java.math.*等等都在rt.jar包下。
3.3 用戶自定義加載器
在Java的日常應用程序開發中,類的加載幾乎是由上述三種類加載器相互配合執行的,在必要的時候,我們還可以自定義加載器,來定製類的加載方式。
爲什麼要使用用戶自定義類加載器
-
隔離加載類
-
修改類加載的方式
-
拓展加載源
-
防止源碼泄漏
用戶自定義加載器實現步驟:
- 開發人員可以通過繼承抽象類 java.lang.ClassLoader 類的方式,實現自己的類加載器,以滿足一些特殊的需求
- 在JDK1.2之前,在自定義類加載器時,總會去繼承ClassLoader類並重寫loadClass()方法,從而實現自定義的類加載類,但是在JDK1.2之後已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中
- 在編寫自定義類加載器時,如果沒有太過於複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。
四、ClassLoader的常用方法及獲取方法
ClassLoader類,它是一個抽象類,其後所有的類加載器都繼承自ClassLoader(不包括啓動類加載器)
4.1 ClassLoader繼承關係
sun.misc.Launcher它是一個java虛擬機的入口應用
4.2 代碼示例
五、雙親委派機制
Java虛擬機對class文件採用的是按需加載的方式,也就是說當需要使用該類時纔會將她的class文件加載到內存生成的class對象。而且加載某個類的class文件時,java虛擬機採用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式。
5.1 雙親委派機制工作原理
- 如果一個類加載器收到了類加載請求,它並不會自己先去加載,而是把這個請求委託給父類的加載器去執行;
- 如果父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器;
- 如果父類加載器可以完成類加載任務,就成功返回,倘若父類加載器無法完成此加載任務,子加載器纔會嘗試自己去加載,這就是雙親委派模式。
5.2 代碼示例
如圖,雖然我們自定義了一個java.lang包下的String嘗試覆蓋核心類庫中的String,但是由於雙親委派機制,啓動加載器會加載java核心類庫的String類(BootStrap啓動類加載器只加載包名爲java、javax、sun等開頭的類),而核心類庫中的String並沒有main方法。
5.3 雙親委派機制的優勢
-
避免類的重複加載,如上
-
保護程序安全,防止核心API被隨意修改
啓動類加載器可以搶在標準擴展類加載器之前去加載類,而標準擴展類裝載器可以搶在類路徑加載器之前去裝載那個類,類路徑裝載 器又可以搶在自定義類加載器之前去加載它。所以Java虛擬機先從最可信的Java核心API查找類型,這是爲了防止不可靠的類扮演被信任的類,試想一 下,網絡上有個名叫java.lang.Integer的類,它是某個黑客爲了想混進java.lang包所起的名字,實際上裏面含有惡意代碼,但是這種伎倆在雙親模式加載體系結構下是行不通的,因爲網絡類加載器在加載它的時候,它首先調用雙親類加載器,這樣一直向上委託,直到啓動類加載器,而啓動類加載器在覈心Java API裏發現了這個名字的類,所以它就直接加載Java核心API的java.lang.Integer類,然後將這個類返回,所以自始自終網絡上的 java.lang.Integer的類是不會被加載的。
- 保證核心API包的訪問權限
但是如果這個移動代碼不是去試圖替換一個被信任的類(就是前面說的那種情況),而是想在一個被信任的包中插入一個全新的類型,情況會怎樣呢?比如一個名爲 java.lang.Virus的類,經過雙親委託模式,最終類裝載器試圖從網絡上下載這個類,因爲網絡類裝載器的雙親們都沒有這個類(當然沒有了,因爲 是病毒嘛)。假設成功下載了這個類,那你肯定會想,Virus和lang下的其他類痛在java.lang包下,暗示這個類是Java API的一部分,那麼是不是也擁有修改Java.lang包中數據的權限呢?答案當然不是,因爲要取得訪問和修改java.lang包中的權 限,java.lang.Virus和java.lang下其他類必須是屬於同一個運行時包的,什麼是運行時包?運行時包是指由同一個類裝載器裝載的、屬 於同一個包的、多個類型的集合。考慮一下,java.lang.Virus和java.lang其他類是同一個類裝載器裝載的嗎?不是 的!java.lang.Virus是由網絡類裝載器裝載的!
自定義類:java.lang.MeDsh(java.lang包需要訪問權限,阻止我們用包名自定義類)
5.4 雙親委派機制在SPI中的應用
-
某個應用程序由雙親委派機制找到引導類加載器,首先調用rt.jar包中的SPI核心,但由於SPI核心當中有各種各樣的接口需要被實現(這裏指具體的服務提供商),這裏我們已JDBC.jar爲例,jdbc.jar可以爲我們提供具體的實現。
-
那麼這時我們需要反向委託,找到線程上下文類加載器去加載jdbc.jar
-
線程上下文類加載器屬於系統類加載器
5.5 沙箱安全機制
自定義String類,但是在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先加載JDK自帶的文件(rt.jar包中java\lang\String.class),報錯信息說沒有main方法,就是因爲加載的rt.jar包中的String類。這樣可以何保證對java核心源代碼的保護,這就是沙箱安全機制。
六、其他
6.1 JVM中表示兩個class對象是否爲同一個類
在JVM中表示兩個class對象是否爲同一個類存在的兩個必要條件
-
類的完整類名必須一致,包括包名
-
即使類的完整類名一致,同時要求加載這個類的ClassLoader(指ClassLoader實例對象)必須相同;是引導類加載器、還是定義類加載器
換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那麼這兩個類對象也是不相等的。
6.2 對類加載器的引用
JVM必須知道一個類型是由啓動類加載器加載的還是由用戶類加載器加載的。如果一個類型由用戶類加載器加載的,那麼JVM會 將這個類加載器的一個引用作爲類型信息的一部分保存在方法區中。當解析一個類型到另一個類型的引用的時候,JVM需要保證兩個類型的加載器是相同的。
6.3 類的主動使用和被動使用
Java程序對類的使用方式分爲:主動使用和被動使用,即是否調用了clinit()方法。
- 主動使用在類加載系統中的第三階段initialization即初始化階段調用了clinit()方法。
而被動使用不會去調用,主動使用,分爲七種情況:
-
創建類的實例
-
訪問某各類或接口的靜態變量,或者對靜態變量賦值
-
調用類的靜態方法
-
反射 比如Class.forName(com.dsh.jvm.xxx)
-
初始化一個類的子類
-
java虛擬機啓動時被標明爲啓動類的類
-
JDK 7 開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化
- 除了以上七種情況,其他使用java類的方式都被看作是對類的被動使用,都不會導致類的初始化。