<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                本課時我們主要從覆蓋 JDK 的類開始講解 JVM 的類加載機制。其實,JVM 的類加載機制和 Java 的類加載機制類似,但 JVM 的類加載過程稍有些復雜。 前面課時我們講到,JVM 通過加載 .class 文件,能夠將其中的字節碼解析成操作系統機器碼。那這些文件是怎么加載進來的呢?又有哪些約定?接下來我們就詳細介紹 JVM 的類加載機制,同時介紹三個實際的應用場景。 我們首先看幾個面試題。 * 我們能夠通過一定的手段,覆蓋 HashMap 類的實現么? * 有哪些地方打破了 Java 的類加載機制? * 如何加載一個遠程的 .class 文件?怎樣加密 .class 文件? 關于類加載,很多同學都知道雙親委派機制,但這明顯不夠。面試官可能要你講出幾個能打破這個機制的例子,這個時候不要慌。上面幾個問題,是我在接觸的一些比較高級的面試場景中,遇到的一些問法。在平常的工作中,也有大量的相關應用,我們會理論聯系實踐綜合分析這些問題。 ### 類加載過程 現實中并不是說,我把一個文件修改成 .class 后綴,就能夠被 JVM 識別。類的加載過程非常復雜,主要有這幾個過程:加載、驗證、準備、解析、初始化。這些術語很多地方都出現過,我們不需要死記硬背,而應該要了解它背后的原理和要做的事情。 ![](https://img.kancloud.cn/12/ca/12ca41c3656bf3dfdba7ee2660dd43ac_630x541.png) 如圖所示。大多數情況下,類會按照圖中給出的順序進行加載。下面我們就來分別介紹下這個過程。 #### 加載 加載的主要作用是將外部的 .class 文件,加載到 Java 的方法區內,你可以回顧一下我們在上一課時講的內存區域圖。加載階段主要是找到并加載類的二進制數據,比如從 jar 包里或者 war 包里找到它們。 #### 驗證 肯定不能任何 .class 文件都能加載,那樣太不安全了,容易受到惡意代碼的攻擊。驗證階段在虛擬機整個類加載過程中占了很大一部分,不符合規范的將拋出 java.lang.VerifyError 錯誤。像一些低版本的 JVM,是無法加載一些高版本的類庫的,就是在這個階段完成的。 #### 準備 從這部分開始,將為一些類變量分配內存,并將其初始化為默認值。此時,實例對象還沒有分配內存,所以這些動作是在方法區上進行的。 我們順便看一道面試題。下面兩段代碼,code-snippet 1 將會輸出 0,而 code-snippet 2 將無法通過編譯。 ``` code-snippet 1: public class A { static int a ; public static void main(String[] args) { System.out.println(a); } } code-snippet 2: public class A { public static void main(String[] args) { int a ; System.out.println(a); } } ``` * [ ] 為什么會有這種區別呢? 這是因為局部變量不像類變量那樣存在準備階段。類變量有兩次賦初始值的過程,一次在準備階段,賦予初始值(也可以是指定值);另外一次在初始化階段,賦予程序員定義的值。 因此,即使程序員沒有為類變量賦值也沒有關系,它仍然有一個默認的初始值。但局部變量就不一樣了,如果沒有給它賦初始值,是不能使用的。 #### 解析 解析在類加載中是非常非常重要的一環,是將符號引用替換為直接引用的過程。這句話非常的拗口,其實理解起來也非常的簡單。 符號引用是一種定義,可以是任何字面上的含義,而直接引用就是直接指向目標的指針、相對偏移量。 直接引用的對象都存在于內存中,你可以把通訊錄里的女友手機號碼,類比為符號引用,把面對面和你吃飯的人,類比為直接引用。 解析階段負責把整個類激活,串成一個可以找到彼此的網,過程不可謂不重要。那這個階段都做了哪些工作呢?大體可以分為: * 類或接口的解析 * 類方法解析 * 接口方法解析 * 字段解析 我們來看幾個經常發生的異常,就與這個階段有關。 * java.lang.NoSuchFieldError 根據繼承關系從下往上,找不到相關字段時的報錯。 * java.lang.IllegalAccessError 字段或者方法,訪問權限不具備時的錯誤。 * java.lang.NoSuchMethodError 找不到相關方法時的錯誤。 解析過程保證了相互引用的完整性,把繼承與組合推進到運行時。 #### 初始化 如果前面的流程一切順利的話,接下來該初始化成員變量了,到了這一步,才真正開始執行一些字節碼。 接下來是另一道面試題,你可以猜想一下,下面的代碼,會輸出什么? ``` public class A { static int a = 0 ; static { a = 1; b = 1; } static int b = 0; public static void main(String[] args) { System.out.println(a); System.out.println(b); } } ``` 結果是 `1 0`。a 和 b 唯一的區別就是它們的 static 代碼塊的位置。 這就引出一個規則:static 語句塊,只能訪問到定義在 static 語句塊之前的變量。所以下面的代碼是無法通過編譯的。 ``` static { b = b + 1; } static int b = 0; ``` 我們再來看第二個規則:JVM 會保證在子類的初始化方法執行之前,父類的初始化方法已經執行完畢。 所以,JVM 第一個被執行的類初始化方法一定是 java.lang.Object。另外,也意味著父類中定義的 static 語句塊要優先于子類的。 `<cinit>與<init>` 說到這里,不得不再說一個面試題:<cinit> 方法和 <init> 方法有什么區別? 主要是為了讓你弄明白類的初始化和對象的初始化之間的差別。 ``` public class A { static { System.out.println("1"); } public A(){ System.out.println("2"); } } public class B extends A { static{ System.out.println("a"); } public B(){ System.out.println("b"); } public static void main(String[] args){ A ab = new B(); ab = new B(); } } ``` 先公布下答案: ``` 1 a 2 b 2 b ``` 你可以看下這張圖。其中 static 字段和 static 代碼塊,是屬于類的,在類的加載的初始化階段就已經被執行。類信息會被存放在方法區,在同一個類加載器下,這些信息有一份就夠了,所以上面的 static 代碼塊只會執行一次,它對應的是 `<cinit>` 方法。 而對象初始化就不一樣了。通常,我們在 new 一個新對象的時候,都會調用它的構造方法,就是 `<init>`,用來初始化對象的屬性。每次新建對象的時候,都會執行。 ![](https://img.kancloud.cn/90/a3/90a3b34d3c90b18b570080b66ccdda12_757x567.png) 所以,上面代碼的 static 代碼塊只會執行一次,對象的構造方法執行兩次。再加上繼承關系的先后原則,不難分析出正確結果。 ### 類加載器 整個類加載過程任務非常繁重,雖然這活兒很累,但總得有人干。類加載器做的就是上面 5 個步驟的事。 如果你在項目代碼里,寫一個 java.lang 的包,然后改寫 String 類的一些行為,編譯后,發現并不能生效。JRE 的類當然不能輕易被覆蓋,否則會被別有用心的人利用,這就太危險了。 那類加載器是如何保證這個過程的安全性呢?其實,它是有著嚴格的等級制度的。 #### 幾個類加載器 首先,我們介紹幾個不同等級的類加載器。 * **Bootstrap ClassLoader** 這是加載器中的大 Boss,任何類的加載行為,都要經它過問。它的作用是加載核心類庫,也就是 rt.jar、resources.jar、charsets.jar 等。當然這些 jar 包的路徑是可以指定的,-Xbootclasspath 參數可以完成指定操作。 這個加載器是 C++ 編寫的,隨著 JVM 啟動。 * **Extention ClassLoader** 擴展類加載器,主要用于加載 lib/ext 目錄下的 jar 包和 .class 文件。同樣的,通過系統變量 java.ext.dirs 可以指定這個目錄。 這個加載器是個 Java 類,繼承自 URLClassLoader。 * **App ClassLoader** 這是我們寫的 Java 類的默認加載器,有時候也叫作 System ClassLoader。一般用來加載 classpath 下的其他所有 jar 包和 .class 文件,我們寫的代碼,會首先嘗試使用這個類加載器進行加載。 * **Custom ClassLoader** 自定義加載器,支持一些個性化的擴展功能。 ### 雙親委派機制 關于雙親委派機制的問題面試中經常會被問到,你可能已經倒背如流了。 雙親委派機制的意思是除了頂層的啟動類加載器以外,其余的類加載器,在加載之前,都會委派給它的父加載器進行加載。這樣一層層向上傳遞,直到祖先們都無法勝任,它才會真正的加載。 打個比方。有一個家族,都是一些聽話的孩子。孫子想要買一塊棒棒糖,最終都要經過爺爺過問,如果力所能及,爺爺就直接幫孫子買了。 但你有沒有想過,“類加載的雙親委派機制,雙親在哪里?明明都是單親?” 我們還是用一張圖來講解。可以看到,除了啟動類加載器,每一個加載器都有一個parent,并沒有所謂的雙親。但是由于翻譯的問題,這個叫法已經非常普遍了,一定要注意背后的差別。 ![](https://img.kancloud.cn/0e/58/0e58f6f2e92c259675346dea87d225f2_797x397.png) 我們可以翻閱 JDK 代碼的 ClassLoader#loadClass 方法,來看一下具體的加載過程。和我們描述的一樣,它首先使用 parent 嘗試進行類加載,parent 失敗后才輪到自己。同時,我們也注意到,這個方法是可以被覆蓋的,也就是雙親委派機制并不一定生效。 ![](https://img.kancloud.cn/e3/d9/e3d9fed30bb5552b5bb62a35a8c0292f_723x607.png) 這個模型的好處在于 Java 類有了一種優先級的層次劃分關系。比如 Object 類,這個毫無疑問應該交給最上層的加載器進行加載,即使是你覆蓋了它,最終也是由系統默認的加載器進行加載的。 如果沒有雙親委派模型,就會出現很多個不同的 Object 類,應用程序會一片混亂。 ### 一些自定義加載器 下面我們就來聊一聊可以打破雙親委派機制的一些案例。為了支持一些自定義加載類多功能的需求,Java 設計者其實已經作出了一些妥協。 #### 案例一:tomcat tomcat 通過 war 包進行應用的發布,它其實是違反了雙親委派機制原則的。簡單看一下 tomcat 類加載器的層次結構。 ![](https://img.kancloud.cn/27/cd/27cd0cd78982d7a55a78ffdfce30321f_659x580.png) 對于一些需要加載的非基礎類,會由一個叫作 WebAppClassLoader 的類加載器優先加載。等它加載不到的時候,再交給上層的 ClassLoader 進行加載。這個加載器用來隔絕不同應用的 .class 文件,比如你的兩個應用,可能會依賴同一個第三方的不同版本,它們是相互沒有影響的。 如何在同一個 JVM 里,運行著不兼容的兩個版本,當然是需要自定義加載器才能完成的事。 那么 tomcat 是怎么打破雙親委派機制的呢?可以看圖中的 WebAppClassLoader,它加載自己目錄下的 .class 文件,并不會傳遞給父類的加載器。但是,它卻可以使用 SharedClassLoader 所加載的類,實現了共享和分離的功能。 但是你自己寫一個 ArrayList,放在應用目錄里,tomcat 依然不會加載。它只是自定義的加載器順序不同,但對于頂層來說,還是一樣的。 #### 案例二:SPI Java 中有一個 SPI 機制,全稱是 Service Provider Interface,是 Java 提供的一套用來被第三方實現或者擴展的 API,它可以用來啟用框架擴展和替換組件。 這個說法可能比較晦澀,但是拿我們常用的數據庫驅動加載來說,就比較好理解了。在使用 JDBC 寫程序之前,通常會調用下面這行代碼,用于加載所需要的驅動類。 ``` Class.forName("com.mysql.jdbc.Driver") ``` 這只是一種初始化模式,通過 static 代碼塊顯式地聲明了驅動對象,然后把這些信息,保存到底層的一個 List 中。這種方式我們不做過多的介紹,因為這明顯就是一個接口編程的思路,沒什么好奇怪的。 **但是你會發現,即使刪除了 Class.forName 這一行代碼,也能加載到正確的驅動類,什么都不需要做,非常的神奇,它是怎么做到的呢**? 我們翻開 MySQL 的驅動代碼,發現了一個奇怪的文件。之所以能夠發生這樣神奇的事情,就是在這里實現的。 路徑: ``` mysql-connector-java-8.0.15.jar!/META-INF/services/java.sql.Driver ``` 里面的內容是: ``` com.mysql.cj.jdbc.Driver ``` 通過在 META-INF/services 目錄下,創建一個以接口全限定名為命名的文件(內容為實現類的全限定名),即可自動加載這一種實現,這就是 SPI。 SPI 實際上是“基于接口的編程+策略模式+配置文件”組合實現的動態加載機制,主要使用 java.util.ServiceLoader 類進行動態裝載。 ![](https://img.kancloud.cn/50/55/5055241d575bee422654655d92138f39_757x268.png) 這種方式,同樣打破了雙親委派的機制。 DriverManager 類和 ServiceLoader 類都是屬于 rt.jar 的。它們的類加載器是 Bootstrap ClassLoader,也就是最上層的那個。而具體的數據庫驅動,卻屬于業務代碼,這個啟動類加載器是無法加載的。這就比較尷尬了,雖然凡事都要祖先過問,但祖先沒有能力去做這件事情,怎么辦? 我們可以一步步跟蹤代碼,來看一下這個過程。 ``` //part1:DriverManager::loadInitialDrivers //jdk1.8 之后,變成了lazy的ensureDriversInitialized ... ServiceLoader <Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); ... //part2:ServiceLoader::load public static <T> ServiceLoader<T> load(Class<T> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } ``` 通過代碼你可以發現 Java 玩了個魔術,它把當前的類加載器,設置成了線程的上下文類加載器。那么,對于一個剛剛啟動的應用程序來說,它當前的加載器是誰呢?也就是說,啟動 main 方法的那個加載器,到底是哪一個? 所以我們繼續跟蹤代碼。找到 Launcher 類,就是 jre 中用于啟動入口函數 main 的類。我們在 Launcher 中找到以下代碼。 ``` public Launcher() { Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader); ... } ``` 到此為止,事情就比較明朗了,當前線程上下文的類加載器,是應用程序類加載器。使用它來加載第三方驅動,是沒有什么問題的。 我們之所以花大量的篇幅來介紹這個過程,第一,可以讓你更好的看到一個打破規則的案例。第二,這個問題面試時出現的幾率也是比較高的,你需要好好理解。 #### 案例三:OSGi OSGi 曾經非常流行,Eclipse 就使用 OSGi 作為插件系統的基礎。OSGi 是服務平臺的規范,旨在用于需要長運行時間、動態更新和對運行環境破壞最小的系統。 OSGi 規范定義了很多關于包生命周期,以及基礎架構和綁定包的交互方式。這些規則,通過使用特殊 Java 類加載器來強制執行,比較霸道。 比如,在一般 Java 應用程序中,classpath 中的所有類都對所有其他類可見,這是毋庸置疑的。但是,OSGi 類加載器基于 OSGi 規范和每個綁定包的 manifest.mf 文件中指定的選項,來限制這些類的交互,這就讓編程風格變得非常的怪異。但我們不難想象,這種與直覺相違背的加載方式,肯定是由專用的類加載器來實現的。 隨著 jigsaw 的發展(旨在為 Java SE 平臺設計、實現一個標準的模塊系統),我個人認為,現在的 OSGi,意義已經不是很大了。OSGi 是一個龐大的話題,你只需要知道,有這么一個復雜的東西,實現了模塊化,每個模塊可以獨立安裝、啟動、停止、卸載,就可以了。 不過,如果你有機會接觸相關方面的工作,也許會不由的發出感嘆:原來 Java 的類加載器,可以玩出這么多花樣。 ### 如何替換 JDK 的類 讓我們回到本課時開始的問題,如何替換 JDK 中的類?比如,我們現在就拿 HashMap為例。 當 Java 的原生 API 不能滿足需求時,比如我們要修改 HashMap 類,就必須要使用到 Java 的 endorsed 技術。我們需要將自己的 HashMap 類,打包成一個 jar 包,然后放到 -Djava.endorsed.dirs 指定的目錄中。注意類名和包名,應該和 JDK 自帶的是一樣的。但是,java.lang 包下面的類除外,因為這些都是特殊保護的。 因為我們上面提到的雙親委派機制,是無法直接在應用中替換 JDK 的原生類的。但是,有時候又不得不進行一下增強、替換,比如你想要調試一段代碼,或者比 Java 團隊早發現了一個 Bug。所以,Java 提供了 endorsed 技術,用于替換這些類。這個目錄下的 jar 包,會比 rt.jar 中的文件,優先級更高,可以被最先加載到。 ### 小結 通過本課時的學習我們可以了解到,一個 Java 類的加載,經過了加載、驗證、準備、解析、初始化幾個過程,每一個過程都劃清了各自負責的事情。 接下來,我們了解到 Java 自帶的三個類加載器。同時了解到,main 方法的線程上下文加載器,其實是 Application ClassLoader。 一般情況下,類加載是遵循雙親委派機制的。我們也認識到,這個雙親,很有問題。通過 3 個案例的學習和介紹,可以看到有很多打破這個規則的情況。類加載器通過開放的 API,讓加載過程更加靈活。 Java 的類加載器是非常重要的知識點,也是面試常考的知識點,本課時提供了多個面試題,你可以實際操作體驗一下。 所以我們在課時開始時的第三個問題就很簡單了,無論是遠程存儲字節碼,還是將字節碼進行加密,這都是業務需求。要做這些,我們實現一個新的類加載器就可以了。 ### 課后問答 * 1、類加載器是加載字節碼變成機器碼給執行引擎去執行的,那么類加載器是誰來加載的? 答案:啟動類加載器,就是最上面那一個,是c代碼實現的,沒有繼承classloader類。它就是一段native邏輯,所以沒有加載這種概念。它的實現參考${openjdk}\hotspot\src\share\vm\classfile 目錄下的 classLoader.cpp 與classLoader.hpp * 2、 ``` static int a; static { a = 1; b = 1; } static int b; public static void main(String[] args) { System.out.println("a = " + a); System.out.println("b = " + b); } ``` 這段代碼的執行結果真的是a=1,b=1;如果static int b=0;那么結果就是a=1,b=0;如果static int b;那么結果就是a=1,b=1。這個我就想不通為什么了,請老師講解一下。 答案:這個原因可以用同樣的方式獲得,建議實操一下。可以看到只有“1: putstatic #3”和”5: putstatic #5“兩個賦值操作。注意聲明動作并沒有賦值動作,它早已經在第3小節的準備階段就已經初始化成默認值了。準備階段->cinit->init,按這個順序分析一下? * 3、如何加載一個遠程的.class文件?怎么樣加密.class文件沒有提及到? 答案:自定義一個ClassLoader,通過覆蓋defineClass和findClass方法即可實現。具體的網絡和加密屬于業務范疇。 * 4、為什么說SPI是打破了類加載的雙親委派機制呢?使用System ClassLoader加載Driver的這個過程,System ClassLoader 仍然是會向上獲取Class,在上級的類加載器無法加載對應的Class后,System ClassLoader再去加載。這不正是雙親委派機制的流程嗎? 答案:SPI發起者是System ClassLoader,System ClassLoader已經是最上層的了。它直接獲取了App ClassLoader進行驅動加載,和雙親委派是相反的。 * 5、局部變量不像類變量那樣存在準備階段。類變量有兩次賦初始值的過程,一次在準備階段,賦予初始值(也可以是指定值)請問,怎么改成【任意的指定值】?修改源碼嗎?怎么改 答案:這里有兩種情況。 static int a = 1 ; 準備階段過后是0; final static int a = 1; 準備階段后是1;區別是final。 * 6、 ``` static int a; static { a = 1; b = 1; } static int b; public static void main(String[] args) { System.out.println("a = " + a); System.out.println("b = " + b); } ``` 為什么這段代碼的執行結果就是a=1,b=1 答案:問題更正下,結果是1 0哈(不是1 1)。文章下面也提到了,是代碼的順序問題。看一下編譯后的字節碼,putstatic操作的順序是a,a,b,b。和我們代碼的順序是一致的,值被按順序覆蓋了。 ``` 0: iconst_0 1: putstatic #3 // Field a:I 4: iconst_1 5: putstatic #3 // Field a:I 8: iconst_1 9: putstatic #5 // Field b:I 12: iconst_0 13: putstatic #5 // Field b:I ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看