引言
在開始之前先來了解一下撰寫的java代碼如何在作業系統上運行起來,

java檔案通過javac編譯成class檔案生成位元組碼,JVM會加載位元組碼,運行時解釋器將位元組碼解釋成一行行機器碼進行執行,在程式運行期間,即時編譯器會針對熱點的代碼將該部分位元組碼編譯成機器碼已獲得更高的執行效率,在整個運行時,解釋器和即時編譯器互相配合,使java程式集合能達到和編譯語言一樣的執行速度,
在上面這段話中,包含了許多現階段我未接觸的技術點,但是今天了解的就是jvm加載位元組碼這個程序,此程序被稱為java類加載的程序,今天,看我入門java類加載,如果沒入門,那權當敲門了!
java類生命周期

該圖表示了一個類的生命周期,完整一點,可以加上最開始的javac編譯階段,而“類加載”只包括加載,連接,初始化三個程序,
注意區別“類加載”與“加載”,加載只是類加載的第一個環節,
有時候決議階段發生在初始化階段之后,具體原因見決議階段,
類的初始化
最早接觸類的加載,應該是在學習反射反射的時候,那就先來看一下如何使用一個類,也就是類的初始化程序,再詳細看一下如何加載到連接初始化一個類,
有5種情況會對類進行初始化:
1)遇到new、getstatic、putstatic或invokestatic這4條位元組碼指令,
常見場景:使用new創建一個物件,操作靜態變數或靜態方法,
Demo demo = new Demo(); // 使用new創建一個物件
Demo.FIELD = 1; // 設定靜態變數值
System.out.println(Demo.FIELD); // 使用靜態變數
Demo.method(); // 執行靜態方法
2)通過反射進行呼叫
Class clazz = Class.forName(“com.dfyang.aop.utils.Demo”);
3)當初始化一個類時,先初始化其父類(這個容易理解,畢竟子類會繼承父類)
4)當虛擬機啟動時,虛擬機需要初始化主類,也就是程式的入口
public static void main(String[] args) {
System.out.println("程式入口");
}
@SpringBootApplication
public class ZooCommunityApplication {
public static void main(String[] args) {
SpringApplication.run(ZooCommunityApplication.class, args);
}
}
5)java.lang.invoke.MethodHandle實體最后決議結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄(這里僅了解)
這里我們已經知道了我們經常使用的類是如何進行初始化,接下來我們再來看看類的加載程序,
類加載
加載是讀取Class檔案,將其轉化為某種靜態資料結構存盤在方法去內,并在堆中生成一個便于用戶呼叫的java.lang.Class型別的物件的程序,
有圖有真相,圖形化編碼方式建立腦神經索引,

位元組碼檔案的來源:
從本地檔案系統中直接加載,
通過網路獲取,比如Web Applet應用,
從打包中獲取,jar、war等,
運行時計算生成,比如動態代理技術,
…
連接階段
連接:驗證

目的在于確保Class檔案的位元組流中包含資訊符合當前虛擬機要求,保證被加載類的正確性,不會危害到虛擬機自身安全,主要包括4種驗證:檔案格式驗證、元資料驗證、位元組碼驗證、符號參考驗證,
其實,檔案格式驗證發生在加載階段,驗證主要進行元資料,位元組碼驗證,符號參考驗證主要發生在決議階段,
總而言之,驗證其實發生在各個階段,jvm會進行嚴格的驗證,同時驗證型別官方也在不斷擴展,
連接:準備階段
該階段為類變數分配記憶體并設定該類變數的默認初始值,比如物件引入設定為null,int型設定為0,
這里不會包含使用final修飾的static,因為final在編譯時就會分配記憶體了,準備階段會顯示初始化,
這里也不會為實體變數分配記憶體以及初始化,類變數會分配在方法區中,而實體變數會隨著物件一起分配在java堆中,
(由于jdk8以前,類元資訊常量池靜態變數等存盤在永久代,方法區實作了永久代,jdk8以后常量池,靜態變數存盤在堆中,類的元空間存盤在元空間,方法區實作了元空間,該說法是否正確,由于筆者暫時未學習記憶體模型及相關知識,具體的尚未探究,暫時存疑)
連接: 決議階段
將常量池內的符號參考轉換為直接參考的程序,
理解為類A中參考了類B,在編譯階段使用字符S表示B的地址,在A加載時到決議階段,觸發B的遞回加載,此時A中的符號參考S被替換為直接參考B的實際地址,
我以為事情就這么結束了,然而事情剛剛開始!!
我們初學多型時遇到過,java通過后期系結的方式來實作多型,那么后期系結如何實作的呢,以下說一下我初次學習的簡單理解,
動態系結其實就是決議階段的動態決議,接著上面所說的,A呼叫的B是一個具體的實作類的話,就成為靜態決議,此時決議的目標類很明確,只要進行加載獲取目標類的加載地址即可替換成功,
假如上層java代碼使用了多型,那么B可能是一個抽象類或者介面,那么它可能有兩個具體的實作類C和D,此時B的具體實作并不明確,當然也就不知道使用的是哪一個具體類的直接參考來替換,既然不知道,那就等運行程序中發生呼叫,此時虛擬機呼叫堆疊中將得到具體的的型別資訊,這時候進行決議,就可以用明確的直接參考進行符號替換,也就是動態決議,這就是為啥有時候決議會發生在初始化階段之后,
此時連接部分完成,外部加載的java類成功引入程式中,
再談類的初始化
初始化階段此時會判斷代碼中是否存在主動地資源初始化操作,
主動初始化不是指的建構式,而是class層面的,比如成員變數賦值動作,靜態變數的賦值動作,以及靜態代碼塊的邏輯,
而只有顯示呼叫new指令,才會呼叫建構式進行物件的初始化,這是物件層面,
那么,類的加載程序終于終于敲門結束,那么既然敲了門那么我們再透過門縫往里面再瞧一瞧,我們簡單看一下實體化子類時,父類與子類中的靜態代碼塊、實體代碼塊、靜態變數、實體變數、建構式的執行順序是怎樣的?
Java父類與子類中靜態代碼塊 實體代碼塊 靜態變數 實體變數 建構式執行順序
詳細順序為:
1.父類靜態代碼塊與父類靜態變數賦值(取決于代碼書寫順序)
2.子類靜態代碼塊與子類靜態變數賦值(取決于代碼書寫順序)
3.父類實體變數賦值與父類代碼塊(取決于代碼書寫順序)
4.父類建構式
5.子類實體變數賦值與子類代碼塊(取決于代碼書寫順序)
6.子類建構式
ok,那么本次JVM類加載機制–敲門篇暫告段落,預知門后世界如何,待我苦修內功,帶你華山論劍!
我是Code_Pianist,一位初學java的修士,關注我,苦修內功,華山論劍!
本篇參考資料:
文章部分圖片來自:b站寒食君
參考內容來自:b站寒食君,DFYoung,leunging,《深入理解java虛擬機》等多方資料匯總
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/302053.html
標籤:其他
