首頁 > 科技

吹爆!學完這份「JVM類載入機制手冊」,你也可以進大廠上班

2021-06-03 17:42:27

Java程式碼執行流程

類生命週期

類的生命週期包括:載入、連結、初始化、使用和解除安裝,其中載入、連結、初始化,屬於類載入的過程,我們下面仔細講解。使用是指我們new物件進行使用,解除安裝指物件被垃圾回收掉了。

載入

載入指的是把class位元組碼檔案從各個來源通過類加器(classLoader)裝入記憶體中:

通過一個類的全限定名(包名+類名)來獲取定義此類的二進位制位元組流。將這個位元組流所代表的靜態儲存結構轉化為方法區的運行時資料結構。在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入位元組碼來源: 一般的載入源包括從本地路徑下編譯生成的class檔案,從jar包中的clas檔案,從遠端網路,以及動態代理時編譯的class檔案。

連結

驗證(Verify)

確保class檔案中的位元組流包含的資訊,符合當前虛擬機器的要求,保證這個被載入的class類的正確性,不會危害到虛擬機器的安全。

檔案格式的驗證,檔案中是否有不規範的或者附加的其他資訊。例如常量中是否有不被支援的常量元資料的驗證,保證其描述的資訊符合Java語言規範的要求,例如是否有父類,是否承了不被允許的finall類等位元組碼的驗證,保證程式語義的合理性,比如要保證類型轉換的合理性符號引用的驗證,比如校檢符號引用中通過全限定名是否能夠找到對應的類,校驗符號引用中的訪問性(priate,pubc等)是否可被當前類訪問等準備(prepare)

為類變數(注息,不是例項變數)分配記憶體,並且予初值。初值不是程式碼中具體寫的初始化的值,而是Java擬機根不同變數類型的賦認初始值。例如int型初值為0, reference為null等。

解析(resolve)

將常量池內的符號引用換為直接引用的過程。舉個例子來說,現在呼叫方法 hello(),這個方法的地址是1234567,那麼hello就是符號引用,1234567就是直接引用。在解析階段,虛擬機器會把所有的類名,方法名,欄位名這些符號引用皆換為具體的記憶體地址或偏移量,也就是直接引用。初始化

這個階段主要是對類變數初始化,是執行類構造器的過程,《Java虛擬機器規範》嚴格規定了有且只有六種情況必須立即對類進行「初始化」

遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時,如果類型沒有進行過初始化,則需要先觸發其初始化階段。能夠生成這四條指令的典型Java程式碼場景有:使用new關鍵字例項化物件的時候。讀取或設定一個類型的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候。呼叫一個類型的靜態方法的時候使用java.lang.reflect包的方法對類型進行反射呼叫的時候,如果類型沒有進行過初始化,則需要先觸發其初始化。當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含main()方法的那個類),虛擬機器會先初始化這個主類。當使用JDK 7新加入的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法控制代碼,並且這個方法控制代碼對應的類沒有進行過初始化,則需要先觸發其初始化。當一個介面中定義了JDK 8新加入的預設方法(被default關鍵字修飾的介面方法)時,如果有這個介面的實現類發生了初始化,那該介面要在其之前被初始化。類載入機制

雙親委派模型

arent Delegation, 雙親委託機制(這個翻譯和socket一樣,莫名其妙), 指多個類載入器之間存在父子關係的時候,某個class類具體由哪個載入器進行載入的問題。

具體過程如下:

當一個類載入的過程中,它首先不會去載入,而是委託給自己的父類去載入,父類又委託給自己的父類。因此所有的類載入都會委託給頂層的父類,即Bootstrap Classloader進行載入。如果父類自己無法完成這個載入請求,子載入器才會嘗試自己去載入。雙親委派機制的作用:

避免類重複載入導致衝突,保證Java核心庫的安全。

例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都能夠保證是同一個類。反之,如果沒有使用雙親委派模型,都由各個類載入器自行去載入的話,如果使用者自己也編寫了一個名為java.lang.Object的類,並放在程式的ClassPath中,那系統中就會出現多個不同的Object類,Java類型體系中最基礎的行為也就無從保證,應用程式將會變得一片混亂。

Bootstrap ClassLoader

Bootstrap ClassLoader 無父類載入器,巢狀在JVM內部,java程式無法直接操作這個類,使用C/C++語言實現。用於載入Java核心類庫,如<JAVA_HOME>/lib目錄下的類庫,出於安全考慮,啟動類只加載包名為:java、javax、sun開頭的類。Extension ClassLoader

擴展類載入器(Extention Classloader) 父類載入器為 Bootstrap ClassLoader,由Java語言編寫。擴展類載入器(Extention Classloader)負責載入JVM擴展類,比如從系統屬性 java.ext.dirs 目錄中載入類庫,或者從JDK安裝目錄 $JAVA_HOME/jre/lib/ext 目錄下載入類庫。我們就可以將我們自己的包放在以上目錄下,就會自動載入進來了。Application ClassLoader

應用程式載入器(Application Classloader)也叫系統類載入器,負責載入環境變數classpath或者系統屬性java.class.path指定路徑下的類庫。它同時也是程式中預設的類載入器,我們Java程式中的類,都是由它載入完成的。Custom ClassLoader

我們可以自定義類載入器,滿足特殊的類載入需求,如解決類衝突,實現熱載入,實現jar包的加密保護。主要由兩種實現方式:

繼承java.lang.ClassLoader,重寫findClass()方法繼承URLClassLoader類,重寫loadClass方法反向委派

Java 中有一個 SPI 機制,全稱是 Service Provider Interface,是 Java 提供的一套用來被第三方實現或者擴展的 API,它可以用來啟用框架擴展和替換元件。

常見的 SPI 有 JDBC、JNDI等,這些 SPI 的介面屬於 Java 核心庫,一般存在rt.jar包中,如下面的java.sql.Driver,這些類的類載入器是Bootstrap ClassLoader。

但是 com.mysql.jdbc.Driver 屬於業務程式碼,這個類是無法由 Bootstrap ClassLoader 載入的。此時出現了一種兩難的境地:Bootstrap類載入器無法直接載入SPI的實現類,同時由於雙親委派模式的存在,Bootstrap類載入器也無法反向委託AppClassLoader載入器SPI的實現類。

在這種情況下,我們就需要一種特殊的類載入器來載入第三方的類庫,而 執行緒上下文類載入器(雙親委派模型的破壞者)就是很好的選擇。

java.util.ServiceLoader 主要用於動態載入SPI介面的實現類,他的類載入器是 Bootstrap ClassLoader。

在上述程式碼中,ClassLoader cl = Thread.currentThread().getContextClassLoader() 將當前的載入器設定為執行緒上下文載入器,通過Launcher 類(jre 中用於啟動入口函數 main 的類)的程式碼可以發現,對於一個剛啟動的應用,他的上下文執行緒載入器就是 Application ClassLoader。

ClassLoader的使用場景

ClassLoader可以用於依賴衝突,熱載入和加密保護

依賴衝突

一個類的全限定名以及載入該類的載入器兩者共同形成了這個類在JVM中的惟一標識,假如不同中介軟體引分別引入一個依賴 JAR,但是JAR的版本都不同,分別為JAR1.0,JAR2.0和JAR3.0,根據maven依賴處理的機制,引用路徑最短的依賴會真正作為應用最終的依賴,其他版本的依賴會被忽略,我們可以使用不同的類載入器進行 JAR 的載入,就能同時引入不同版本的依賴.

熱載入

java項目的啟動少則幾十秒,多則幾分鐘,如此慢的啟動速度極大地影響了程式開發的效率,我們可以通過classloader完成對變更內容的載入,進行快速地偵錯。下面我實現一個簡單的Java熱載入。

自定義類載入器

定義需要進行熱載入的類

封裝熱載入類的相關資訊

類的熱載入(核心)

測試

我們使用idea進行斷點偵錯

修改源碼並重新編譯

繼續執行,從輸出中我們可以知道,class檔案已經被替換並載入

加密保護

基於java開發編譯產生的jar包是由.class位元組碼組成,由於位元組碼的檔案格式是有明確規範的。因此對於位元組碼進行反編譯,就很容易知道其源碼實現了。如果不希望被別人窺探源碼,那就需要對jar包進行加密。

jar包加密的本質,還是對位元組碼檔案進行操作。但是JVM虛擬機器載入class的規範是統一的,因此我們在最終載入class檔案的時候,還是需要滿足其class檔案的格式規範,否則虛擬機器是不能正常載入的。因此我們可以在打包的時候對class進行正向的加密操作,然後,在載入class檔案之前通過自定義classloader先進行反向的解密操作,然後再按照標準的class檔案標準進行載入,這樣就完成了class檔案正常的載入。因此這個加密的jar包只有能夠實現解密方法的classloader才能正常載入。

需要Java學習資料的小夥伴,關注回覆「資料」獲得噢。


IT145.com E-mail:sddin#qq.com