面試BAT必問的JVM,今天我們來說一說它類載入器的底層原理
類載入器的關係
類載入器的分類
- JVM支援兩種類載入器,一種為引導類載入器(Bootstrap ClassLoader),另外一種是自定義類載入器(User Defined ClassLoader)
- 引導類載入器是由C/C++編寫的無法訪問到
- Java虛擬機器規定:所有派生於抽象類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@1540e19d //獲取其上層:獲取不到引導類載入器 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 } }
系統自帶的類載入器介紹
-
啟動類載入器(引導類載入器、Bootstrap ClassLoader)
- 由c/c++語言實現的,巢狀在jvm內部
- 用來載入java核心庫
- 並不繼承java.lang.ClassLoader,沒有父載入器
- 為擴充套件類載入器和系統類載入器的父載入器
- 只能載入java、javax、sun開頭的類
-
擴充套件類載入器(Extension ClassLoader)
- java語言編寫,sun.misc.Launche包下。
- 派生於ClassLoader類,父類載入器為Bootstrap ClassLoader
- 從java.ext.dirs系統屬性指定的目錄中載入類庫或者載入jre/lib/ext子目錄下的類庫(使用者可以在該目錄下編寫JAR,也會由此載入器所載入)
-
系統類載入器(System ClassLoader\AppClassLoader)
- 派生於ClassLoader,父類載入器為Extension ClassLoader
- 負責載入classpath或者系統屬性java.class.path指定路徑下的類庫
- java語言編寫,sun.misc.Launche包下。
- 負責載入程式中預設的類,可以通過getSystemClassLoader()方法獲取該類的載入器。
-
使用者自定義類載入器(後面詳細介紹)
- 隔離載入類
- 修改類載入的方式
- 擴充套件載入源
- 防止原始碼洩漏(可以對位元組碼檔案加密)
- 繼承ClassLoader類方式實現自定義類載入器
關於ClassLoader
-
ClassLoader是一個抽象類,其後的所有類載入器都繼承此類
注:這些方法都不是抽象方法。
獲取ClassLoader的路徑
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
雙親委派機制(面試)
-
Java虛擬機器對class檔案採用的是按需載入的方式,當需要使用到這個類的時候才會對它的class檔案載入到記憶體生成class物件,載入的過程中使用的雙親委派模式,即把請求交給父類處理。
-
如果一個類載入器收到了類載入的請求,它不會自己載入,而是先把這個請求給自己的父類載入器去執行
-
如果這個父類載入器還有父類載入器,則會再將請求給自己的父類載入器,依次遞迴到頂層的啟動類載入器
-
依次進行判斷是否能完成委派(載入此類),若能完成委派則該類就由此載入器載入,若無法完成委派,則將委託給子類載入器進行判斷是否能完成委派,依次遞迴到底層載入器,若期間被載入則完成載入階段不會再遞迴(注)。
注:類只能被一個載入器所載入。
-
雙親委派的優勢
-
避免類的重複載入
-
保護程式的安全,防止核心API被篡改
-
例如:
-
建立一個java.lang.String類,因為有雙親委派的機制,所以會將String類交給引導類載入器來判斷是否能被載入。引導載入器判斷可以載入此類(是核心類中的String),完成載入,則"惡意"寫的String類無法生效,防止String類被惡意篡改。這裡也稱沙箱安全機制(保護java核心原始碼)
package java.lang;
public class String {
//
static{
System.out.println("我是自定義的String類的靜態程式碼塊");
}
//錯誤: 在類 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
//因為載入的是核心類的String,在String中找不到main方法
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();//無輸出
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
package java.lang;
public class ShkStart {
//錯誤:java.lang.SecurityException: Prohibited package name: java.lang
public static void main(String[] args) {
System.out.println("hello!");
}
}
//因為java.lang包由引導類載入器載入,引導類中並沒有此類,為了安全引導類
破壞雙親委派模型:
較大規模的破壞雙親委派模型的有3種:
-
由於雙親委派模型是在JDK1.2之後才引入的,所以在JDK1.2之前是不符合雙親委派模型的:
-
ClassLoader這類在JDK1.0開始有存在的,在JDK1.2之前,ClassLoader中是通過私有方法loadClassInternal()去呼叫自己內部的loadClass()。為了滿足雙親委派以及向下相容,在JDK1.2後的ClassLoader類中,又為該類添加了protected的findClass()方法,JDK1.2之後就不推薦通過覆蓋重寫loadClass()方法了,而是在新新增的findClass()方法中書寫自己的類載入邏輯,若loadClass()方法中的父類載入失敗則會呼叫自己的findClass()方法。
-
由於雙親委派模型的旨意是越核心的類越由高層的載入器所載入(上文提到過的String類),倘若這些核心類要去呼叫使用者的基礎類,例如JNDI服務(是對 資源進行集中管理和查詢,它需要呼叫由獨立廠商實現並部署在應用程式的ClassPath下的JNDI介面提供者(SPI,Service Provider Interface)的程式碼,但啟動類載入器不可能"認識"這些程式碼)
-
為了解決呼叫問題,設計了一個執行緒上下文類載入器(Thread Context ClassLoader),這個類載入器可以通過java.lang.Thread類的setContextClassLoaser()方法進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過的話,那這個類載入器預設就是應用程式類載入器。
-
有了這個上下文類載入器,就可以去載入所需要的SPI程式碼,實際上就是從父類載入器去請求子類載入器去完成類的載入驅動,違背了雙親委派的一般性規則。Java中所有涉及SPI的載入動作基本上都採用這種方式,例如JNDI、JDBC、JCE、JAXB和JBI等。
-
為了滿足"熱部署"、"動態部署"等功能而導致的。在OSGi(動態模組技術)環境下,類載入器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加複雜的網狀結構,當收到類載入請求時,OSGi將按照順序進行類搜尋
關於類載入器的一些補充
1. JVM中判斷一個類是否是同一個類有兩個必要條件:
- 這兩個類的全限定名要一致
- 這兩個類被同一個類載入器載入。
2. 對類載入器的引用:
- JVM必須知道一個型別是由引導類載入器(啟動類載入器)載入的還是由使用者類載入器載入的。
- 如果一個類是由使用者類載入器所載入的,那麼JVM會將這個類載入器的一個引用作為類資訊的一部分儲存在方法區。
- 當解析一個型別到另外一個型別的引用的時候,JVM需要保證這兩個型別的類載入器是相同的。
3. 類的主動使用和被動使用
- 主動使用:
- 建立類的例項
- 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
- 呼叫類的靜態方法
- 反射
* 初始化一個類的子類
* JVM啟動時被標明為啟動類的類 - JDK 7 開始提供的動態代理:java.invoke.MethodHandle例項的解析結果,REF_getStatic、REF_putStatic、REF_invokeStatic控制代碼對應的類沒有初始化、則初始化
* 除以上7種情況,其他使用Java類的方式都為被動使用,被動使用不會導致類的初始化。
最後
大家看完有什麼不懂的可以在下方留言討論,也可以關注我私信問我,我看到後都會回答的。也歡迎大家關注我的公眾號:前程有光,馬上金九銀十跳槽面試季,整理了1000多道將近500多頁pdf文件的Java面試題資料放在裡面,助你圓夢BAT!文章都會在裡面更新,整理的資料也會放在裡面。謝謝你的觀看,覺得文章對你有幫助的話記得關注我點個贊支援一下!