說明:
本文雖然借鑒了網絡的部分内容,但是由于網絡上内容基本都是摘抄JVM書來的,有的地方很晦澀難懂,尤其是SPI和線程上下文類加載器TCCL那裡。
本文通過根據jdbc的驅動加載過程,跟蹤
、
雙親委派源碼
、
rt.jar
、
Class.forName
、
SPI
TCCL
的執行順序,分析了它們之間的關系。總結完收獲很大,希望你也有所收獲
各小節沒有标序号,但大章節序号是有的,慢慢看即可,我花了一整天整理學習。不說了,去睡覺了
類的使用流程:
- 是否加載了該類
- 沒有加載:使用類加載器加載該類
- 加載了:連結–初始化—調用main方法
一、簡述
類加載歸納為有三個階段:
先編譯java為class,啟動程式後開始進行類裝載
1) 、加載:
從檔案系統或者網絡中查找并加載類的二進制資料,利用二進制資料建立class對象
2) 、連接配接:
2.1) 、驗證 : 確定被加載的類的正确性,確定class檔案的位元組流中資訊符合JVM的要求,不會危害JVM的安全,使得JVM免受惡意代碼的攻擊。主要包括四種驗證,檔案格式驗證,中繼資料驗證,位元組碼驗證,符号引用驗證。
2.2) 、準備:為類的static靜态變量配置設定記憶體,并将其初始化為預設值,但是到達初始化之前類變量都沒有初始化為真正的初始值。(這裡不包含final修飾的static變量,因為final在編譯時候就會配置設定了,準備階段會顯示初始化。)(這裡不會為執行個體變量配置設定初始化,類變量會配置設定在方法區中,而執行個體變量會随着對象一起配置設定到java堆中)這些記憶體都将在方法區中進行配置設定。
2.3) 解析
2.3、解析:把類中的符号引用轉換為直接引用,就是在類的常量池中尋找類、接口、字段和方法的符号引用,把這些符号引用替換成直接引用的過程。虛拟機規範之中并未規定解析階段發生的具體時間,隻要求了在執行anewarray、checkcast, getfield, getstatic, instanceof, invokeinterface, invokespecial, invokestatic、invokevirtual, multianewarray、new、 putfield和 putstatIc這13個用于操作符号引用的位元組碼指令之前,先對它們所使用的符号引用進行解析。是以虛拟機實作會根據需要來判斷,到底是在類被加載器加載時就對常量池中的符号引用進行解析,還是等到一個符号引用将要被使用前才去解析它。
解析動作主要針對類或接口、字段、類方法、接口方法四類符号引用進行,分别對應于常量池的 CONSTANT Class info、 CONSTANT_Fieldref_info、 CONSTANT_Methodref_info及 CONSTANT_InterfaceMethodref_info四種常量類型。
- 符号引用( Symbolic References):符号引用以一組符号來描述所引用的目标,符号引用可以是任何形式的字面量,隻要使用時能無歧義地定位到目标即可。符号引用與虛拟機實作的記憶體布局無關,無用的且标并不一定已經加載到記憶體中。
- 符号引用在 Class檔案中它以 CONSTANT_Class_info、CONSTANT_Fieldref_info、 CONSTANT_Methodref_info等類型的常量出現
- 直接引用 (Direct Referenc):直接引用可以是直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。直接引用是與虛拟機實作的記憶體布局相關同虛拟機執行個體上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目标必定已經在記憶體中存在。
2.3.1、解析階段:
對同一個符号引用進行多次解析請求是很常見的事情,虛拟機實作可能會對第一次解析的結果進行緩存(在運作時常量池中記錄直接引用,并把常量辨別為已解析狀态)進而避免解析動作重複進行。無論是否真正執行了多次解析動作,虛拟機需要保證的都是在同一個實體中,如果一個符号引用之前已經被成功解析過,那麼後續的引用解析請求就應當一直成功;同樣地,如果第一次解析失敗了,其他指令對這個符号的解析請求也應該收到相同的異常。
下面将講解這四種引用的解析過程。
- 1.類或接口的解析:
假設目前代碼所處的類為D,如果要把一個從未解析過的符号引用N解析為一個類或接口C的直接引用,那虛拟機完成整個解析的過程需要包括以下3個步驟:
1)如果C不是一個數組類型,那虛拟機将會把代表N的全限定名傳遞給D的類加載器去加載這個類C。在加載過程中,由于無資料驗證、位元組碼驗證的需要,又将可能觸發其他相關類的加載動作,例如加載這個類的父類或實作的接口。一旦這個加載過程出現了任何異常,解析過程就将宣告失敗
2)如果C是一個數組類型,并且數組的元素類型為對象,也就是N的描述符會是類似“[ Ljava. ang Integer”的形式,那将會按照第1點的規則加載數組元素類型。如果N的描述符如前面所假設的形式,需要加載的元素類型就是“ java.lang.Integer”,接着由虛拟機生成一個代表此數組次元和元素的數組對象
3)如果上面的步驟沒有出現任何異常,那麼C在虛拟機中實際上已經成為一個有效的類或接口了,但在解析完成之前還要進行符号引用驗證,确認C是否具備對D的通路權限。如果發現不具備通路權限,将抛出 java. lang. IllegalAccessError異常
- 2.字段解析:
要解析一個未被解析過的字段符号引用,首先将會對字段表内 class index項中索引的CONSTANT_Class_info符号引用進行解解析,也就是字段所屬的類成接口的符号引用。如果在解析這個類或接口符号引用的過程中出現了任何異常,都會導緻字段符号引用解析的失敗。如果解析成功完成,那将這個字段所屬的類或接口用C表示,虛拟機規範要求按照如下步驟對C進行後續字段的搜尋
1)如果C本身就包含了簡單名稱和字段描述符都與目标相比對的字段,則返這個字段的直接引用,查找結束
2)否則,如果在C中實作了接口,将會按照繼承關系從上往下遞歸搜尋各個接口和它的父接口,如果接口中包含了簡單名稱和字段描述符都與目标相比對的字段,則傳回這個字段的直接引用,查找結束
3)否則,如果C不是 java. lang Object的話,将會按照繼承關系從上往下遞歸搜尋其父類)如果在父類中包含了簡單名稱和字段描述符都與目标相比對的字段,則傳回這個字段的直接引用,查找結束
4)否則,查找失敗,抛出 java. lang NoSuch Field Error異常
如果查找過程成功傳回了引用,将會對這個字段進行權限驗證,如果發現不具備對字段的通路權限,将抛出 java. lang. IllegalAccess Error異常
在實際應用中,虛拟機的編譯器實作可能會比上述規範要求得更加嚴格一些,如果有一個同名字段同時出現在C的接口和父類中,或者同時在自己或父類的多個接口中出現,那編譯器将可能拒絕編譯。在代碼清單7-4中,如果注釋了Sub類中的“ public static int A=4;”,接口與父類同時存在字段A,那編譯器将提示“ The field Sub.A is ambiguous" ,并且會拒絕編譯這段代碼
- 3.類方法解析
類方法解析的第一個步驟與字段解析一樣,也是需要先解析出類方法表的 class index項中索引的方法所屬的類或接口的符号引用,如果解析成功,我們依然用C表示這個類,接下來虛拟機将會按照如下步驟進行後續的類方法搜尋:
1)類方法和接口方法符号引用的常量類型定義是分開的,如果在類方法表中發現 class_index中索引的C是個接口,那就直接抛出java.lang.IncompatibleClassChangeError異常。
2)如果通過了第(1)步,在類C中查找是否有簡單名稱和描述符都與目标相比對的方法,如果有則傳回這個方法的直接引用,查找結束。
3)否則,在類C的父類中遞歸查找是否有簡單名稱和描述符都與目标相比對的方法,如果有則傳回這個方法的直接引用,查找結束。
4)否則,在類C實作的接口清單及它們的父接口之中遞歸查找是否有簡單名稱和描述符都與目标相比對的方法,如果存在比對的方法,說明類C是一個抽象類,這時候查找結束,抛出java.lang.AbstractMethodError異常
5)否則,宣告方法查找失敗,抛出java.lang.NoSuchMethodError
最後,如果查找過程成功傳回了直接引用,将會對這個方法進行權限驗證;如果發現不具備對此方法的通路權限,将抛出 java .lang.IllegalAccessError異常
- 4.接口方法解析
接口方法也是需要先解析出接口方法表的 class index項中索引的方法所屬的類或接口的符号引用,如果解析成功,依然用C表示這個接口,接下來虛拟機将會按照如下步驟進行後續的接口方法搜尋:
1)與類方法解析相反,如果在接口方法表中發現 class index中的索引C是個類而不是接口,那就直接抛出java.lang. IncompatibleClassChangeError異常。
2)否則,在接口C中查找是否有簡單名稱和描述符都與目标相比對的方法,如果有則傳回這個方法的直接引用,查找結束。
3)否則,在接口C的父接口中遞歸查找,直到 javalang Object類(查找範圍會包括 Object類)為止,看是否有簡單名稱和描述符都與目标相比對的方法,如果有則傳回這個方法的直接引用,查找結束。
4)否則,宣告方法查找失敗,抛出java.lang.NoSuchMethodError異常
由于接口中的所有方法都預設是 public的,是以不存在通路權限的問題,是以接口方法的符号解析應當不會抛出java.lang. IllegalAccess Error異常。
3) 、初始化:
為類的靜态變量賦予正确的初始值。為新的對象配置設定記憶體,為執行個體變量賦預設值,為執行個體變量賦正确的初始值。初始化階段就是指向類構造器方法
<clinit>()
【意思是class init】的過程,此方法不需定義,是javac編譯器自動收集類中的所有類變量的指派動作和靜态代碼塊中的語句合并而來。
<clinit>()
不同于類的構造器,若該類具有父類,JVM會保證子類的
<clinit>
執行前,父類的
<clinit>
已經執行完畢。JVM必須保證一個類的
<clinit>()
方法在多線程下被同步加鎖。
類加載最後階段,若該類具有超類,則對其進行初始化,執行靜态初始化器和靜态初始化成員變量(如前面隻初始化了預設值的static變量将會在這個階段指派,成員變量也将被初始化)。
3.1、
<clinit>()
是由編譯器自動收集類中的所有類變量的指派動作(static變量)和靜态語句塊(static代碼塊)中的語句合并産生的,編譯器收集的順序是由語句在源檔案中出現的順序所決定的,靜态語句塊中隻能通路到定義在靜态語句塊之前的變量,定義在它之後的變量,在前面的靜态語句塊中可以指派,但是不能通路。
3.2、
<clinit>()
方法與類的構造函數(或者說執行個體構造器
<init>
方法)不同,它不需要顯式地調用父類構造器,虛拟機會保證在子類的
<clinit>()
方法執行之前,父類的
<clinit>()
方法已經執行完畢。是以在虛拟機中第一個被執行的
<clinit>()
方法的類肯定是java.lang.Object
3.3、由于父類的
<clinit>()
方法先執行,也就意味着父類中定義的靜态語塊要優先于子類的變量指派操作,如代碼清單7-5中,字段B的值将會是2而不是1
static class Parent{
public static int A=1;
static{
A=2;
}
}
public class Sub extends Parent{
public static int B=A;
}
public static void main(String[] args){
Sub.B.sout;
}
3.4、
<clinit>()
方法對于類和接口來說并不是必須的,如果一個類中沒有靜态代碼塊,也沒有對變量的指派操作,那麼編譯器可以不為這個類生成
<clinit>()
方法方法
3.5、 接口中不能使用靜态代碼塊,但如果有變量初始化的指派操作,接口與類一樣都會生成
<clinit>()
方法。但接口與類不同的是,執行接口的
<clinit>()
方法不需要先執行父接口的
<clinit>()
方法。隻有父接口定義的變量被使用時,父接口才回被初始化。另外,接口的實作類在初始化時也一樣不會執行接口的
<clinit>()
方法
3.6、 虛拟機會保證一個類的
<clinit>()
方法在多線程環境中被正确地加鎖和同步,如果多個線程同時去初始化一個類,那麼隻會有一個線程去執行這個類的
<clinit>()
方法,其他線程都需要阻塞等待,直到活動線程執行
<clinit>()
方法完畢。如果一個類的
<clinit>()
方法有耗時很長的操作,那就可能造成多個程序阻塞,在實際應用中這種阻塞往往是很隐蔽的。
執行個體初始化是在執行個體的構造函數中,而他相應的父類是調用super()完成的,有父類有無參構造時,super可以省略,此時子類可以使用this調用構造方法,this在構造方法的作用是調用子類的其他構造方法;父類沒有無參構造時,子類不能使用this調用構造方法
- 如果沒有顯示寫super(),那麼将加在第一句。
- 當父類沒有無參構造函數時,在子類構造方法中必須顯示指定,如super(“hello”)
非靜态成員的指派,是在自己的構造調用之後,并且是在自己的構造調用完父類的構造super之後,
在非靜态成員全部指派完成,才會繼續執行自己構造内,剩餘代碼。
以final關鍵字為例先體會一下類加載流程
// 常量都是用final來修飾的,是以隻要在包含它類執行個體化對象完成之前初始化就行了,什麼都不影響。但是如果前面加個static表明類裝載時這個常量必須是有個狀态的(被賦予了值,初始化了),是以如果用static就必須類加載時初始化。
// 隻被final關鍵字修飾的常量,可以在其類加載時就初始化,也可以到類的構造方法裡面再對它進行初始化:例如
class A{
final int i;//或者final int i=10; // 有沒有值無所謂,執行個體化的構造方法完成之前有值就可
public A(){
i=10;
}
}
//用static和final關鍵字同時修飾的常量就必須得在定義時初始化,例如:
class A{
static final int i=10;//編譯時候就指派了,它是常量
}
// 基本類型,是值不能被改變 //引用類型,是位址值不能被改變,對象中的屬性可以改變
public static void method(final int x) { //此處的final修飾的 x随着方法使用完畢後回收,當再次調用時,重新配置設定空間
System.out.println(x);
}
說明:
- 在java代碼中,類的加載、連接配接和初始化過程都是在程式運作期間完成的。(類從磁盤加載到記憶體中經曆的三個階段)
- 類從磁盤上加載到記憶體中要經曆五個階段:加載、連接配接、初始化、使用、解除安裝
二、1)類加載器的分類
類加載定義:将類的.class檔案中的二進制資料(位元組流)讀入到記憶體中,将其放在運作時資料區的方法區内,然後在記憶體中建立一個java.lang.Class對象(規範并未說明Class對象位于哪裡,HotSpot虛拟機将其放在方法區中)用來封裝内在方法區内的資料結構。
類的加載時機:類并不需要等到某個類被“首次主動使用”時再加載它:JVM規範允許類加載器在預料某個類将要被使用時就預先加載它,如果在預先加載的過程中遇到了.class檔案缺失或存在錯誤,類加載器必須在程式首次主動使用該類才報告錯誤(LinkageError錯誤),如果這個類沒有被程式主動使用,那麼類加載器就不會報告錯誤。
将二進制位元組流鎖代表的靜态存儲結構轉化為方法區的運作時資料結構
注:
- class檔案在檔案開頭有特定的檔案标示
- ClassLoader隻負責class檔案的加載,至于它是否可以運作,則由Execution Engine決定
-
加載.class檔案的方式
(1)從本地系統中直接加載
(2)通過網絡下載下傳.class檔案
(3)從zip,jar等歸檔檔案中加載.class檔案
(4)從專用資料庫中提取.class檔案
(5)将java源檔案動态編譯為.class檔案
① 啟動類加載器
① 啟動類加載器/根加載器/引導類加載器(Bootstrap):
- C++編寫。預設加載路徑
,或者被-Xbootclasspath參數所指定的路徑。裡面有如rt.jar/sun/misc/Launcher.class,Object.class等。該加載器沒有父加載器,它負責加載虛拟機中的核心類庫。根類加載器從系統屬性sun.boot.class.path所指定的目錄中加載類庫。類加載器的實作依賴于底層作業系統,屬于虛拟機的實作的一部分,它并沒有內建java.lang.ClassLoader類。出于安全考慮,根加載器隻加載包名為java,javax,sun等開頭的類,意味着及時将你自己的jar放到該目錄下也不一定被加載,因為在JVM内已經按照檔案名識别。$JAVAHOME/$jre/lib/rt.jar
//通過java對象.object.getClass().getClassLoader();擷取該類的載器
Object object = new Object(); // object類的類加載器是根加載器
object.getClass().getClassLoader();//null。Bootstrap根加載器是c++寫的,java查不出來
try {
object.getClass().getClassLoader().getParent();//報錯,根加載器是最初級的了
} catch (Exception e) {
System.out.println(object.getClass().getClassLoader() + "沒有父加載器了");
}
//-----------------------------------------
Object object2 = new Main2();//自己寫的類
System.out.println(object2.getClass().getClassLoader());//[email protected]
//自定義類預設的加載器是應用加載器AppClassloader//sun.misc.launcher$AppClassLoader$18b4aac2。位于rt.jar包中
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();//擷取系統加載器 /
System.out.println(systemClassLoader);//[email protected]
System.out.println(systemClassLoader.getParent());// [email protected]
System.out.println(systemClassLoader.getParent().getParent());//null
//擷取根加載器所能加載的路徑
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
/*
file:/E:/Java/jdk1.8.0_231/jre/lib/resources.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/rt.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/sunrsasign.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/jsse.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/jce.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/charsets.jar
file:/E:/Java/jdk1.8.0_231/jre/lib/jfr.jar
file:/E:/Java/jdk1.8.0_231/jre/classes
*/
System.out.println(System.getProperty("sun.boot.class.path"));//擷取根加載器路徑 // 結果和上面的周遊結果一緻
② 擴充類加載器
- Java編寫 ,由sun.misc.Launcher$ExtClassLoader(意思是說ExtClassLoader是Launcher的靜态内部類)
- 預設加載路徑
(或通過JDK安裝目錄/jre/lib/ext/*.jar
系統屬性重新指定)如果使用者建立的JAR放在此目錄下,也會由拓展類加載器自動加載。-Djava.ext.dirs
類結構
繼承了ClassLoader,并且是Launcher的靜态内部類
public class Launcher {
static class ExtClassLoader extends URLClassLoader {
public class URLClassLoader extends SecureClassLoader implements Closeable {
public class SecureClassLoader extends ClassLoader {
加載路徑
預設加載路徑
JDK安裝目錄/jre/lib/ext/*.jar
(或通過
-Djava.ext.dirs
系統屬性重新指定)如果使用者建立的JAR放在此目錄下,也會由拓展類加載器自動加載。
System.out.println(System.getProperty("java.ext.dirs"));//擷取擴充類加載器路徑
// E:\Java\jdk1.8.0_231\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
③ 應用程式類加載器
應用程式類加載器(AppClassLoader,也叫系統類加載器):
- Java編寫,它的父加載器為擴充類加載器,他是使用者自定義的類加載器的預設父加載器。sun.misc.Launcher$AppClassLoader,也是靜态内部類
- 加載目前應用的classpath的所有類。預設加載路徑為:
(可以通過環境變量$classpath
重新指定)。-Djava.class.path
- 可以通過
擷取到應用類加載器。ClassLoader.getSystemClassLoader()
類結構
繼承了ClassLoader,并且是Launcher的靜态内部類
//
public class Launcher {
static class AppClassLoader extends URLClassLoader {
public class URLClassLoader extends SecureClassLoader implements Closeable {
public class SecureClassLoader extends ClassLoader {
加載路徑
加載路徑
System.out.println(System.getProperty("java.class.path"));//擷取應用類加載器路徑
/*
E:\Java\jdk1.8.0_231\jre\lib\charsets.jar;E:\Java\jdk1.8.0_231\jre\lib\deploy.jar;。。。E:\Java\jdk1.8.0_231\jre\lib\management-agent.jar;E:\Java\jdk1.8.0_231\jre\lib\plugin.jar;E:\Java\jdk1.8.0_231\jre\lib\resources.jar;E:\Java\jdk1.8.0_231\jre\lib\rt.jar;F:\springbootTest\testBoot\target\classes;D:\MavenRepository\org\springframework\boot\spring-boot-starter-web\2.3.1.RELEASE\spring-boot-starter-web-2.3.1.RELEASE.jar;D:\MavenRepository\org\springframework\boot\spring-boot-starter\2.3.1.RELEASE\spring-boot-starter-2.3.1.RELEASE.jar;
。。。省略
D:\MavenRepository\org\springframework\spring-jcl\5.2.7.RELEASE\spring-jcl-5.2.7.RELEASE.jar;D:\MavenRepository\org\projectlombok\lombok\1.18.12\lombok-1.18.12.jar;E:\JetBrains\IntelliJ IDEA 2019.3.3\lib\idea_rt.jar
*/
System.out.println(System.getProperty("sun.boot.class.path"));//列印跟加載路徑
// 輸出樣式為E:\Java\jdk1.8.0_231\jre\lib\resources.jar;E:\Java\jdk1.8.0_231\jre\lib\rt.jar;E:\Java\jdk1.8.0_231\jre\lib\sunrsasign.jar;E:\Java\jdk1.8.0_231\jre\lib\jsse.jar;E:\Java\jdk1.8.0_231\jre\lib\jce.jar;E:\Java\jdk1.8.0_231\jre\lib\charsets.jar;E:\Java\jdk1.8.0_231\jre\lib\jfr.jar;E:\Java\jdk1.8.0_231\jre\classes
④使用者自定義加載器
④使用者自定義加載器:Java.lang.ClassLoader的子類,使用者可以定制類的加載方式。預設加載路徑
\$CLASSPATH
;
- 為什麼要使用自定義類加載器:隔離加載類、修改類加載的方式、拓展加載源、防止源碼洩漏
- 實作步驟:繼承抽象類Java.lang.ClassLoader,重寫findClass()方法
層次為:根類加載器–>擴充類加載器–>系統應用類加載器–>自定義類加載器
三、各加載器的源碼
前面我們稍微提過了,ExtClassLoader和AppClassLoader是Launcher的靜态内部類。
什麼時候初始化呢?
Launcher
的構造與2個類加載器
Launcher
下面說明了在執行Launcher()無參構造時就構造了系統類加載器,指派到Launcher的屬性中
指派系統類加載器的時候,因為它的父加載器是擴充類加載器,是以先執行個體化了一個擴充類加載器,然後在執行個體化擴充類加載器的時候傳進去
public class Launcher {
// 自己把自己執行個體化了
private static Launcher launcher = new Launcher();
//
private static String bootClassPath =
System.getProperty("sun.boot.class.path");
private ClassLoader loader;
public Launcher() {
// Create the extension class loader
ClassLoader extcl;
extcl = ExtClassLoader.getExtClassLoader();
// Now create the class loader to use to launch the application
loader = AppClassLoader.getAppClassLoader(extcl);
//設定AppClassLoader為線程上下文類加載器,這個文章後面部分講解
Thread.currentThread().setContextClassLoader(loader);
}
上面那個
System.getProperty("sun.boot.class.path")
結果為
C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;
C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;
C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;
C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;
C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;
C:\Program Files\Java\jre1.8.0_91\classes
AppClassLoader源碼:
/**
* The class loader used for loading from java.class.path.
* runs in a restricted security context.
*/
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader extcl){
final String s = System.getProperty("java.class.path");
final File[] path = (s == null) ? new File[0] : getClassPath(s);
return AccessController.doPrivileged(
new PrivilegedAction<AppClassLoader>() {
public AppClassLoader run() {
URL[] urls =
(s == null) ? new URL[0] : pathToURLs(path);
return new AppClassLoader(urls, extcl);
}
});
}
......
}
/**
用指定的父類加載建立一個新的類加載器
如果有安全管理器,将調用它的SecurityManager#checkCreateClassLoader()方法,它可能引發安全異常
*/
protected ClassLoader(ClassLoader parent) {// 傳入父類加載器
this(checkCreateClassLoader(), parent);
}
protected ClassLoader(){
// 預設會傳入系統類加載器作為父類加載器
this(checkCreateClassLoader(), getSystemClassLoader());
}
四、雙親委派模型
如果自定義了一個java.lang.String類,是不起作用的。為什麼呢?
類加載器用來把類加載到java虛拟機中。從JDK1.2版本開始,類的加載過程采用父親委托機制,這種機制能更好地保證Java平台的安全。在此委托機制中,除了java虛拟機自帶的根類加載器以外,其餘的類加載器都有且隻有一個父加載器。當java程式請求加載器loader1加載A類時,loader1首先委托自己的父加載器去加載A類,若父加載器能加載,則由父加載器完成加載任務,否則才由加載器loader1本身加載A類。
當一個類收到了類加載請求,他首先不會嘗試自己去加載這個類,而是把這個請求委派給父類類加載去完成,每一個層次類加載器都是如此,是以所有的加載請求都應該傳送到啟動類加載其中,隻有當父類加載器回報自己無法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的Class),子類加載器才會嘗試自己去加載。
若有一個類能夠成功加載Test類,那麼這個類加載器被稱為定義類加載器,所有能成功傳回Class對象引用的類加載器(包括定義類加載器)稱為初始類加載器。
比如一個自定義類,可以由自定義類加載器和系統加載器加載,但是類路徑下沒有class檔案,是以系統加載器加載不到,隻有自定義加載器能加載到。此時,系統加載器和自定義加載器都是定義類加載器;而自定義加載器才是初始類加載器。詳情可以看Test16
采用雙親委派的一個好處是比如加載位于 rt.jar 包中的類 java.lang.Object,不管是哪個加載器加載這個類,最終都是委托給頂層的啟動類加載器進行加載,這樣就保證了使用不同的類加載器最終得到的都是同樣一個 Object對象。
雙親委托模型的好處:
- (1)可以確定Java和核心庫的安全:所有的Java應用都會引用java.lang中的類,也就是說在運作期java.lang中的類會被加載到虛拟機中,如果這個加載過程如果是由自己的類加載器所加載,那麼很可能就會在JVM中存在多個版本的java.lang中的類,而且這些類是互相不可見的(命名空間的作用)。借助于雙親委托機制,Java核心類庫中的類的加載工作都是由啟動根加載器去加載,進而確定了Java應用所使用的的都是同一個版本的Java核心類庫,他們之間是互相相容的;
- (2)確定Java核心類庫中的類不會被自定義的類所替代;
- (3)不同的類加載器可以為相同名稱的類(binary name)建立額外的命名空間。相同名稱的類可以并存在Java虛拟機中,隻需要用不同的類加載器去加載即可。相當于在Java虛拟機内部建立了一個又一個互相隔離的Java類空間。
沙箱安全機制:自定義String類,但是在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先加載jdk自帶的檔案(rt.jar包中java/lang/String.class),報錯資訊說沒有main方法就是因為加載的是rt.jar包中的String類,這樣可以保證對java核心源代碼的保護,這就是沙箱安全。
命名空間與類的隔離
兩個class相同條件:類名相同,且要同一個類加載器加載的。隻有兩者同時滿足的情況下,JVM才認為這兩個class是相同的
命名空間:命名空間由該加載器及所有父加載器所加載的類構成。每個類加載器都有自己的命名空間
- 在同一個命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類;
- 在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類;
- 同一命名空間内的類是互相可見的,非同一命名空間内的類是不可見的;
- 子加載器可以見到父加載器加載的類,父加載器也不能見到子加載器加載的類。
同樣不同的類加載器加載的類生産的對象也是不能類型轉換的
類的隔離
// 這部分的代碼借鑒了https://blog.csdn.net/a745233700/article/details/89245847
package classloader;
public class NewworkClassLoaderTest {
public static void main(String[] args) {
try {
//測試加載網絡中的class檔案
String rootUrl = "http://localhost:8080/httpweb/classes";
String className = "org.classloader.simple.NetClassLoaderSimple";
// 不同的類加載器
NetworkClassLoader ncl1 = new NetworkClassLoader(rootUrl);
Class<?> clazz1 = ncl1.loadClass(className);
Object obj1 = clazz1.newInstance();
// 類加載器2
NetworkClassLoader ncl2 = new NetworkClassLoader(rootUrl);
Class<?> clazz2 = ncl2.loadClass(className);
Object obj2 = clazz2.newInstance();
clazz1.getMethod("setNetClassLoaderSimple", Object.class).invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
package org.classloader.simple;
public class NetClassLoaderSimple {
private NetClassLoaderSimple instance;
public void setNetClassLoaderSimple(Object obj) {
this.instance = (NetClassLoaderSimple)obj;
}
}
首先獲得網絡上一個class檔案的二進制名稱,然後通過自定義的類加載器NetworkClassLoader建立兩個執行個體,并根據網絡位址分别加載這份class,并得到這兩個ClassLoader執行個體加載後生成的Class執行個體clazz1和clazz2,最後将這兩個Class執行個體分别生成具體的執行個體對象obj1和obj2,再通過反射調用clazz1中的setNetClassLoaderSimple方法。
自定義加載器
為什麼要自定義類加載器:
- 隔離加載類
- 修改類加載的方式
- 擴充加載源
- 防止源碼洩漏
- 自定義路徑下查找自定義的class類檔案,也許我們需要的class檔案并不總是在已經設定好的Classpath下面,那麼我們必須想辦法來找到這個類,在這種清理下我們需要自己實作一個ClassLoader。
- 確定安全性:Java位元組碼很容易被反編譯,對我們自己的要加載的類做特殊處理,如保證通過網絡傳輸的類的安全性,可以将類經過加密後再傳輸,在加密到JVM之前需要對類的位元組碼在解密,這個過程就可以在自定義的ClassLoader中實作。
- 實作類的熱部署:可以定義類的實作機制,如果我們可以檢查已經加載的class檔案是否被修改,如果修改了,可以重新加載這個類。
自定義類加載器方法:
- 繼承抽象類java.lang.ClassLoader
- 在JDK1.2之前,需要重寫loadClass();JDK1.2之後,不需要重寫loadClass()了,建議重寫
findClass()
-
的功能是找到class檔案并把位元組碼加載到記憶體中。自定義的ClassLoader一般覆寫該方法,以便使用不同的加載路徑,然後調用defineClass()解析位元組碼。findClass()
-
方法用來将byte位元組流解析成JVM能夠識别的Class對象。有了這個方法意味着我們不僅僅可以通過class檔案執行個體化對象,還可以通過其他方式執行個體化對象,如我們通過網絡接收到一個類的位元組碼,拿這個位元組碼流直接建立類的Class對象形式執行個體化對象。defineClass()
- 自定義的加載器可以覆寫方法loadClass()以便定義不同的加載機制。
- 如果自定義的加載器僅覆寫了findClass(),而未覆寫loadClass(即加載規則一樣,但加載路徑不同);則調用getClass().getClassLoader()傳回的仍然是AppClassLoader!因為真正的load類,還是AppClassLoader
-
- 如果沒有太多複雜的需求,可以直接繼承URLCloassLoader類,就可以避免自己編寫findClass()及其擷取位元組碼流的方式,使自定義類加載器編寫更加簡潔。可以看test16
JVM中表示兩個class對象是否為同一個類存在兩個必要條件:
- 類的完整類名必須一緻,包括包名
-
加載這個類的ClassLoader(指CLassLoader執行個體對象)必須相同
換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個class檔案,被同一個虛拟機所加載,但隻要加載它們的ClassLoader執行個體對象不同,那麼這兩個類對象也是不相等的。
應用:
-
加載自定義格式的class檔案
如果我們從網路上下載下傳一個class檔案的位元組碼,但是為了安全性在傳輸之前對這個位元組碼進行了簡單的加密處理,然後再通過網絡傳輸。當用戶端接收到這個類的位元組碼後需要經過解密才能還原成原始的類格式,然後再通過ClassLoader的defineClass()方法建立這個類的執行個體,最後完成類的加載工作。
比如上面的代碼中,在擷取到位元組碼(byte[] classData = getData(name);)之後再通過一個類似以下的代碼:
private byte[] deCode(byte[] src){ byte[] decode = null; //do something niubility! 精密解碼過程 return decode; }
hello-world
/**
* 自定義類加載器
*/
public class MyClassLoader extends ClassLoader {
private String rootDir;/*自定義類加載的查找class的路徑*/
/*指定該類加載器會查找的rootDir目錄,和父加載器*/
public MyClassLoader(String rootDir, ClassLoader parent){
// 父類無參構造預設會加載系統類加載器
super(parent);
this.rootDir = rootDir;
}
/*指定該類加載器會查找的rootDir目錄*/
public MyClassLoader(String rootDir){
this.rootDir = rootDir;
}
/**
* 自定義自己的類加載器,如沒有要改變類加載順序的必要的話,則重寫findClass方法,因為這個方法是JDK預留了給我們實作的,
* 否則就需要修改loadClass的實作。
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//<1>.根據類的全路徑(包含包名)類名和放置的目錄确定類檔案的路徑
String className = name.substring(name.lastIndexOf(".")+1)+ ".class";
String classFile = rootDir + File.separator + className;
FileInputStream fileInputStream = null;
byte[] classData = null;
try {
//<2>.将class檔案讀取到位元組數組
fileInputStream = new FileInputStream(new File(classFile));
classData = new byte[fileInputStream.available()];
fileInputStream.read(classData,0,classData.length);
//<3>.将位元組資料建立一個class
return defineClass(name,classData,0,classData.length);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fileInputStream != null){
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//<4>如果父類加載器不是自定義的,上面的加載過程沒加載成功,則此調用會throw ClassNotFoundException
return super.findClass(name);
}
}
public class CustomClassLoaderTest {
/*定義了一個目錄存放class檔案,這個其實可以修改為可配置參數*/
private static final String rootDir = "D:/class/";
public static void main(String[] args) throws Exception {
/*<1> 從指定的目錄下查找對應的class檔案,進行加載,然後建立該對象,如果加載存在則加載成功,則類加載器應為MyClassLoader*/
MyClassLoader classLoader = new MyClassLoader(rootDir);
Class c = classLoader.loadClass("com.learn.classloader.custom.Person");
Object object = c.newInstance();
Method getNameMethod = c.getMethod("getName");
Method getAgeMethod = c.getMethod("getAge");
System.out.println("name:" + getNameMethod.invoke(object) + ",age:" + getAgeMethod.invoke(object));
System.out.println("類加載器為:" + object.getClass().getClassLoader());
}
}
因為該Person類,在IDE中編譯後放在了classpath,而classpath預設是由ApplicationClassLoader進行加載的,而MyClassLoader的parent為ApplicationClassLoader,是以如果在IDE中執行測試程式,根據雙親委派機制,Person類的類加載器将一直是ApplicationClassLoader,下圖是運作的結果:
顯然,直接在IDE中執行測試類是沒有辦法使用我們自定義的類加載器實作類加載的,這一切的原因就是Person類在classpath中,是以解決方法就是将編譯後的Person類,放到"D:/class/"目錄下,這個是例子中定義的目錄,根據具體情況自己指定。如下圖所示:
接下來,我們先将項目打包成Jar包,此時Jar中含有編譯後的Person類,然後需要将該類從中删除掉,然後再運作Jar程式,如果不删除jar包中的Peson類,則會仍會是ApplicationClassLoader,如下所示:
下面删除jar包中的Person類,那麼就會從"D:/class"裡進行加載,也就是自定義的類加載器去加載的,如下所示,顯然類加載器已經是MyClassLoader了:
實作類的熱部署
JVM預設不能熱部署類,因為加載類時會去調用findLoadedClass(),如果類已被加載,就不會再次加載。
JVM判斷類是否被加載有兩個條件:完整類名是否一樣,ClasssLoader是否是同一個
是以要實作熱部署的話,隻需要使用ClassLoader的不同執行個體來加載。
如果用同一個ClassLoader執行個體來加載同一個類,則會抛出LinkageError.
Jsp就是一個熱部署的例子。
所謂的熱部署就是利用同一個class檔案,不同的類加載器在記憶體建立出兩個不同的class對象(關于這點的原因前面已分析過,即利用不同的類加載執行個體),由于JVM在加載類之前會檢測請求的類是否已加載過(即在loadClass()方法中調用findLoadedClass()方法),如果被加載過,則直接從緩存擷取,不會重新加載。注意同一個類加載器的執行個體和同一個class檔案隻能被加載器一次,多次加載将報錯,是以我們實作的熱部署必須讓同一個class檔案可以根據不同的類加載器重複加載,以實作所謂的熱部署。實際上前面的實作的FileClassLoader和FileUrlClassLoader已具備這個功能,但前提是直接調用findClass()方法,而不是調用loadClass()方法,因為ClassLoader中loadClass()方法體中調用findLoadedClass()方法進行了檢測是否已被加載,是以我們直接調用findClass()方法就可以繞過這個問題,當然也可以重載loadClass方法,但強烈不建議這麼幹。利用FileClassLoader類測試代碼如下:
public static void main(String[] args) throws ClassNotFoundException {
String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
//建立自定義檔案類加載器
FileClassLoader loader = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);
try {
//加載指定的class檔案,調用loadClass()
Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
Class<?> object2=loader2.loadClass("com.zejian.classloader.DemoObj");
System.out.println("loadClass->obj1:"+object1.hashCode());
System.out.println("loadClass->obj2:"+object2.hashCode());
//加載指定的class檔案,直接調用findClass(),繞過檢測機制,建立不同class對象。
Class<?> object3=loader.findClass("com.zejian.classloader.DemoObj");
Class<?> object4=loader2.findClass("com.zejian.classloader.DemoObj");
System.out.println("loadClass->obj3:"+object3.hashCode());
System.out.println("loadClass->obj4:"+object4.hashCode());
/**
* 輸出結果:
* loadClass->obj1:644117698
loadClass->obj2:644117698
findClass->obj3:723074861
findClass->obj4:895328852
*/
} catch (Exception e) {
e.printStackTrace();
}
}
五、雙親委派源碼淺析
Class.forName
通常用法:
Class<A> a = Class.forName("A類的全類名");
兩個重載方法:
public static Class<?> forName(String className);
public static Class<?> forName(String name, boolean initialize,ClassLoader loader);
第二個構造函數指定了父類加載器,這兒可能要有疑問了,第一個方法預設使用哪個類加載器來加載的呢?(答案是得看它在哪個對象中執行的)
我們來看下具體實作:
public static Class<?> forName(String className)
throws ClassNotFoundException {
// 使用native方法擷取調用類的Class對象
Class<?> caller = Reflection.getCallerClass();
// 傳入的是調用者的類加載器,比如在A類裡執行了forName,那麼類加載器就是加載A類的類加載器
return forName0(className,
true,
ClassLoader.getClassLoader(caller),
caller);
// native方法,看不了了
}
// 擷取類的加載器
static ClassLoader getClassLoader(Class<?> caller) {
if (caller == null) {
return null;
}
return caller.getClassLoader0();
}
@CallerSensitive
public static Class<?> forName(String name,
boolean initialize,
ClassLoader loader){
Class<?> caller = null;
// 擷取安全管理器
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
好了,我們知道了,要通過Class.forName加載器一個類,你可以自己傳入一個類加載器,你不傳預設使用的是調用Class.forName處所用的類加載器
上面的邏輯重要的是如何擷取一個類的類加載器
ClassLoader#loadClass
步驟:
- 1.加鎖,防止重複加載
- 2.查找是否已經被加載過,如果已經被加載過了則直接傳回
- 3.判斷是否有父類加載器,如果有則交給父類加載器進行加載,直到Bootstrap ClassLoader
- 4.如果父類加載器都沒有加載成功,則會調用子類加載器的findClass,嘗試用字類加載器加載.
//java.lang.ClassLoader#loadClass(java.lang.String, boolean)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
//<1> 加鎖
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//<2> 查找是否已經被加載過
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//<3>.判斷是否有父類加載器
if (parent != null) {
//<3.1> 有父類加載器,先使用父類加載器進行加載
c = parent.loadClass(name, false);
} else {
//<3.2> 如果沒父類加載器,則表明類加載器已經是Bootstrap ClassLoader
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();
//<4> 如果該類還沒有加載,則會調用findClass繼續加載,這個方法的除非顯然是上面代碼沒有加載成功才會走該分支,也就是如果父類加載失敗了,子類通過的是該類進行加載的。
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;
}
}
上面的原理就是為什麼推薦重寫findClass()而不是loadClass()。重寫findClass()不僅簡單,而且還能都避免覆寫預設加載器的父類委托、緩存機制等,一舉兩得。
findClass
方法含義:給定一個全類名字元串,找能加載這個位址class二進制檔案的類加載器
傳回值:加載好的class對象
接下來我們來檢視一下ClassLoader#findClass(String name),會發現該方法是空實作,直接抛出了異常,如下所示的代碼,這是什麼原因了;其實在簽名提到了,所有類加載器,獨立于虛拟機外部,并且完全繼承自抽象類java.lang.ClassLoader,是以該findClass(String name)方法由具體的的實作類來實作,也就是如果我們自己定義類加載器,在不打破雙親委派機制的前提下,則隻需要重寫這個findClass(String name)方法即可。
//java.lang.ClassLoader#findClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
//直接抛出了ClassNotFoundException異常,顯然父類沒有找到則會抛出該異常,看回上面的loadClass就可以知道,子類是由try-catch該異常,然後再有子類加載的findClass嘗試加載
throw new ClassNotFoundException(name);
}
那麼接下來就看看系統、擴充類加載器的findClass吧
其實,擴充、系統類加載器都沒有複寫findClass()方法,他們的實作都在他們的共同父類
URLClassLoader
中
//java.net.URLClassLoader#findClass
protected Class<?> findClass(final String name){
final Class<?> result;
result = AccessController.doPrivileged(
// 傳入lambda函數式接口的實作類
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//将帶包名的全限定的類名,轉為檔案路徑,例如将com.learn.Student轉為/com/learn/Student.class
String path = name.replace('.', '/').concat(".class");
//加載class資源
Resource res = ucp.getResource(path, false);
if (res != null) {
//開始解析class
return defineClass(name, res);
} else { return null; }
}
},
acc
);
//如果加載失敗,則會抛出ClassNotFoundException
if (result == null) { throw new ClassNotFoundException(name); }
return result;
}
ClassLoader
再介紹一下所有類加載器(除了根)的父類ClassLoader
它是一個抽象類,其後所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)
getParent();
loadClass(String);
findClass();
findLoadedClass();
defineClass();
resolveClass();
- Java虛拟機與程式的生命周期
-
(1)執行了System.exit()方法
(2)程式正常執行結束
(3)程式在執行過程中遇到了異常或錯誤而異常終止
(4)由于作業系統出現錯誤而導緻虛拟機程序終止
擷取類加載器的途徑:
// 擷取目前類的加載器
(1)clazz.getClassLoader();
// 擷取目前線程上下文的加載器
(2)Thread.currentThread().getContextClassLoader();
// 擷取系統的加載器
(3)ClassLoader.getSystemClassLoader();
// 擷取調用者的加載器
(4)DriverManager.getCallerClassLoader();
六、雙親委派的打破
雙親委派模型并不是一個強制性的限制,而僅僅隻是Java設計者推薦開發者的類加載器實作方式。故如果有需要,則可以破壞雙親委派模型,實作自己的類加載器。
雙親委派模型,解決了各個類加載器的基礎類統一的問題,但是如果基礎類又要回調使用者的代碼,也就是基礎類的實作類,那要怎麼辦。為了解決這個問題,Java設計團隊,隻好引入:線程上下文類加載器
首先說下線程上下文類加載器的擷取/設定方式
getContextClassLoader();
setContextClassLoader(ClassLoader cl);
// 如果沒有通過 setContextClassLoader(ClassLoader cl)方法進行設定的話,線程将繼承其父線程的上下文類加載器。而最原始的程序的類加載器是系統類加載器
// 即預設 系統類加載器
@CallerSensitive
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null) return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
這樣,在根加載器加載不了類的時候,就可以拿到根加載器的上下文加載器,比如讓系統類加載器幫它加載一個類
雙親委派的打破–線程上下文類加載器
SPI
服務提供者接口(Service Provider Interface,SPI)
在Java應用中存在着很多服務提供者接口(Service Provider Interface,SPI),這些接口允許第三方為它們提供實作,如常見的 SPI 有 JDBC、JNDI等,這些 SPI 的接口屬于 Java 核心庫,一般存在
rt.jar
包中,由Bootstrap類加載器加載
而 SPI 的第三方實作代碼則是作為Java應用所依賴的 jar 包被存放在
classpath
路徑下,由于SPI接口中的代碼經常需要加載具體的第三方實作類并調用其相關方法,但SPI的核心接口類是由引導類加載器來加載的,而Bootstrap類加載器無法直接加載SPI的實作類,同時由于雙親委派模式的存在,Bootstrap類加載器也無法反向委托AppClassLoader加載器SPI的實作類。在這種情況下,我們就需要一種特殊的類加載器來加載第三方的類庫,而線程上下文類加載器就是很好的選擇。
由于SPI中的類經常需要調用外部實作類的方法,而classpath路徑下的jdbc.jar(包含外部實作類)無法通過Bootstrap類加載器加載,是以隻能委派線程上下文類加載器把jdbc.jar中的實作類加載到記憶體以便SPI相關類使用。顯然這種線程上下文類加載器的加載方式破壞了“雙親委派模型”,它在執行過程中抛棄雙親委派加載鍊模式,使程式可以逆向使用類加載器,當然這也使得Java類加載器變得更加靈活。
線程上下文類加載器
線程上下文類加載器(Thread Context ClassLoader), TCCL。
在JDBC4.0之前,我們開發有連接配接資料庫的時候,通常會用
Class.forName("com.mysql.jdbc.Driver")
這句先加載資料庫相關的驅動,然後再進行擷取連接配接等的操作。而JDBC4.0之後不需要用Class.forName(“com.mysql.jdbc.Driver”)來加載驅動,直接擷取連接配接就可以了,現在這種方式就是使用了Java的「SPI」擴充機制來實作。
// 加載Class到AppClassLoader(系統類加載器),然後注冊驅動類
// Class.forName("com.mysql.jdbc.Driver").newInstance();
String url = "jdbc:mysql://localhost:3306/testdb";
// 通過java庫擷取資料庫連接配接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
為什麼本來是系統類加載器幫着加載
Class.forName("com.mysql.jdbc.Driver")
,但卻不用了呢?肯定是JVM幫我們的執行了類似于這句的語句
首先明确一下這條語句的目的:執行個體化一個Driver的class對象
那為什麼這麼做呢?我們也就多寫一句java語句,原因在于,它想達到一個效果:隻要你引入了我的包,那就自動加載我的那個實作類,不用你自己找這個實作類的全類名了
首先說明下是如何做到的:在META-INF/services/下建立檔案,檔案名為接口名,檔案内容為實作類全類名
那麼誰幫我們完成建立class對象到類裝載器中呢?
其實就是
DriverManager.getConnection()
這個語句,在連接配接前執行個體化class對象
代碼如下,但是要說明的是,誰去加載這些實作類呢?答案是 TCCL(目前線程的類加載器)
//DriverManager是Java核心包rt.jar的類
public class DriverManager {
// 靜态代碼塊
static {
loadInitialDrivers();//執行該方法
}
//loadInitialDrivers方法
private static void loadInitialDrivers() {
String drivers;
// 先讀取系統屬性
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
// 通過SPI加載驅動類,即加載Driver的實作類,生成class對象,然後再生成執行個體對象
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 擷取該接口的加載器 // ServiceLoader是什麼?它實作了Iterable接口,你可以認為它是個list即可
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
return null;
}
});
// 繼續加載系統屬性中的驅動類
String[] driversList = drivers.split(":");
for (String aDriver : driversList) {
// 使用AppClassloader加載
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
}
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
// 這裡傳入cl,去獲得Driver實作類的class對象
c = Class.forName(cn, false, loader);
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
注意上面有個loader,從哪裡來的?這裡不啰嗦了,它是ServiceLoader執行個體的屬性,那麼是誰給它指派的呢?
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
從上面代碼中我們知道,傳入的是目前線程的 線程上下文類加載器
前面說了,預設的上下文類加載器是【系統類加載器】,那麼就用系統類加載器去加載Driver的實作類
總結
再啰嗦一下:
我們從DriverManager來,DriverManager是根加載器能加載的,但是它調用Class.forName的時候,前面說了,如果不指定classLoader,那麼使用的就是執行Class.forName語句所在類的類加載器。
顯然它是從
–>
DriverManager
類下來的,這兩個類都在
ServiceLoader
包下,都是用根加載器加載的,而根加載器加載不了Driver的實作類,因為它不再rt.jar等包下,是以Class.forName時需要自己傳入cl,那麼傳入哪個cl呢?就是從上下文類加載器中拿到的。
rt.jar
好了,我們加載好了Drvier的實作類了,得到對于的class對象了,看下Driver實作類幹嘛了
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() {
}
static {
// Driver已經加載到TCCL中了,此時可以直接執行個體化
DriverManager.registerDriver(new Driver());
}
}
就往驅動管理器DriverManager中注冊了該驅動執行個體
Driver帶不帶cj的問題
我們自己寫demo
Class.forName("com.mysql.jdbc.Driver")
的時候經常會碰到這個相關的報錯,其實是不是報錯,它就是提醒你,現在Driver實作類已經更新了,即使你自己Class.forName加載Driver的時候,也是加載的com.mysql.cj.jdbc.Driver
而com.mysql.jdbc.Driver繼承類如下:
public class Driver extends com.mysql.cj.jdbc.Driver {
public Driver() throws SQLException {
super();
}
static {
System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
+ "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
}
}
七、回到getConnection
到此驅動注冊基本完成,接下來我們回到最開始的那段樣例代碼:java.sql.DriverManager.getConnection()。它最終調用了以下方法:
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) {
/* 傳入的caller由Reflection.getCallerClass()得到,該方法
* 可擷取到調用本方法的Class類,這兒擷取到的是目前應用的類加載器
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
SQLException reason = null;
// 周遊注冊到registeredDrivers裡的Driver類
for(DriverInfo aDriver : registeredDrivers) {
// 檢查Driver類有效性
if(isDriverAllowed(aDriver.driver, callerCL)) {
// 調用com.mysql.jdbc.Driver.connect方法擷取連接配接
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
return (con);
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
}
八、Tomcat與spring的類加載器案例
其實Tomcat、Spring、SpringBoot、SpringMVC都有類似于SPI的機制
你不用每個都去找了,我們猜猜它們的原理:
執行到某一句的時候,因為要建立一個類執行個體或者執行一個類靜态方法,于是要求提前準備一些所需要的基礎類執行個體,但是還是此時隻有接口,額實作類因為每個jar包産商不一樣,是以實作類不一樣,怎麼找實作類呢?還是去指定路徑下找檔案,得到實作類全類名,然後用Class.forName加載,這個過程中可能會涉及TCCL
然後說明一下Tomcat其實自己定義了幾個自己的類加載器,用于加載不同路徑下的class檔案,比如下面幾個自定義類加載器
- 加載java類庫的
-
:加載放置在CommonClassLoader
目錄中:類庫可被Tomcat和所有的Web應用程式共同使用。/common/*
-
:加載放置在CatalinaClassLoader
目錄中:類庫可被Tomcat使用,但對所有的Web應用程式都不可見。/server/*
-
:加載放置在SharedClassLoader
目錄中:類庫可被所有的Web應用程式共同使用,但對Tomcat自己不可見。/shared/*
-
- 加載Web應用程式
-
:加載放置在WebAppClassLoader
目錄中:類庫僅僅可以被此Web應用程式使用,對Tomcat和其他Web應用程式都不可見。其中 WebApp 類加載器和 Jsp 類加載器通常會存在多個執行個體,每一個 Web 應用程式對應一個 WebApp 類加載器,每一個 JSP 檔案對應一個 Jsp 類加載器。/WebApp/WEB-INF/*
-
從圖中的委派關系中可以看出,
CommonClassLoader
能加載的類都可以被
CatalinaClassLoader
和
SharedClassLoader
使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加載的類則與對方互相隔離。WebAppClassLoader 可以使用 SharedClassLoader 加載到的類,但各個 WebAppClassLoader 執行個體之間互相隔離。而 JasperLoader 的加載範圍僅僅是這個 JSP 檔案所編譯出來的那一個 Class,它出現的目的就是為了被丢棄:當伺服器檢測到 JSP 檔案被修改時,會替換掉目前的 JasperLoader 的執行個體,并通過再建立一個新的 Jsp 類加載器來實作 JSP 檔案的 HotSwap 功能。
Spring加載問題
Tomcat 加載器的實作清晰易懂,并且采用了官方推薦的“正統”的使用類加載器的方式。這時作者提一個問題:如果有 10 個 Web 應用程式都用到了spring的話,可以把Spring的jar包放到
common
或
shared
目錄下讓這些程式共享。Spring 的作用是管理每個web應用程式的bean,getBean時自然要能通路到應用程式的類,而使用者的程式顯然是放在
/WebApp/WEB-INF
目錄中的(由
WebAppClassLoader
加載),那麼在
CommonClassLoader
或
SharedClassLoader
中的 Spring 容器如何去加載并不在其加載範圍的使用者程式(/WebApp/WEB-INF/)中的Class呢?
解答:
spring根本不會去管自己被放在哪裡,它統統使用TCCL來加載類,而TCCL預設設定為了WebAppClassLoader,也就是說哪個WebApp應用調用了spring,spring就去取該應用自己的WebAppClassLoader來加載bean,簡直完美~
源碼分析
有興趣的可以接着看看具體實作。在web.xml中定義的listener為org.springframework.web.context.ContextLoaderListener,它最終調用了org.springframework.web.context.ContextLoader類來裝載bean,具體方法如下(删去了部分不相關内容):
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 建立WebApplicationContext
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
// 将其儲存到該webapp的servletContext中
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// 擷取線程上下文類加載器,預設為WebAppClassLoader
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
// 如果spring的jar包放在每個webapp自己的目錄中
// 此時線程上下文類加載器會與本類的類加載器(加載spring的)相同,都是WebAppClassLoader
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
// 如果不同,也就是上面說的那個問題的情況,那麼用一個map把剛才建立的WebApplicationContext及對應的WebAppClassLoader存下來
// 一個webapp對應一個記錄,後續調用時直接根據WebAppClassLoader來取出
currentContextPerThread.put(ccl, this.context);
}
return this.context;
}
具體說明都在注釋中,spring考慮到了自己可能被放到其他位置,是以直接用TCCL來解決所有可能面臨的情況。
總結
通過上面的兩個案例分析,我們可以總結出線程上下文類加載器的适用場景:
- 當高層提供了統一接口讓低層去實作,同時又要是在高層加載(或執行個體化)低層的類時,必須通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類。
- 當使用本類托管類加載,然而加載本類的ClassLoader未知時,為了隔離不同的調用者,可以取調用者各自的線程上下文類加載器代為托管。
九、連結
類被加載後,就進入連接配接階段。連接配接階段就是将已經讀入到記憶體的類的二進制資料合并到虛拟機的運作時環境中去。
-
類的連接配接-驗證
1)類檔案的結構檢查
2)語義檢查
3)位元組碼驗證
4)二進制相容性的驗證
-
類的連接配接-準備
在準備階段,java虛拟機為類的靜态變量配置設定記憶體,并設定預設的初始值(不是真正的值)。例如對于以下Sample類,在準備階段,将為int類型的靜态變量a配置設定4個位元組的記憶體空間,并且賦予預設值0,為long類型的靜态變量b配置設定8個位元組的記憶體空間,并且賦予預設值0;
public class Sample{
private static int a=1;
public static long b;
public static long c;
static { b=2; }
}
十、始化
首先明确是類的初始化而不是類執行個體的初始化。
首先idea安裝jclasslib插件
驗證、準備、解析
初始化階段就是執行構造器方法
<clinit>
,他不是我們普通的構造器。而是類的初始化而且他是自動生成的,編譯器收集類中是以類靜态變量的指派動作和靜态代碼塊中的語句合并而來。沒有靜态變量和靜态代碼塊就不生成clinit方法了。
構造器方法中指令按語句在源檔案中出現的順序執行。
JVM 會確定子類的clinit方法在父類 clinit已經執行結束。
虛拟機必須保證一個類的clinit方法在多線程下被同步加鎖。
在連結階段已經定義好了,在這個階段隻是覆寫。但是不能在定義前調用它,這是編譯器決定的。
而init是我們真正的構造器。
在初始化階段,Java虛拟機執行類的初始化語句,為類的靜态變量賦予初始值。在程式中,靜态變量的初始化有兩種途徑:
- (1)在靜态變量的聲明處進行初始化;
- (2)在靜态代碼塊中進行初始化。
類的初始化步驟:
- (1)假如這個類還沒有被加載和連接配接,那就先進行加載和連接配接
- (2)假如類存在直接父類,并且這個父類還沒有被初始化,那就先初始化直接父類
- 當java虛拟機初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則不适用于接口。是以,一個父接口并不會因為它的子接口或者實作類的初始化而初始化。隻有當程式首次使用特定的接口的靜态變量時,才會導緻該接口的初始化。
- (3)假如類中存在初始化語句,那就依次執行這些初始化語句
Java程式對類的使用方式可分為兩種
- (1)主動使用
- (2)被動使用
所有的Java虛拟機實作必須在每個類或接口被Java程式“首次主動使用”時才能初始化他們
- 主動使用(七種)
- (1)new建立類的執行個體
- (2)通路某個類或接口的靜态變量( getstatic(助記符)),或者對該靜态變量指派 putstatic
- (3)調用類的靜态方法 invokestatic
- (4)反射
Class.forName("com.Test")
- (5)初始化一個類的子類
- (6)Java虛拟機啟動時被标明啟動類的類
- (7)JDK1.7開始提供的動态語言支援(了解)
-
被動使用
除了上面七種情況外,其他使用java類的方式都被看做是對類的被動使用,都不會導緻類的初始化。如
- 當通路一個靜态域時,隻有真正聲明這個域的類才會被初始化。如:通過子類引用父類的靜态變量,不會導緻子類初始化
- 通過數字定義類引用,不會觸發此類的初始化
- 引用常量不會觸發此類的初始化(常量在連結階段就存入調用類的常量池中了)
- 調用ClassLoader類的loadClass方法加載一個類,并不是對類的主動使用,不會導緻類的初始化。
執行順序
- 靜态代碼塊: 靜态代碼塊在類被加載的時候就運作了,而且隻運作一次,并且優先于各種代碼塊以及構造函數。如果一個類中有多個靜态代碼塊,會按照書寫順序依次執行。
- 一般情況下,如果有些代碼需要在項目啟動的時候就執行,這時候就需要靜态代碼塊。比如一個項目啟動需要加載的很多配置檔案等資源,我們就可以都放入靜态代碼塊中。
- 在類加載的時候,靜态方法也已經加載了,但是我們必須要通過類名或者對象名才能通路,也就是說相比于靜态代碼塊,靜态代碼塊是主動運作的,而靜态方法是被動運作的。
- 靜态變量要放在靜态代碼塊前
- 先初始化父類再初始化子類。先調用父類的構造函數再調用子類的構造函數
- 構造代碼塊:在java類中使用{}聲明的代碼塊(和靜态代碼塊的差別是少了static關鍵字)。**構造代碼塊在建立對象時被調用,每次建立對象都會調用一次,但是優先于構造函數執行。**構造代碼塊依托于構造函數,也就是說,如果你不執行個體化對象,構造代碼塊是不會執行的
1、父類的靜态變量和靜态塊指派(按照聲明順序)
2、自身的靜态變量和靜态塊指派(按照聲明順序)
3、main方法
3、父類的成員變量和塊指派(按照聲明順序)
4、父類構造器指派
5、自身成員變量和塊指派(按照聲明順序)
6、自身構造器指派
7、靜态方法,執行個體方法隻有在調用的時候才會去執行