Java類載入機制,這篇大概、也許、可能就夠了

MDove發表於2018-07-30

寫在前面

關於Java類載入機制一至有沒辦法說的痛苦。因為當初我在學習這方面的內容時,多多少少有一些懵逼,所以這次的文章,將盡可能的把概念性的東西轉化成容易理解的內容,所以希望各位看到文章的童鞋可以有所收穫~

#正文開始

第一步,先讓我們們看一段程式碼:


public class Main {
	static{
		System.out.println("我是靜態程式碼塊");
	}
	
	{
		System.out.println("我是例項程式碼塊");
	}
	
	public static void main(String[] args) {
		Main main1=new Main();
		Main main2=new Main();
	}
}

複製程式碼

各位小夥伴,這段程式碼run起來之後會是什麼樣的結構?這裡就不賣關子了,直接貼結果。

Java類載入機制,這篇大概、也許、可能就夠了

OK,如果小夥伴們,知道這個結果,並且也理解這個結果,那麼接下來的內容就可以跳過啦。如果有疑問的話,那就讓我們帶著這個答案,往下看,內容很少,。重在理解~


Java類載入機制

先看一下概念

虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。(來源《深入理解Java虛擬機器.第二版》以下簡稱為深入JVM)

概念總是枯燥的,讓我們開始對這個概念進行一些便於理解的分析和梳理。

Java類載入機制,這篇大概、也許、可能就夠了

梳理

1、載入

首先是載入階段:此階段是Java將位元組碼(.class檔案)資料從不同的資料來源(我們的jar檔案、class 檔案,甚至是網路資料來源等;只要結構正確即可)讀取到JVM中,並對映為JVM認可的資料結構(Class 物件,可以理解成就是java.lang.Class) 。 按照《深入JVM》的描述,這個過程有三步(有部分用詞的加工):

  • 1、通過一個類的全限定名來獲取定義此類的二進位制位元組流。(也就是先通過路徑找到這個類的.class檔案)
  • 2、將這個.class位元組流所代表的的程式碼結構轉化為方法區的執行時資料結構。(可以理解為此時已經在虛擬機器中成了能夠被識別的程式碼結構)
  • 3、在記憶體生成能夠代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的入口。(這裡很不好理解,我對此的理解是:雖然程式碼結構已經存在,但是我們沒有辦法直接去使用。因此這裡抽象出了java.lang.Class物件作為方位.class檔案位元組碼的介面。)

在這裡,我曾經有些疑惑那就是位元組碼和二進位制檔案。其實都是.class檔案,我們簡單編譯一下上述的Main.java

Java類載入機制,這篇大概、也許、可能就夠了

首先我們移動到Main所在的目錄,編譯並檢視.class的位元組碼。

Java類載入機制,這篇大概、也許、可能就夠了
ok,這是位元組碼,如果我們使用一些文字編輯器,比如Sublime,我們看到就是二進位制形式的檔案內容:

Java類載入機制,這篇大概、也許、可能就夠了

Tips:載入階段,我們可以自定義類載入器,去實現自己的類載入過程。

2、連線

連線和載入過程是交叉進行的,也就是說載入階段沒有完成,連線階段可能就已經開始了。

第二階段是連線 ,這是核心的步驟,簡單說是把原始的類定義資訊平滑地轉化入JVM執行的過程中。這裡可進一步細分為三個步驟:

  • 1、驗證 :這是虛擬機器安全的重要保障,JVM 需要核驗位元組資訊是符合Java虛擬機器規範的,驗證階段有可能觸發更多class的載入。
  • 2、準備:建立類或介面中的靜態變數,並初始化靜態變數的初始值。這裡的初始化重點在於分配所需要的記憶體空間,不會進行賦值,也就是說這裡初始化的值是預設值,比如public static int value = 666,此時的value等於0,而非666。而真正的賦值操作在初始化階段。
  • 3、解析:在這一步會將常量池中的符號引用(symbolic reference)替換為直接引用。這段很短的文字我理解了很久,因為它包含了大量的概念:常量池、符號引用、直接引用。接下來逐條解釋:

常量池:常量池裡除了String物件,final型別的常量,還有符號引用符號引用:用於描述位元組碼檔案中各欄位,各方法、各介面等。我是這麼理解的:如果欄位、方法都是想象想要旅遊的遊客的話,那麼符號引用就是旅遊公司,但是旅遊公司只負責收錢組織遊客,他們不負責真正帶遊客出去玩,真正帶他們去玩的是導遊(直接引用)。也就是說符號引用就是一個能夠代表所有欄位、方法的這麼一個角色。 直接引用:直接引用想到於能夠找到對應記憶體地址的角色,也就是上述例子中的導遊。


PS:不知道這麼解釋能不能理解解析的過程,如果還是迷糊,可以檢視知乎大佬對此的專業回答:https://www.zhihu.com/question/30300585

3、初始化

最後是初始化階段 , 這一步開始執行靜態欄位賦值的動作,靜態初始化塊內的邏輯,編譯器在編譯階段就已經把該執行的程式碼邏輯整理好了,這裡需要注意的是:父類的初始化邏輯優先於子類的邏輯。

這裡有一個細節需要注意:靜態程式碼塊中只能訪問到定義在靜態程式碼塊之前的變數。如果此靜態變數在靜態程式碼塊後邊,那麼靜態程式碼塊裡只能對其賦值,不可訪問:

Java類載入機制,這篇大概、也許、可能就夠了


載入結束

一直走到這,我們的類正式載入完畢,也是生成了我們對應的Class物件。但是請留意,這裡還沒有涉及到類的例項化,也就是說此時還沒有開始new操作。

當執行new的時候,而且類已經經歷過載入,那麼才會執行對應的例項化,比如分配記憶體,執行程式碼塊,構造方法之類的(如果有父類要先對應執行這些內容)。

觸發類載入的操作

  • new關鍵字;get/set一個static變數(final、在編譯期進入常量池的靜態欄位除外);呼叫static方法。
  • 使用反射,如果此類沒有被載入會先進行載入操作。
  • main方法對應的類,會在JVM啟動是載入。
  • 使用一些動態代理方式時。

雙親委派機制

關於雙親委派機制,MDove的文章說的簡單明瞭,其實就是一張圖:

Java類載入機制,這篇大概、也許、可能就夠了

用《深入JVM》的話,解釋一下: 雙親委派模型的工作過程:如果一個類載入器收到了一個類的載入請求時,首先它不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層級的類載入器都是如此。因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求時,子載入器才會嘗試自己去載入。

尾聲

關於類載入的的梳理,到此就結束了,不知道各位小夥伴們有沒有理解呢?如果小夥伴們有自己的理解,或者文中有不當之處,歡迎評論區留言~此致敬禮!

我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,已經我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~

個人公眾號:IT面試填坑小分隊

相關文章