JVM 之類載入

zzzzMing發表於2018-09-25

一.概述

Java不同於C/C++這類傳統的編譯型語言,也不同於php這一類動態的指令碼語言。可以說Java是一種半編譯語言,我們所寫的類會先被編譯成.class檔案,這個.class是一串二進位制的位元組流。然後當要使用這個類的時候,就會將這個類對應的.class檔案載入進記憶體中。而將這個.class的內容載入進記憶體,正是通過Jvm類載入機制實現的。

虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗,轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。

二.類載入的各個步驟

載入

載入時“類載入”過程的第一步,在載入過程中,虛擬機器需要完成以下三件事

  1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流。
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
  3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區的這個類的各種資料的訪問入口。

值得一提的是,在載入階段既可以使用系統提供的引導類載入器來完成,也可以由使用者自定義的類載入器來完成,相對而是比較自由的,但對於陣列則不是這樣了,陣列類本身不通過類載入建立,它是由Java虛擬機器直接建立的。但資料所存放的元素型別是需要類載入器去建立的。

載入階段與下一階段的連線部分是交叉進行的,但載入階段和連線階段的開始時間仍然會保持固定的先後順序。

驗證

驗證時連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊複合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。雖然說陣列越界,將物件胡亂轉型這些操作會被編譯器拒絕編譯,但.class檔案並不一定要求從Java原始碼編譯而來,可以從其他途徑產生,故而需要對.class檔案的二進位制流進行驗證。

驗證階段的重要性是不言而喻的,這一階段是否嚴謹,直接決定了Java虛擬機器是否能承受惡意程式碼的攻擊,從執行效能的角度上講,驗證階段的工作量在虛擬機器的類載入系統中又佔了相當大的一部分。

從整體上看,驗證階段大致可分為4部分的檢驗動作:檔案格式驗證,後設資料驗證,位元組碼驗證,符號引用驗證。

  • 符號驗證:主要目的是保證輸入的位元組流能正確地解析並儲存於方法區之內,格式上符合描述一個Java型別資訊的要求。這一部分是基於二進位制流驗證的,之後會載入到記憶體中,後續驗證是在記憶體中驗證。
  • 後設資料驗證:這一驗證主要是對類的後設資料資訊進行語義校驗,保證不存在不符合Java語言規範的後設資料資訊。
  • 位元組碼驗證:這一部分是驗證階段中最複雜的一階段,主要目的是通過資料流和控制流分析,確定程式是合法的,符合邏輯的。
  • 符號引用驗證:符號引用是發生在虛擬機器將符號引用轉化為直接引用的時候,目的是卻好解析動作能正常執行。

準備

準備階段是為正式類變數(靜態變數)分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都講在方法區中進行分配的。值得一提的是,這時候進行分配的僅為類變數(靜態變數),而不包括例項變數。

通常情況下,設定類變數初始值,這個初始值指的是資料型別的預設值,比如int型則是0。但若類變數被final修飾,則情況又不一樣,那樣的話會直接對給定值進行賦值。

解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。這裡解釋以下什麼是符號引用,什麼是直接引用。

符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義得定位到目標即可。

直接引用:直接引用可以是指向目標的指標,相對偏移量或是一個能間接定位到目標的控制程式碼。

解析動作主要針對類或介面,欄位,類方法,介面方法,方法型別,方法控制程式碼和呼叫點限定符7類符號引用進行。

初始化

類初始化階段是類載入過程的最後一步,前面的類載入過程,除了在載入階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才會真正開始執行類中定義的Java程式碼。

在準備階段,變數已經賦過一次系統要求的初始值,而在初始化階段,則根據程式設計師通過程式制定的計劃區初始化類變數和其他資源。

三.有意思的程式碼段

public class StaticTest
 {

     public static void main(String[] args)
     {
         staticFunction();
     }
  
     static StaticTest st = new StaticTest();
  
     static
     {
         System.out.println("1");
     }
  
     {
         System.out.println("2");
     }
  
     StaticTest()
     {
         System.out.println("3");
         System.out.println("a="+a+",b="+b);
     }
  
     public static void staticFunction(){
         System.out.println("4");
     }
  
     int a=110;
     static int b =112;
 }

這段程式碼的執行結果是什麼呢?

答案是:

2

3

a=110,b=0

1

4

這是為什麼呢,大家不妨思考以下。

理解這段程式碼不光是要明白Java的類載入機制,還需要明白初始化階段,靜態程式碼塊與靜態成員變數的初始化順是與程式碼順序有關的。

類載入的過程是:裝載–>連線(驗證,準備,解析)–>初始化。

1.在準備階段,會為類變數設定預設值,所以在案例一中:st=null,b=0,

2.在初始化階段,會先執行類構造器,

換句話說,就是執行static修飾的程式碼塊和為static修飾的變數賦值而已。而static修飾的程式碼塊和類變數的執行順序是按照它在檔案中的先後順序執行的。而static StaticTest st = new StaticTest()排在第一,所以會執行 new StaticTest(),也就是進行物件的初始化

2.1.在物件的初始化過程中,會先執行成員變數(程式碼塊),然後再執行構造方法.成員變數的執行順序也是誰先宣告,誰先執行,所以排在第一的程式碼塊

2.2成員變數執行完後,執行構造方法.此時,a=110,b=0;

3.由static StaticTest st = new StaticTest();觸發的非靜態程式碼的初始化過程到此結束,接下來繼續執行靜態程式碼的初始化,於是輸出 1 。

4.整個類載入到此結束,執行程式碼,輸出 4 。

再看下一道

public class StaticTest
 {

     public static void main(String[] args)
     {
         staticFunction();
     }
  
  
     static
     {
         System.out.println("1");
     }
  
     {
         System.out.println("2");
     }
  
     StaticTest()
     {
         System.out.println("3");
         System.out.println("a="+a+",b="+b);
     }
  
     public static void staticFunction(){
         System.out.println("4");
     }
  
     int a=110;
     static int b =112;
     static StaticTest st = new StaticTest();  //將這條語句放到最下面
 }

僅僅是改變一條語句,而這段程式碼的執行結果是

1

2

3

a=110,b=112

4

大家不妨運用上面的知識,想想是為什麼。


推薦閱讀:
大資料儲存的進化史 --從 RAID 到 Hdfs
貝葉斯分類演算法例項 --根據姓名推測男女
從分治演算法到 MapReduce

相關文章