1. 程式人生 > 實用技巧 >面試BAT必問的JVM,今天我們來說一說它類載入器的底層原理

面試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!文章都會在裡面更新,整理的資料也會放在裡面。謝謝你的觀看,覺得文章對你有幫助的話記得關注我點個贊支援一下!