死磕JVM:類加載機制

死磕JVM:類加載機制
一、前言
在java代碼編譯後會生成class字節碼文件,而在class字節碼文件中的描述的各種信息最終都會被加載到jvm中來運行和使用,筆者是在通過閱讀周志明的《深入理解Java虛擬機》第二版後,對書中類加載機制相關內容進行歸納、總結、整理後形成的本文,若想深入瞭解類加載機制或者JVM,筆者建議《深入理解Java虛擬機》是一個好的選擇。

二、類加載的時機
類從被加載到jvm中到卸載出內存,它的整個生命週期爲:加載、驗證、準備、解析、初始化、使用、卸載。
其中加載、驗證、準備、初始化、卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班的開始,而解析階段卻不一定,爲了支持運行時綁定它可能在初始化後開始。這些階段通常都是互相交叉混合進行,通常都在一個階段的執行過程中調用、激活另外一個階段。

什麼時候進行類加載過程的第一個階段?
jvm規範中沒有進行強制約束,可以交給虛擬機的實現來自由把握,。但初始化階段,虛擬機規定了有5種情況必須立即對類進行初始化(而加載、驗證、準備要在之前進行):
(1)遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行初始化,則需要先觸發其初始化,此處需要注意已在編譯期把結果放入常量池的靜態字段除外。
(2)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行初始化,則需要先觸發其初始化。
(3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
(4)當虛擬機啓動時,用戶需要指定一個要執行的主類。虛擬機會先初始化這個主類。
(5)當使用JDK1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先對其進行初始化。

這5種場景中的行爲稱爲對一個類的主動引用。除此之外,所有引用的類的方式都不會觸發初始化,稱爲被動引用。

在《深入理解java虛擬機》書中舉出了三個極爲容易理解錯的被動引用,先總結如下:
(1)通過子類引用父類的靜態字段不會導致子類初始化。
(2)通過數組定義來引用類,不會觸發此類的初始化。
(3)常量在編譯階段會存入調用類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發定義常量類的初始化。

接口與類的初始化的有什麼不同?當一個類在初始化時,要求其父類全部已經初始化了,但是在一個接口初始化時,並不要求其父接口全部門已經初始化了,只有在真正使用父接口的時候纔會初始化。

三、類加載的過程
1.加載
(1)通過一個類的全限定名來獲取定義此類的二進制字節流。
(2)將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
(3)在內存中生成一個代表這個類的java.lang.class對象,作爲方法區這個類的各種數據的訪問接口。

2.驗證
(1)文件格式驗證。主要爲驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。
(2)元數據驗證。主要爲對字節碼描述的信息進行語義分析,以保證其描述的信息符合java 語言的規範。
(3)字節碼驗證。主要爲將類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全 的事件。
(4)符號引用驗證。主要爲對類自身以外(常量池中各種符號引用)的信息進行匹配性校驗。這個校驗發生在 虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三個階段解析階段發生。

3.準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。這個時候進行內存分配的僅包括類變量(被static修飾的表量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在java堆中。初始值在通常情況下是數據類型的零值。如果類字段屬性表中存在ConstantValue屬性(final),那麼在準備階段變量value就會被初始化爲ConstantValue屬性所對用的值。

4解析
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程,虛擬機規範中並未規定解析階段發生的具體時間,只要求了在執行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestaic、invokevirtual、idc、idc_w、multianewarray、new、putfield和putstatic這16個用於操作符號引用的字節碼指令之前,先對它們所使用的符號引用進行解析。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄、和調用點限定符7類符號引用進行。
5.初始化
初始化階段是執行類構造器方法的過程。方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句快中只能訪問到定義在靜態語句塊之前的變量,定義在之後的變量,在前面的靜態語句塊中可以賦值,但不能訪問。虛擬機會保證在子類的方法執行之前,父類的方法已經執行完畢,因此虛擬機中第一個被執行的方法肯定是java.lang.Object.

四、類加載器
虛擬機設計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作放到java虛擬機外部去實現,以便應用程序自己決定去如何獲取所需要的類,實現這個動作的代碼模塊稱爲”類加載器”。

1.類與類加載器
對於任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java中的唯一性,每一個類加載器,都擁有一個獨立的類名稱空間。

2.類加載器分類
從java虛擬機的角度來講,只存在兩種不同的類加載器:一種是啓動類加載器,這個類加載器使用C++語言實現,是虛擬機自身的一部分;另外一種就是所有的其他類加載器,這些類加載器都由java語言實現,獨立於虛擬機外部,並且全部繼承自抽象類java.lang.ClassLoader.按照Java開發者的角度可以把類加載器分爲三類:啓動類加載器、擴展類加載器(它負責加載<JAVA_HOME>\lib\ext目錄中的或者被java.ext.dirs系統變量指定的路徑中的所有類庫,開發者可以直接使用)、應用程序類加載器(一般情況下是程序中默認的類加載器)。

3.雙親委派模型
雙親委派模型要求除了啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。類加載之間的父子關係一般不會以繼承的關係來實現,而是都使用組合關係來複用父加載器的代碼。它的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器纔會嘗試加載。

4.破壞雙親委派模型
(1)爲了解決基礎類要調用回用戶的代碼,java設計團隊曾引入了一個線程上下文類加載器,這個類加載器可以通過java.lang.Thread類的setContextClassloaser()方法進行設置,如果創建線程時未設置,它會從父線程繼承一個,如果應用程序的全局範圍內都沒有設置過,則默認就是應用程序類加載器,通常使用在JNDI、JDBC、JCE、JAXB、JBI等涉及SPI操作,這些服務通過線程上下文類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載器去完成類加載,這種行爲實際上打破了雙親委派模型。
(2)用戶追求對程序動態性(代碼熱替換、模塊熱部署等等),例如業界著名的OSGI。

原文:https://blog.csdn.net/qq_36236890/article/details/80558728

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章