<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ClassLoader知識點總結(jié)

          共 21842字,需瀏覽 44分鐘

           ·

          2021-02-23 20:33

          本文公眾號來源:我沒有三顆心臟
          作者:我沒有三顆心臟
          本文已收錄至我的GitHub

          前言

          ClassLoader 可以說是 Java 最為神秘的功能之一了,好像大家都知道怎么回事兒 (雙親委派模型好像都都能說得出來...),又都說不清楚具體是怎么一回事 (為什么需要需要有什么實際用途就很模糊了...)

          今天,我們就來深度扒一扒,揭開它神秘的面紗!

          Part 1. 類加載是做什么的?

          首先,我們知道,Java 為了實現(xiàn) 「一次編譯,到處運行」 的目標(biāo),采用了一種特別的方案:先 編譯與任何具體及其環(huán)境及操作系統(tǒng)環(huán)境無關(guān)的中間代碼(也就是 .class 字節(jié)碼文件),然后交由各個平臺特定的 Java 解釋器(也就是 JVM)來負(fù)責(zé) 解釋 運行。

          ClassLoader (顧名思義就是類加載器) 就是那個把字節(jié)碼交給 JVM 的搬運工 (加載進內(nèi)存)。它負(fù)責(zé)將 字節(jié)碼形式 的 Class 轉(zhuǎn)換成 JVM 中 內(nèi)存形式 的 Class 對象。

          字節(jié)碼可以是來自于磁盤上的 .class 文件,也可以是 jar 包里的 *.class,甚至是來自遠(yuǎn)程服務(wù)器提供的字節(jié)流。字節(jié)碼的本質(zhì)其實就是一個有特定復(fù)雜格式的字節(jié)數(shù)組 byte[] (從后面解析 ClassLoader 類中的方法時更能體會)

          另外,類加載器不光可以把 Class 加載到 JVM 之中并解析成 JVM 統(tǒng)一要求的對象格式,還有一個重要的作用就是 審查每個類應(yīng)該由誰加載

          而且,這些 Java 類不會一次全部加載到內(nèi)存,而是在應(yīng)用程序需要時加載,這也是需要類加載器的地方。

          Part 2. ClassLoader 類結(jié)構(gòu)分析

          以下就是 ClassLoader 的主要方法了:

          • defineClass() 用于將 byte 字節(jié)流解析成 JVM 能夠識別的 Class 對象。有了這個方法意味著我們不僅可以通過 .class 文件實例化對象,還可以通過其他方式實例化對象,例如通過網(wǎng)絡(luò)接收到一個類的字節(jié)碼。

            (注意,如果直接調(diào)用這個方法生成類的 Class 對象,這個類的 Class 對象還沒有 resolve,JVM 會在這個對象真正實例化時才調(diào)用 resolveClass() 進行鏈接)

          • findClass() 通常和 defineClass() 一起使用,我們需要直接覆蓋 ClassLoader 父類的 findClass() 方法來實現(xiàn)類的加載規(guī)則,從而取得要加載類的字節(jié)碼。(以下是 ClassLoader 源碼)

            protected?Class?findClass(String?name)?throws?ClassNotFoundException?{
            ??throw?new?ClassNotFoundException(name);
            }

            如果你不想重新定義加載類的規(guī)則,也沒有復(fù)雜的處理邏輯,只想在運行時能夠加載自己制定的一個類,那么你可以用 this.getClass().getClassLoader().loadClass("class") 調(diào)用 ClassLoader 的 loadClass() 方法來獲取這個類的 Class 對象,這個 loadClass() 還有重載方法,你同樣可以決定再什么時候解析這個類。

          • loadClass() 用于接受一個全類名,然后返回一個 Class 類型的對象。(該方法源碼蘊含了著名的雙親委派模型)

          • resolveClass() 用于對 Class 進行 鏈接,也就是把單一的 Class 加入到有繼承關(guān)系的類樹中。如果你想在類被加載到 JVM 中時就被鏈接(Link),那么可以在調(diào)用 defineClass() 之后緊接著調(diào)用一個 resolveClass() 方法,當(dāng)然你也可以選擇讓 JVM 來解決什么時候才鏈接這個類(通常是真正被實實例化的時候)。

          ClassLoader 是個抽象類,它還有很多子類,如果我們要實現(xiàn)自己的 ClassLoader,一般都會繼承 URLClassLoader 這個子類,因為這個類已經(jīng)幫我們實現(xiàn)了大部分工作。

          例如,我們來看一下 java.net.URLClassLoader.findClass() 方法的實現(xiàn):

          //?入?yún)?Class?的?binary?name,如?java.lang.String
          protected?Class?findClass(final?String?name)?throws?ClassNotFoundException?{
          ????//?以上代碼省略
          ??
          ????//?通過?binary?name?生成包路徑,如?java.lang.String?->?java/lang/String.class
          ????String?path?=?name.replace('.',?'/').concat(".class");
          ????//?根據(jù)包路徑,找到該?Class?的文件資源
          ????Resource?res?=?ucp.getResource(path,?false);
          ????if?(res?!=?null)?{
          ????????try?{
          ???????????//?調(diào)用?defineClass?生成?java.lang.Class?對象
          ????????????return?defineClass(name,?res);
          ????????}?catch?(IOException?e)?{
          ????????????throw?new?ClassNotFoundException(name,?e);
          ????????}
          ????}?else?{
          ????????return?null;
          ????}
          ??
          ????//?以下代碼省略
          }

          Part 3. Java 類加載流程詳解

          以下就是 ClassLoader 加載一個 class 文件到 JVM 時需要經(jīng)過的步驟。

          事實上,我們每一次在 IDEA 中點擊運行時,IDE 都會默認(rèn)替我們執(zhí)行以下的命令:

          • javac Xxxx.java ?? 找到源文件中的 public class,再找 public class 引用的其他類,Java 編譯器會根據(jù)每一個類生成一個字節(jié)碼文件;
          • java Xxxx ?? 找到文件中的唯一主類 public class,并根據(jù) public static 關(guān)鍵字找到跟主類關(guān)聯(lián)可執(zhí)行的 main 方法 (這也是為什么 main 方法需要被定義為 public static void 的原因了——我們需要在類沒有加載時訪問),開始執(zhí)行。

          在真正的運行 main 方法之前,JVM 需要 加載、鏈接 以及 初始化 上述的 Xxxx 類。

          第一步:加載(Loading)

          這一步是讀取到類文件產(chǎn)生的二進制流(findClass()),并轉(zhuǎn)換為特定的數(shù)據(jù)結(jié)構(gòu)(defineClass()),初步校驗 cafe babe 魔法數(shù) (二進制中前四個字節(jié)為 0xCAFEBABE 用來標(biāo)識該文件是 Java 文件,這是很多軟件的做法,比如 zip壓縮文件、常量池、文件長度、是否有父類等,然后在 Java 中創(chuàng)建對應(yīng)類的 java.lang.Class 實例,類中存儲的各部分信息也需要對應(yīng)放入 運行時數(shù)據(jù)區(qū) 中(例如靜態(tài)變量、類信息等放入方法區(qū))。

          以下是一個 Class 文件具有的基本結(jié)構(gòu)的簡單圖示:

          如果對 Class 文件更多細(xì)節(jié)感興趣的可以進一步閱讀:https://juejin.im/post/6844904199617003528

          這里我們可能會有一個疑問,為什么 JVM 允許還沒有進行驗證、準(zhǔn)備和解析的類信息放入方法區(qū)呢?

          答案是加載階段和鏈接階段的部分動作(比如一部分字節(jié)碼文件格式驗證動作)是 交叉進行 的,也就是說 加載階段還沒完成,鏈接階段可能已經(jīng)開始了。但這些夾雜在加載階段的動作(驗證文件格式等)仍然屬于鏈接操作。

          第二步:鏈接(Linking)

          Link 階段包括驗證、準(zhǔn)備、解析三個步驟。下面??我們來詳細(xì)說說。

          驗證:確保被加載的類的正確性

          驗證是連接階段的第一步,這一階段的目的是 為了確保 Class 文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求,并且不會危害虛擬機自身的安全。驗證階段大致會完成 4 個階段的檢驗動作:

          • 文件格式驗證: 驗證字節(jié)流是否符合 Class 文件格式的規(guī)范;例如:是否以 0xCAFEBABE 開頭、主次版本號是否在當(dāng)前虛擬機的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型。
          • 元數(shù)據(jù)驗證: 對字節(jié)碼描述的信息進行語義分析(注意:對比 javac 編譯階段的語義分析),以保證其描述的信息符合 Java 語言規(guī)范的要求;例如:這個類是否有父類,除了 java.lang.Object 之外。
          • 字節(jié)碼驗證: 通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。
          • 符號引用驗證: 確保解析動作能正確執(zhí)行。

          驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗證,那么可以考慮采用 -Xverifynone 參數(shù)來關(guān)閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

          準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值

          準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在 方法區(qū) 中分配。對于該階段有以下幾點需要注意:

          • 1?? 這時候進行內(nèi)存分配的 僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在 Java 堆中。

          • 2?? 這里所設(shè)置的 初始值通常情況下是數(shù)據(jù)類型默認(rèn)的零值(如 00Lnullfalse等),而不是被在 Java 代碼中被顯式地賦予的值。

          • 3?? 如果類字段的字段屬性表中存在 ConstantValue 屬性,即 同時被 finalstatic 修飾,那么在準(zhǔn)備階段變量 value 就會被初始化為 ConstValue 屬性所指定的值。

          ?? 例如,假設(shè)這里有一個類變量 public static int value = 666;,在準(zhǔn)備階段時初始值是 0 而不是 666,在 初始化階段 才會被真正賦值為 666

          ?? 假設(shè)是一個靜態(tài)類變量 public static final int value = 666;,則再準(zhǔn)備階段 JVM 就已經(jīng)賦值為 666 了。

          解析:把類中的符號引用轉(zhuǎn)換為直接引用(重要)

          解析階段是虛擬機將常量池內(nèi)的 符號引用 替換為 直接引用 的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符 7 類符號引用進行。

          ?? 符號引用 的作用是在編譯的過程中,JVM 并不知道引用的具體地址,所以用符號引用進行代替,而在解析階段將會將這個符號引用轉(zhuǎn)換為真正的內(nèi)存地址。

          ?? 直接引用 可以理解為指向 類、變量、方法 的指針,指向 實例 的指針和一個 間接定位 到對象的對象句柄。

          為了理解??上面兩種概念的區(qū)別,來看一個實際的例子吧:

          public?class?Tester?{

          ????public?static?void?main(String[]?args)?{
          ????????String?str?=?"關(guān)注【面試造火箭】,關(guān)注更多精彩";
          ????????System.out.println(str);
          ????}
          }

          我們先在該類同級目錄下運行 javac Tester 編譯成 .class 文件然后再利用 javap -verbose Tester 查看類的詳細(xì)信息 (為了節(jié)省篇幅只截取了 main 方法反編譯后的代碼)

          //?上面是類的詳細(xì)信息省略...
          {
          ?//?.....
          ??public?static?void?main(java.lang.String[]);
          ????descriptor:?([Ljava/lang/String;)V
          ????flags:?(0x0009)?ACC_PUBLIC,?ACC_STATIC
          ????Code:
          ??????stack=2,?locals=2,?args_size=1
          ?????????0:?ldc???????????#7??????????????????//?String?關(guān)注【面試造火箭】,關(guān)注更多精彩
          ?????????2:?astore_1
          ?????????3:?getstatic?????#9??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream;
          ?????????6:?aload_1
          ?????????7:?invokevirtual?#15?????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V
          ????????10:?return
          ??????LineNumberTable:
          ????????line?4:?0
          ????????line?5:?3
          ????????line?6:?10
          }
          SourceFile:?"Tester.java"

          可以看到,上面??定義的 str 變量在編譯階段會被解析稱為 符號引用,符號引用的標(biāo)志是 astore_,這里就是 astore_1

          store_1的含義是將操作數(shù)棧頂?shù)?關(guān)注【面試造火箭】,關(guān)注更多精彩 保存回索引為 1 的局部變量表中,此時訪問變量 str 就會讀取局部變量表索引值為 1 中的數(shù)據(jù)。所以局部變量 str 就是一個符號引用。

          再來看另外一個例子:

          public?class?Tester?{

          ????public?static?void?main(String[]?args)?{
          ????????System.out.println("關(guān)注【面試造火箭】,關(guān)注更多精彩");
          ????}
          }

          這一段代碼反編譯之后得到如下的代碼:

          //?上面是類的詳細(xì)信息省略...
          {
          ??//?......
          ??public?static?void?main(java.lang.String[]);
          ????descriptor:?([Ljava/lang/String;)V
          ????flags:?(0x0009)?ACC_PUBLIC,?ACC_STATIC
          ????Code:
          ??????stack=2,?locals=1,?args_size=1
          ?????????0:?getstatic?????#7??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream;
          ?????????3:?ldc???????????#13?????????????????//?String?關(guān)注【面試造火箭】,關(guān)注更多精彩
          ?????????5:?invokevirtual?#15?????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V
          ?????????8:?return
          ??????LineNumberTable:
          ????????line?4:?0
          ????????line?5:?8
          }
          SourceFile:?"Tester.java"

          我們可以看到這里直接使用了 ldc 指令將 關(guān)注【面試造火箭】,關(guān)注更多精彩 推送到了棧,緊接著就是調(diào)用指令 invokevirtual,并沒有將字符串存入局部變量表中,這里的字符串就是一個 直接引用

          第三步:初始化(Initialization)

          初始化,為類的靜態(tài)變量賦予正確的初始值,JVM 負(fù)責(zé)對類進行初始化,主要對類變量進行初始化。在 Java 中對類變量進行初始值設(shè)定有兩種方式:

          • 1?? 聲明類變量是指定初始值;
          • 2?? 使用靜態(tài)代碼塊為類變量指定初始值;

          JVM 初始化步驟:

          • 1?? 假如這個類還沒有被加載和連接,則程序先加載并連接該類
          • 2?? 假如該類的直接父類還沒有被初始化,則先初始化其直接父類
          • 3?? 假如類中有初始化語句,則系統(tǒng)依次執(zhí)行這些初始化語句

          類初始化時機:只有當(dāng)對類的主動使用的時候才會導(dǎo)致類的初始化,類的主動使用包括以下幾種:

          • 創(chuàng)建類的實例,也就是 new 的方式
          • 訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
          • 調(diào)用類的靜態(tài)方法
          • 反射(如 Class.forName("com.wmyskxz.Tester")
          • 初始化某個類的子類,則其父類也會被初始化
          • Java 虛擬機啟動時被標(biāo)明為啟動類的類,直接使用 java.exe 命令來運行某個主類
          • 使用 JDK 7 新加入的動態(tài)語言支持時,如果一個 java.lang.invoke.MethodHanlde 實例最后的解析結(jié)果為 REF_getstaticREF_putstaticREF_invokeStaticREF_newInvokeSpecial 四種類型的方法句柄時,都需要先初始化該句柄對應(yīng)的類
          • 接口中定義了 JDK 8 新加入的默認(rèn)方法(default修飾符),實現(xiàn)類在初始化之前需要先初始化其接口

          Part 4. 深入理解雙親委派模型

          我們在上面??已經(jīng)了解了一個類是如何被加載進 JVM 的——依靠類加載器——在 Java 語言中自帶有三個類加載器:

          • Bootstrap ClassLoader 最頂層的加載類,主要加載 ?核心類庫%JRE_HOME%\lib 下的rt.jarresources.jarcharsets.jarclass 等。
          • Extention ClassLoader 擴展的類加載器,加載目錄 %JRE_HOME%\lib\ext 目錄下的 jar 包和 class 文件。
          • Appclass Loader 也稱為 SystemAppClass 加載當(dāng)前應(yīng)用的 classpath 的所有類。

          我們可以通過一個簡單的例子來簡單了解 Java 中這些自帶的類加載器:

          public?class?PrintClassLoader?{

          ????public?static?void?main(String[]?args)?{
          ????????printClassLoaders();
          ????}

          ????public?static?void?printClassLoaders()?{
          ????????System.out.println("Classloader?of?this?class:"
          ????????????+?PrintClassLoader.class.getClassLoader());
          ????????System.out.println("Classloader?of?Logging:"
          ????????????+?com.sun.javafx.util.Logging.class.getClassLoader());
          ????????System.out.println("Classloader?of?ArrayList:"
          ????????????+?java.util.ArrayList.class.getClassLoader());
          ????}
          }

          上方程序打印輸出如下:

          Classloader?of?this?class:sun.misc.Launcher$AppClassLoader@18b4aac2
          Classloader?of?Logging:sun.misc.Launcher$ExtClassLoader@60e53b93
          Classloader?of?ArrayList:null

          如我們所見,這里分別對應(yīng)三種不同類型的類加載器:AppClassLoader、ExtClassLoader 和 BootstrapClassLoader(顯示為 null)。

          一個很好的問題是:Java 類是由 java.lang.ClassLoader 實例加載的,但類加載器本身也是類,那么誰來加載類加載器呢?

          我們假裝不知道,先來跟著源碼一步一步來看。

          先來看看 Java 虛擬機入口代碼

          在 JDK 源碼 sun.misc.Launcher 中,蘊含了 Java 虛擬機的入口方法:

          public?class?Launcher?{
          ????private?static?Launcher?launcher?=?new?Launcher();
          ????private?static?String?bootClassPath?=
          ????????System.getProperty("sun.boot.class.path");

          ????public?static?Launcher?getLauncher()?{
          ????????return?launcher;
          ????}

          ????private?ClassLoader?loader;

          ????public?Launcher()?{
          ????????//?Create?the?extension?class?loader
          ????????ClassLoader?extcl;
          ????????try?{
          ????????????extcl?=?ExtClassLoader.getExtClassLoader();
          ????????}?catch?(IOException?e)?{
          ????????????throw?new?InternalError(
          ????????????????"Could?not?create?extension?class?loader",?e);
          ????????}

          ????????//?Now?create?the?class?loader?to?use?to?launch?the?application
          ????????try?{
          ????????????loader?=?AppClassLoader.getAppClassLoader(extcl);
          ????????}?catch?(IOException?e)?{
          ????????????throw?new?InternalError(
          ????????????????"Could?not?create?application?class?loader",?e);
          ????????}

          ????????//?設(shè)置?AppClassLoader?為線程上下文類加載器,這個文章后面部分講解
          ????????Thread.currentThread().setContextClassLoader(loader);
          ????}
          ????/*
          ?????*?Returns?the?class?loader?used?to?launch?the?main?application.
          ?????*/

          ????public?ClassLoader?getClassLoader()?{
          ????????return?loader;
          ????}
          ????/*
          ?????*?The?class?loader?used?for?loading?installed?extensions.
          ?????*/

          ????static?class?ExtClassLoader?extends?URLClassLoader?{}
          ??/**
          ?????*?The?class?loader?used?for?loading?from?java.class.path.
          ?????*?runs?in?a?restricted?security?context.
          ?????*/

          ????static?class?AppClassLoader?extends?URLClassLoader?{}
          }

          源碼有精簡,但是我們可以得到以下信息:

          1?? Launcher 初始化了 ExtClassLoader 和 AppClassLoader。

          2?? Launcher 沒有看到 Bootstrap ClassLoader 的影子,但是有一個叫做 bootClassPath 的變量,大膽一猜就是 Bootstrap ClassLoader 加載的 jar 包的路徑。

          (ps: 可以自己嘗試輸出一下 System.getProperty("sun.boot.class.path") 的內(nèi)容,它正好對應(yīng)了 JDK 目錄 libclasses 目錄下的 jar 包——也就是通常你配置環(huán)境變量時設(shè)置的 %JAVA_HOME/lib 的目錄了——同樣的方式你也可以看看 Ext 和 App 的源碼)

          3?? ExtClassLoader 和 AppClassLoader 都繼承自 URLClassLoader,進一步查看 ClassLoader 的繼承樹,傳說中的雙親委派模型也并沒有出現(xiàn)。(甚至看不到 Bootstrap ClassLoader 的影子,Ext 也沒有直接繼承自 App 類加載器)

          ClassLoader 繼承樹

          (??注意,這里可以明確看到每一個 ClassLoader 都有一個 parent 變量,用于標(biāo)識自己的父類,下面??詳細(xì)說)

          4?? 注意以下代碼:

          ClassLoader?extcl;
          ????????
          extcl?=?ExtClassLoader.getExtClassLoader();

          loader?=?AppClassLoader.getAppClassLoader(extcl);

          分別跟蹤查看到這兩個 ClassLoader 初始化時的代碼:

          //?一直追蹤到最頂層的?ClassLoader?定義,構(gòu)造器的第二個參數(shù)標(biāo)識了類加載器的父類
          private?ClassLoader(Void?unused,?ClassLoader?parent)?{
          ??this.parent?=?parent;
          ??//?代碼省略.....
          }
          //?Ext?設(shè)置自己的父類為?null
          public?ExtClassLoader(File[]?var1)?throws?IOException?{
          ??super(getExtURLs(var1),?(ClassLoader)null,?Launcher.factory);
          ??SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
          }
          //?手動把?Ext?設(shè)置為?App?的?parent(這里的?var2?是傳進來的?extc1)
          AppClassLoader(URL[]?var1,?ClassLoader?var2)?{
          ??super(var1,?var2,?Launcher.factory);
          ??this.ucp.initLookupCache(this);
          }

          由此,我們得到了這樣一個類加載器的關(guān)系圖:

          類加載器的父類都來自哪里?

          奇怪,為什么 ExtClassLoader 的 parent 明明是 null,我們卻一般地認(rèn)為 Bootstrap ClassLoader 才是 ExtClassLoader 的父加載器呢?

          答案的一部分就藏在 java.lang.ClassLoader.loadClass() 方法里面:(這也就是著名的「雙親委派模型」現(xiàn)場了)

          protected?Class?loadClass(String?name,?boolean?resolve)
          ??throws?ClassNotFoundException
          {
          ??synchronized?(getClassLoadingLock(name))?{
          ????//?首先檢查是否已經(jīng)加載過了
          ????Class?c?=?findLoadedClass(name);
          ????if?(c?==?null)?{
          ??????long?t0?=?System.nanoTime();
          ??????try?{
          ????????if?(parent?!=?null)?{
          ?????//?父加載器不為空則調(diào)用父加載器的?loadClass?方法
          ??????????c?=?parent.loadClass(name,?false);
          ????????}?else?{
          ??????????//?父加載器為空則調(diào)用?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();
          ????????//?父加載器沒有找到,則調(diào)用?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)?{
          ??????//?調(diào)用?resolveClass()
          ??????resolveClass(c);
          ????}
          ????return?c;
          ??}
          }

          代碼邏輯很好地解釋了雙親委派的原理。

          1?? 當(dāng)前 ClassLoader 首先從 自己已經(jīng)加載的類中查詢是否此類已經(jīng)加載,如果已經(jīng)加載則直接返回原來已經(jīng)加載的類。(每個類加載器都有自己的加載緩存,當(dāng)一個類被加載了以后就會放入緩存,等下次加載的時候就可以直接返回了。)

          2?? 當(dāng)前 ClassLoader 的緩存中沒有找到被加載的類的時候,委托父類加載器去加載,父類加載器采用同樣的策略,首先查看自己的緩存,然后委托父類的父類去加載,一直到 Bootstrap ClassLoader。(當(dāng)所有的父類加載器都沒有加載的時候,再由當(dāng)前的類加載器加載,并將其放入它自己的緩存中,以便下次有加載請求的時候直接返回。)

          所以,答案的另一部分是因為最高一層的類加載器 Bootstrap 是通過 C/C++ 實現(xiàn)的,并不存在于 JVM 體系內(nèi) (不是一個 Java 類,沒辦法直接表示為 ExtClassLoader 的父加載器),所以輸出為 null

          (我們可以很輕易跟蹤到 findBootstrapClass() 方法被 native 修飾:private native Class findBootstrapClass(String name);

          ?? OK,我們理解了為什么 ExtClassLoader 的父加載器為什么是表示為 null 的 Bootstrap 加載器,那我們 自己實現(xiàn)的 ClassLoader 父加載器應(yīng)該是誰呢?

          觀察一下 ClassLoader 的源碼就知道了:

          protected?ClassLoader(ClassLoader?parent)?{
          ????this(checkCreateClassLoader(),?parent);
          }
          protected?ClassLoader()?{
          ????this(checkCreateClassLoader(),?getSystemClassLoader());
          }

          類加載器的 parent 的賦值是在 ClassLoader 對象的構(gòu)造方法中,它有兩個情況:

          1?? 由外部類創(chuàng)建 ClassLoader 時直接指定一個 ClassLoader 為 parent

          2?? 由 getSystemClassLoader() 方法生成,也就是在 sun.misc.Laucher 通過 getClassLoader() 獲取,也就是 AppClassLoader。直白的說,一個 ClassLoader 創(chuàng)建時如果沒有指定 parent,那么它的 parent 默認(rèn)就是 AppClassLoader。(建議去看一下源碼)

          為什么這樣設(shè)計呢?

          簡單來說,主要是為了 安全性,避免用戶自己編寫的類動態(tài)替換 Java 的一些核心類,比如 String,同時也 避免了重復(fù)加載,因為 JVM 中區(qū)分不同類,不僅僅是根據(jù)類名,相同的 class 文件被不同的 ClassLoader 加載就是不同的兩個類,如果相互轉(zhuǎn)型的話會拋 java.lang.ClassCaseException

          如果我們要實現(xiàn)自己的類加載器,不管你是直接實現(xiàn)抽象類 ClassLoader,還是繼承 URLClassLoader 類,或者其他子類,它的父加載器都是 AppClassLoader。

          因為不管調(diào)用哪個父類構(gòu)造器,創(chuàng)建的對象都必須最終調(diào)用 getSystemClassLoader() 作為父加載器 (我們已經(jīng)從上面??的源碼中看到了)。而該方法最終獲取到的正是 AppClassLoader (別稱 SystemClassLoader)

          這也就是我們熟知的最終的雙親委派模型了。

          Part 5. 實現(xiàn)自己的類加載器

          什么情況下需要自定義類加載器

          在學(xué)習(xí)了類加載器的實現(xiàn)機制之后,我們知道了雙親委派模型并非強制模型,用戶可以自定義類加載器,在什么情況下需要自定義類加載器呢?

          1?? 隔離加載類。在某些框架內(nèi)進行中間件與應(yīng)用的模塊隔離,把類加載器到不同的環(huán)境。比如,阿里內(nèi)某容器框架通過自定義類加載器確保應(yīng)用中依賴的 jar 包不會影響到中間件運行時使用的 jar 包。

          2?? 修改類加載方式。類的加載模型并非強制,除了 Bootstrap 外,其他的加載并非一定要引入,或者根據(jù)實際情況在某個時間點進行按需的動態(tài)加載。

          3?? 擴展加載源。比如從數(shù)據(jù)庫、網(wǎng)絡(luò),甚至是電視機頂盒進行加載。(下面??我們會編寫一個從網(wǎng)絡(luò)加載類的例子)

          4?? 防止源碼泄露。Java 代碼容易被編譯和篡改,可以進行編譯加密。那么類加載器也需要自定義,還原加密的字節(jié)碼。

          一個常規(guī)的例子

          實現(xiàn)一個自定義的類加載器比較簡單:繼承 ClassLoader,重寫 findClass() 方法,調(diào)用 defineClass() 方法,就差不多行了。

          Tester.java

          我們先來編寫一個測試用的類文件:

          public?class?Tester?{

          ????public?void?say()?{
          ????????System.out.println("關(guān)注【面試造火箭】,解鎖更多精彩!");
          ????}
          }

          在同級目錄下執(zhí)行 javac Tester.java 命令,并把編譯后的 Tester.class 放到指定的目錄下(我這邊為了方便就放在桌面上啦 /Users/wmyskxz/Desktop

          MyClassLoader.java

          我們編寫自定義 ClassLoader 代碼:

          import?java.io.ByteArrayOutputStream;
          import?java.io.File;
          import?java.io.FileInputStream;
          import?java.io.IOException;

          public?class?MyClassLoader?extends?ClassLoader?{

          ????private?final?String?mLibPath;

          ????public?MyClassLoader(String?path)?{
          ????????//?TODO?Auto-generated?constructor?stub
          ????????mLibPath?=?path;
          ????}

          ????@Override
          ????protected?Class?findClass(String?name)?throws?ClassNotFoundException?{
          ????????//?TODO?Auto-generated?method?stub

          ????????String?fileName?=?getFileName(name);

          ????????File?file?=?new?File(mLibPath,?fileName);

          ????????try?{
          ????????????FileInputStream?is?=?new?FileInputStream(file);

          ????????????ByteArrayOutputStream?bos?=?new?ByteArrayOutputStream();
          ????????????int?len?=?0;
          ????????????try?{
          ????????????????while?((len?=?is.read())?!=?-1)?{
          ????????????????????bos.write(len);
          ????????????????}
          ????????????}?catch?(IOException?e)?{
          ????????????????e.printStackTrace();
          ????????????}

          ????????????byte[]?data?=?bos.toByteArray();
          ????????????is.close();
          ????????????bos.close();

          ????????????return?defineClass(name,?data,?0,?data.length);

          ????????}?catch?(IOException?e)?{
          ????????????//?TODO?Auto-generated?catch?block
          ????????????e.printStackTrace();
          ????????}

          ????????return?super.findClass(name);
          ????}

          ????//?獲取要加載的?class?文件名
          ????private?String?getFileName(String?name)?{
          ????????//?TODO?Auto-generated?method?stub
          ????????int?index?=?name.lastIndexOf('.');
          ????????if?(index?==?-1)?{
          ????????????return?name?+?".class";
          ????????}?else?{
          ????????????return?name.substring(index?+?1)?+?".class";
          ????????}
          ????}
          }

          我們在 findClass() 方法中定義了查找 class 的方法,然后數(shù)據(jù)通過 defineClass() 生成了 Class 對象。

          ClassLoaderTester 測試類

          我們需要刪除剛才在項目目錄創(chuàng)建的 Tester.java 和編譯后的 Tester.class 文件來觀察效果:

          import?java.lang.reflect.InvocationTargetException;
          import?java.lang.reflect.Method;

          public?class?ClassLoaderTester?{

          ????public?static?void?main(String[]?args)?{
          ????????//?創(chuàng)建自定義的?ClassLoader?對象
          ????????MyClassLoader?myClassLoader?=?new?MyClassLoader("/Users/wmyskxz/Desktop");
          ????????try?{
          ????????????//?加載class文件
          ????????????Class?c?=?myClassLoader.loadClass("Tester");

          ????????????if(c?!=?null){
          ????????????????try?{
          ????????????????????Object?obj?=?c.newInstance();
          ????????????????????Method?method?=?c.getDeclaredMethod("say",null);
          ????????????????????//通過反射調(diào)用Test類的say方法
          ????????????????????method.invoke(obj,?null);
          ????????????????}?catch?(InstantiationException?|?IllegalAccessException
          ????????????????????|?NoSuchMethodException
          ????????????????????|?SecurityException?|
          ????????????????????IllegalArgumentException?|
          ????????????????????InvocationTargetException?e)?{
          ????????????????????//?TODO?Auto-generated?catch?block
          ????????????????????e.printStackTrace();
          ????????????????}
          ????????????}
          ????????}?catch?(ClassNotFoundException?e)?{
          ????????????//?TODO?Auto-generated?catch?block
          ????????????e.printStackTrace();
          ????????}
          ????}
          }

          運行測試,正常輸出:

          關(guān)注【面試造火箭】,解鎖更多精彩!

          加密解密類加載器

          突破了 JDK 系統(tǒng)內(nèi)置加載路徑的限制之后,我們就可以編寫自定義的 ClassLoader。你完全可以按照自己的意愿進行業(yè)務(wù)的定制,將 ClassLoader 玩出花樣來。

          例如,一個加密解密的類加載器。(不涉及完整代碼,我們可以來說一下思路和關(guān)鍵代碼)

          首先,在編譯之后的字節(jié)碼文件中動一動手腳,例如,給文件每一個 byte 異或一個數(shù)字 2:(這就算是模擬加密過程)

          File?file?=?new?File(path);
          try?{
          ??FileInputStream?fis?=?new?FileInputStream(file);
          ??FileOutputStream?fos?=?new?FileOutputStream(path+"en");
          ??int?b?=?0;
          ??int?b1?=?0;
          ??try?{
          ????while((b?=?fis.read())?!=?-1){
          ??????//?每一個?byte?異或一個數(shù)字?2
          ??????fos.write(b?^?2);
          ????}
          ????fos.close();
          ????fis.close();
          ??}?catch?(IOException?e)?{
          ????//?TODO?Auto-generated?catch?block
          ????e.printStackTrace();
          ??}
          }?catch?(FileNotFoundException?e)?{
          ??//?TODO?Auto-generated?catch?block
          ??e.printStackTrace();
          }

          然后我們再在 findClass() 中自己解密:

          File?file?=?new?File(mLibPath,fileName);

          try?{
          ??FileInputStream?is?=?new?FileInputStream(file);

          ??ByteArrayOutputStream?bos?=?new?ByteArrayOutputStream();
          ??int?len?=?0;
          ??byte?b?=?0;
          ??try?{
          ????while?((len?=?is.read())?!=?-1)?{
          ??????//?將數(shù)據(jù)異或一個數(shù)字?2?進行解密
          ??????b?=?(byte)?(len?^?2);
          ??????bos.write(b);
          ????}
          ??}?catch?(IOException?e)?{
          ????e.printStackTrace();
          ??}

          ??byte[]?data?=?bos.toByteArray();
          ??is.close();
          ??bos.close();

          ??return?defineClass(name,data,0,data.length);

          }?catch?(IOException?e)?{
          ??//?TODO?Auto-generated?catch?block
          ??e.printStackTrace();
          }

          (代碼幾乎與上面??一個例子等同,所以只說一下思路和完整代碼)

          網(wǎng)絡(luò)類加載器

          其實非常類似,也不做過多講解,直接上代碼:

          import?java.io.ByteArrayOutputStream;??
          import?java.io.InputStream;??
          import?java.net.URL;??
          ??
          public?class?NetworkClassLoader?extends?ClassLoader?{??
          ??
          ????private?String?rootUrl;??
          ??
          ????public?NetworkClassLoader(String?rootUrl)?{??
          ????????//?指定URL??
          ????????this.rootUrl?=?rootUrl;??
          ????}??
          ??
          ????//?獲取類的字節(jié)碼??
          ????@Override??
          ????protected?Class?findClass(String?name)?throws?ClassNotFoundException?{??
          ????????byte[]?classData?=?getClassData(name);??
          ????????if?(classData?==?null)?{??
          ????????????throw?new?ClassNotFoundException();??
          ????????}?else?{??
          ????????????return?defineClass(name,?classData,?0,?classData.length);??
          ????????}??
          ????}??
          ??
          ????private?byte[]?getClassData(String?className)?{??
          ????????//?從網(wǎng)絡(luò)上讀取的類的字節(jié)??
          ????????String?path?=?classNameToPath(className);??
          ????????try?{??
          ????????????URL?url?=?new?URL(path);??
          ????????????InputStream?ins?=?url.openStream();??
          ????????????ByteArrayOutputStream?baos?=?new?ByteArrayOutputStream();??
          ????????????int?bufferSize?=?4096;??
          ????????????byte[]?buffer?=?new?byte[bufferSize];??
          ????????????int?bytesNumRead?=?0;??
          ????????????//?讀取類文件的字節(jié)??
          ????????????while?((bytesNumRead?=?ins.read(buffer))?!=?-1)?{??
          ????????????????baos.write(buffer,?0,?bytesNumRead);??
          ????????????}??
          ????????????return?baos.toByteArray();??
          ????????}?catch?(Exception?e)?{??
          ????????????e.printStackTrace();??
          ????????}??
          ????????return?null;??
          ????}??
          ??
          ????private?String?classNameToPath(String?className)?{??
          ????????//?得到類文件的URL??
          ????????return?rootUrl?+?"/"??
          ????????????????+?className.replace('.',?'/')?+?".class";??
          ????}??
          }??

          (代碼來自:https://blog.csdn.net/justloveyou_/article/details/72217806)

          Part 6. 必要的擴展閱讀

          學(xué)習(xí)到這里,我們對 ClassLoader 已經(jīng)不再陌生了,但是仍然有一些必要的知識點需要去掌握 (限于篇幅和能力這里不擴展了),希望您能認(rèn)真閱讀以下的材料:(可能排版上面層次不齊,但內(nèi)容都是有質(zhì)量的,并用 ?? 標(biāo)注了更加重點一些的內(nèi)容)

          1?? ??能不能自己寫一個類叫 java.lang.System 或者 java.lang.String - https://blog.csdn.net/tang9140/article/details/42738433

          2?? 深入理解 Java 之 JVM 啟動流程 - https://cloud.tencent.com/developer/article/1038435

          3?? ??真正理解線程上下文類加載器(多案例分析) - https://blog.csdn.net/yangcheng33/article/details/52631940

          4?? ??曹工雜談:Java 類加載器還會死鎖?這是什么情況? - https://www.cnblogs.com/grey-wolf/p/11378747.html#_label2

          5?? 謹(jǐn)防JDK8重復(fù)類定義造成的內(nèi)存泄漏 - https://segmentfault.com/a/1190000022837543

          7?? ??Tomcat 類加載器的實現(xiàn) - https://juejin.im/post/6844903945496690695

          8?? ??Spring 中的類加載機制 - https://www.shuzhiduo.com/A/gVdnwgAlzW/

          參考資料

          1. 《深入分析 Java Web 技術(shù)內(nèi)幕》 | 許令波 著
          2. Java 類加載機制分析 - https://www.jianshu.com/p/3615403c7c84
          3. Class 文件解析實戰(zhàn) - https://juejin.im/post/6844904199617003528
          4. 圖文兼?zhèn)淇炊惣虞d機制的各個階段,就差你了!- https://juejin.im/post/6844904119258316814
          5. Java面試知識點解析(三)——JVM篇 - https://www.wmyskxz.com/2018/05/16/java-mian-shi-zhi-shi-dian-jie-xi-san-jvm-pian/
          6. 一看你就懂,超詳細(xì)Java中的ClassLoader詳解 - https://blog.csdn.net/briblue/article/details/54973413

          對線面試官》系列目前已經(jīng)連載15篇啦!進度是一周更新兩篇,歡迎持續(xù)關(guān)注

          - END -
          怎樣偷偷努力 驚艷所有人?

          點擊小卡片關(guān)注【面試造火箭

          關(guān)注后回復(fù)「888」還可獲取【面試資料】網(wǎng)盤地址喲!

          瀏覽 65
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久操AV在线播放 | 精品国产天线2024 | 综合久久久 | 亚洲 在线观看 | 黄片无码在线 |