您好,登錄后才能下訂單哦!
本篇內容介紹了“Java類加載是什么意思”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
來源:《Java高并發編程詳解 多線程與架構設計》
類加載的過程可以簡單分為三個階段:
加載階段:主要負責查找并且加載類的二進制數據文件
連接階段:可以細分為驗證、準備、解析三個階段,驗證就是確保類文件的正確性,準備就是為類的靜態變量分配內存,并且為其初始化默認值,解析就是把類中的符號引用轉換為直接引用
初始化階段:為類的靜態變量賦予正確的初始值
JVM
規范規定了每個類或接口在首次主動使用的時候都需要進行初始化,規定了以下六種主動使用類的場景:
通過new
關鍵字會導致類的初始化
訪問類的靜態變量
訪問類的靜態方法
對某個類進行反射操作
初始化子類會導致父類初始化
啟動類(就是包含main()
的類)也會初始化
除了以上六種情況外,其余的都叫被動使用,不會導致類的加載和初始化,比如引用類的靜態常量不會導致類的初始化。
前面也說了類加載可以簡單分為三個階段:
加載階段
連接階段
初始化階段
下面先來看一下加載階段。
加載階段就是將class
文件中的二進制數據讀取到內存之中,然后將該字節流代表的靜態存儲結構轉換為方法區中運行時數據結構,并且在堆中生成一個該類的java.lang.Class
對象,作為訪問方法區數據結構的入口。
類加載的最終產物就是堆內存中的class
對象,JVM
規范中指出類加載是通過一個全限定名去獲取二進制數據流,來源包括:
class
文件:這是最常見的格式,就是加載javac
編譯后的字節碼文件
運行時動態生成:比如ASM
可以動態生成,或者可以通過動態代理java.lang.Proxy
生成等
通過網絡獲取:比如RMI
讀取壓縮文件:比如JAR
、WAR
包
從數據庫讀取:比如讀取MySQL
中的BLOB
字段類型的數據
運行時生成class
文件并且動態加載:比如Thrift
、Avro
等序列化框架,將某個schema
生成若干個class
文件并進行加載
類加載階段結束后,JVM
會將這些二進制字節流按照JVM
定義的格式存放在方法區中,形成特定的數據結構后再在堆內存中實例化一個java.lang.Class
對象。
該階段可以分為三個小階段:
驗證
準備
解析
需要注意的是這三個小階段其實不是順序進行的,而是交叉著進行的,也就是解析的時候其實也會有驗證的過程。
驗證是為了確保字節流所包含的內容符合JVM
規范,并且不會出現危害JVM
自身安全的代碼,當字節流信息不符合要求的時候,會拋出VerifyError
這樣的異常或其子異常,驗證的信息包括:
文件格式
元數據
字節碼
符號引用
包括:
魔數(0xCAFEBABE
)
主次版本號
是否存在殘缺或附加信息
常量池常量類型是否支持
常量池引用是否指向不存在常量或不支持類型常量
其他
元數據驗證其實是進行語義分析的過程,語義分析是為了確保字節流符合JVM
規范要求,包括:
檢查某個類是否存在父類,是否繼承某個接口,這些父類或接口是否合法,或是否存在
檢查是否繼承了final
的類
檢查抽象類,檢查是否實現了父類的抽象方法或接口方法
檢查重載,比如相同的方法名稱、相同的參數但是返回類型不同,這是不允許的
字節碼驗證主要是驗證程序的控制流程,包括:
保證當前線程在程序計數器中的指令不會跳轉到不合法的字節碼指令中去
保證類型的轉換是合法的
保證任意時刻虛擬機棧中的操作棧類型與指令代碼都能正確被執行
其他驗證
驗證符號引用轉換為直接引用的合法性,保證解析動作的順利執行,包括:
通過符號引用描述的字符串全限定名稱是否能夠順利找到相關的類
符號引用中的類、字段、方法是否對當前類可見
其他
經過驗證后,就開始了準備階段,這階段比較簡單,就是對對象的靜態變量分配內存并且設置初始值,類變量的內存會被分配到方法區中。設置初始值就是為相應的類變量給定一個相關類型在沒有被設置時的默認值,比如Int
的初始值為0,引用的初始值為null
。
解析就是在常量池中尋找類、字段、接口和方法的符號引用,并且將這些符號引用替換成直接引用的過程。解析主要針對類接口、字段、類方法和接口方法進行的,包括:
類接口解析
字段解析
類方法解析
接口方法解析
初始化階段主要就是執行<clinit>
方法的過程,該方法是編譯階段生成的,也就是說包含在字節碼文件中,該方法包含了所有類變量的賦值動作和靜態語句塊的執行代碼。另一方面,<clinit>
與構造方法不同,不需要顯式調用父類構造器,虛擬機會保證父類的<clinit>
方法最先執行。
還需要注意的是<clinit>
只能被虛擬機執行,虛擬機還會保證多線程下的安全性,因此,如果在靜態代碼塊中如果包含了加載其他類的操作可能會引起死鎖,例子可以看這里。
JVM
中的三類核心類加載器JVM
中有三類核心類加載器,分別是:
啟動類加載器:啟動類加載器是最頂層的類加載器,沒有父加載器,由C++
編寫,負責JVM
核心類庫的加載,比如加載整個java.lang
包中的類
擴展類加載器:擴展類加載器的父加載器是啟動類加載器,主要加載jre/lib/ext
子目錄下的類庫,純Java
實現,是URLClassLoader
的子類
應用類加載器:也叫系統類加載器,負責加載classpath
下的類庫,應用類加載器的父加載器為擴展類加載器,同時它也是自定義類加載器的默認父加載器
一個類加載器加載一個類的時候,并不會嘗試直接加載該類,而是先交給父加載器嘗試加載,一直到頂層的父加載器(啟動類加載器),如果父加載器加載失敗,則會自己嘗試加載,圖示如下:
JDK
中提供了很多SPI
(Service Provider Interface
),比如JDBC
等,JDBC
只規定了這些接口之間的邏輯關系,但不提供具體的實現,換句話說,JDBC
完全透明了應用程序和第三方廠商數據庫驅動的具體實現,應用程序只需要面向接口編程即可。但問題是:
java.lang.sql
中的所有接口都是由JDK
提供的,加載這些接口的類加載器是啟動類加載器
第三方廠商的類庫驅動由系統類加載器加載
由于雙親委派機制,Connections
、Statement
等都是由啟動類加載器加載,而第三方JDBC
驅動包中的實現不會被加載。解決這個問題的關鍵,就是使用了線程上下文類加載器打破了雙親委派機制。
比如MySQL
驅動的加載過程,就是通過線程上下文類加載器加載的,
private static Connection getConnection(String url, Properties info, Class<?> caller) throws SQLException { //... if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) { callerCL = Thread.currentThread().getContextClassLoader(); } while(true) { //... if (isDriverAllowed(aDriver.driver, callerCL)) { } } //... } private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) { //... try { aClass = Class.forName(driver.getClass().getName(), true, classLoader); } catch (Exception var5) { result = false; } //... return result; }
通過線程上下文類加載器,就變成了啟動類加載器去委托子類加載器去加載實現的方式,也就是JDK
自己親自打破了雙親委派機制這種方式,這種加載方式幾乎涉及所有的SPI
加載,包括JAXB
、JCE
、JBI
等。
“Java類加載是什么意思”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。