【jvm】jvm的類載入機制
前言:提到jvm的類載入機制,就不得不說我當年的沙雕經歷了,當年不明白為啥面試官都喜歡問jvm的類載入機制,當時心想學這東西有啥用,它怎麼載入關我啥事呀,能寫程式碼不就好了嗎?但無奈應試教育教會了我,雖然不知道為啥要學,但人家要考,你就得學,然後學唄,學完算是知道它是怎麼載入類的了,但依舊沒能深刻理解,而且所學的也僅僅停留在理論這個層面,而平時寫程式碼大多是寫業務,又很少接觸到這些,於是沒多久就給忘了...為了你們跟我一樣,開篇我先通過一道面試題來勾引你們,加深印象,這道面試題可以說是非常經典了:
class Singleton{ private static Singleton singleton = new Singleton(); public static int value1; public static int value2 = 0; private Singleton(){ value1++; value2++; } public static Singleton getInstance(){ return singleton; } } class Singleton2{ public static int value1; public static int value2 = 0; private static Singleton2 singleton2 = new Singleton2(); private Singleton2(){ value1++; value2++; } public static Singleton2 getInstance2(){ return singleton2; } } public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); System.out.println("Singleton1 value1:" + singleton.value1); System.out.println("Singleton1 value2:" + singleton.value2); Singleton2 singleton2 = Singleton2.getInstance2(); System.out.println("Singleton2 value1:" + singleton2.value1); System.out.println("Singleton2 value2:" + singleton2.value2); }
上面程式碼執行後的結果是:
Singleton1 value1 : 1
Singleton1 value2 : 0
Singleton2 value1 : 1
Singleton2 value2 : 1
是不是有點驚訝,不要緊,在我真正理解Jvm類載入機制之前,也是這種想法,why? 擺了佛冷!
先不要急問為啥是這樣的結果,我們先來學習下Jvm的類載入機制,學完再來分析,你就豁然開朗了.
先來看下Jvm的類載入步驟,一般分為五步,如果拆分開來,可以分為七步:
第一步:載入
載入主要是將.class檔案以二進位制流的形式讀入jvm中.
載入.class檔案的方式 – 從本地系統中直接載入 – 通過網路下載.class檔案 – 從zip,jar等歸檔檔案中載入.class檔案 – 從專有資料庫中提取.class檔案 – 將Java原始檔動態編譯為.class檔案
在類的載入階段,Jvm需要完成以下三件事:
1)通過類的全限定名獲取該類的二進位制位元組流;
2)將位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;
3)在記憶體中生成一個該類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。
第二步:連線
連線一共由三個步驟構成
①驗證:主要是為了檢查載入進來的位元組碼是否符合Jvm規範,不要是那些不合法的甚至對Jvm造成損害的位元組碼檔案.
驗證階段會完成以下4個階段的檢驗動作:
1)檔案格式驗證 2)元資料驗證(是否符合Java語言規範) 3)位元組碼驗證(確定程式語義合法,符合邏輯) 4)符號引用驗證(確保下一步的解析能正常執行)
②準備:為靜態變數所在方法區分配記憶體,並設定預設初始值.
③解析:虛擬機器將常量池內的符號引用替換為直接引用的過程
第三步:初始化
初始化階段是類載入過程的最後一步,主要是根據程式中的賦值語句主動為類變數賦值.
在Java中對類變數進行初始值設定有兩種方式:
1)宣告類變數是指定初始值
2)使用靜態程式碼塊為類變數指定初始值
類的初始化步驟:
如果是一個子類進行初始化會先對其父類進行初始化,保證其父類在子類之前進行初始化;所以其實在java中初始化一個類,那麼必然是先初始化java.lang.Object,因為所有的java類都繼承自java.lang.Object
類初始化時機:
只有當對類的主動使用的時候才會導致類的初始化,類的主動使用包括以下六種:
– 建立類的例項,也就是new的方式
– 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
– 呼叫類的靜態方法
– 反射(如Class.forName(“com.shengsiyuan.Test”))
– 初始化某個類的子類,則其父類也會被初始化
– Java虛擬機器啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來執行某個主類
第四步:使用
使用沒啥好說的,就是呼叫類裡面的方法/變數/常量等...
第五步:解除安裝
GC把無用物件從jvm記憶體中解除安裝.
在以下幾種情況下,jvm會結束生命週期:
– 執行了System.exit()方法
– 程式正常執行結束
– 程式在執行過程中遇到了異常或錯誤而異常終止
– 由於作業系統出現錯誤而導致Java虛擬機器程序終止
有了上面的jvm類載入機制儲備,我們再回過頭來看這道面試題:
#先看singleton1
①載入:以二進位制流的形式載入進虛擬機器
②驗證:驗證是否符合jvm規範
③準備:為靜態變數value1,value2分配記憶體,並初始化值,由於是value1和value都是基本資料型別int,非引用型別,所以初始值均為0,此時value1和value2均為0
④解析:將常量池中的符號引用變為直接引用
⑤初始化:由於在Main方法中呼叫了靜態方法getInstance,拆成兩步,結果一目瞭然:
第一步:該靜態方法構造例項時其構造器將value1和value2都做了自增,value1和value2的值都變為了1;
第二步:由於呼叫了靜態方法,初始化被觸發,會對變數進行賦值操作,由於value2在程式碼中有value2=0這樣的賦值語句,於是value2的值變為了0;
⑥使用:在System.out.println列印到控制檯,value1的值為1,value2的值為0
⑦解除安裝:被jvm GC回收
#再看singleton2
①載入:以二進位制流的形式載入進jvm虛擬機器
②驗證:驗證是否符合jvm規範
③準備:為靜態變數value1和value2分配記憶體,並設定初始值,由於value1和value2都是基本資料型別int,非引用型別,所以初始值均為1,此時value1=0,value2=0;
④解析:將常量池中的符號引用替換為直接引用
⑤初始化:由於Main方法中呼叫了靜態方法getInstance,同樣拆成兩步:
第一步:由於呼叫了靜態方法,初始化被觸發,會對變數進行賦值操作,此時value1和value2均為0,賦值語句value2=0執行後,其值仍保持0不變;
第二步:該靜態方法構造例項時其構造器將value1和value2的值都做了自增,value1和value2的值都變為1
⑥使用:在System.out.println列印到控制檯,value1的值為1,value2的值也為1
⑦解除安裝:被jvm GC回收
細心點的朋友會發現,其實singleton1和sington2的主要區別就是:
singleton1是在構造方法執行後執行了賦值操作,sington2是在構造方法執行前執行了賦值操作,這和程式碼所處的位置有關,java是預設從上往下執行程式碼的,所以在以後書寫程式碼時務必注意程式碼所處的位置,否則可能會讓你在jvm類載入機制上入坑一把...
說完了類的載入機制,再來了解下jvm的類載入器,Jvm的類載入器主要是在類載入的第一步,為jvm以二進位制的流的形式獲取位元組碼檔案.
jvm的類載入器主要有以下三種:
啟動類載入器(Bootstrap ClassLoader):最頂層的類載入器,負責載入 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可(按檔名識別,如rt.jar)的類。
擴充套件類載入器(Extension ClassLoader):負責載入 JAVA_HOME\lib\ext 目錄中的,或通過java.ext.dirs系統變數指定路徑中的類庫。
應用程式類載入器(Application ClassLoader):也叫做系統類載入器,可以通過getSystemClassLoader()獲取,負責載入使用者路徑(classpath)上的類庫。如果沒有自定義類載入器,一般這個就是預設的類載入器。
各個類載入器之間的層次關係如圖:
類載入器之間的這種層次關係叫做雙親委派模型:
雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器.
雙親委派模型的工作過程是:
-
如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成。
-
每一個層次的類載入器都是如此。因此,所有的載入請求最終都應該傳送到頂層的啟動類載入器中。
-
只有當父載入器反饋自己無法完成這個載入請求時(搜尋範圍中沒有找到所需的類),子載入器才會嘗試自己去載入。
作用:對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。因此,使用雙親委派模型來組織類載入器之間的關係,有一個顯而易見的好處:類隨著它的類載入器一起具備了一種帶有優先順序的層次關係.
例如類java.lang.Object
,它由啟動類載入器載入。雙親委派模型保證任何類載入器收到的對java.lang.Object
的載入請求,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。
相反,如果沒有使用雙親委派模型,由各個類載入器自行去載入的話,如果使用者自己編寫了一個稱為java.lang.Object的類,並用自定義的類載入器載入,那系統中將會出現多個不同的Object類,Java型別體系中最基礎的行為也就無法保證,應用程式也將會變得一片混亂.
雙親委派模型的程式碼實現:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//1 首先檢查類是否被載入
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//2 沒有則呼叫父類載入器的loadClass()方法;
c = parent.loadClass(name, false);
} else {
//3 若父類載入器為空,則預設使用啟動類載入器作為父載入器;
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
//4 若父類載入失敗,丟擲ClassNotFoundException 異常後
c = findClass(name);
}
}
if (resolve) {
//5 再呼叫自己的findClass() 方法。
resolveClass(c);
}
return c;
}
破壞雙親委派模型
雙親委派模型很好的解決了各個類載入器載入基礎類的統一性問題。即越基礎的類由越上層的載入器進行載入。
若載入的基礎類中需要回呼叫戶程式碼,而這時頂層的類載入器無法識別這些使用者程式碼,怎麼辦呢?這時就需要破壞雙親委派模型了。
下面介紹兩個例子來講解破壞雙親委派模型的過程。
JNDI破壞雙親委派模型
JNDI是Java標準服務,它的程式碼由啟動類載入器去載入。但是JNDI需要回調獨立廠商實現的程式碼,而類載入器無法識別這些回撥程式碼(SPI)。
為了解決這個問題,引入了一個執行緒上下文類載入器。 可通過Thread.setContextClassLoader()設定。
利用執行緒上下文類載入器去載入所需要的SPI程式碼,即父類載入器請求子類載入器去完成類載入的過程,而破壞了雙親委派模型。
Spring破壞雙親委派模型
Spring要對使用者程式進行組織和管理,而使用者程式一般放在WEB-INF目錄下,由WebAppClassLoader類載入器載入,而Spring由Common類載入器或Shared類載入器載入。
那麼Spring是如何訪問WEB-INF下的使用者程式呢?
使用執行緒上下文類載入器。 Spring載入類所用的classLoader都是通過Thread.currentThread().getContextClassLoader()獲取的。當執行緒建立時會預設建立一個AppClassLoader類載入器(對應Tomcat中的WebAppclassLoader類載入器): setContextClassLoader(AppClassLoader)。
利用這個來載入使用者程式。即任何一個執行緒都可通過getContextClassLoader()獲取到WebAppclassLoader。
參考地址:https://nomico271.github.io/2017/07/07/JVM%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/