Java虛擬機如何把編譯好的.class文件加載到虛擬機裏面?加載以後如何初始化類?靜態類變量和實例類變量的初始化過程是否相同,分別是如何初始化的呢?這篇文章就是解決上面3個問題的。html
本文前面理論部分比較枯燥,可是若是耐心讀完,結合後面的實例,我相信你之後絕對不會再遇到java類初始化這樣的疑惑。如有不正之處,請多多諒解並歡迎各位可以給予批評指正,提早謝謝各位。java
1. Java虛擬機加載.class過程shell
虛擬機把Class文件加載到內存,而後進行校驗,解析和初始化,最終造成java類型,這就是虛擬機的類加載機制。加載,驗證,準備,初始化這5個階段的順序是肯定的,類的加載過程,必須按照這種順序開始。這些階段一般是相互交叉和混合進行的。解析階段在某些狀況下,能夠在初始化階段以後再開始---爲了支持java語言的運行時綁定。Java虛擬機規範中,沒有強制約束何時要開始加載,可是,卻嚴格規定了幾種狀況必須進行初始化(加載,驗證,準備則須要在初始化以前開始):編程
1)遇到 new、getstatic、putstatic、或者invokestatic 這4條字節碼指令,若是沒有類沒有進行過初始化,則出發初始化緩存
2)使用java.lang.reflect包的方法,對壘進行反射調用的時候,若是沒有初始化,則先觸發初始化安全
3)初始化一個類時候,若是發現父類沒有初始化,則先觸發父類的初始化
數據結構
在《java編程思想》中對這段也有描述,只不過沒有這麼詳細的描述,它描述的角度是咱們平時應用的角度。多線程
2. 加載,驗證,解析jvm
加載就是經過指定的類全限定名,獲取此類的二進制字節流,而後將此二進制字節流轉化爲方法區的數據結構,在內存中生成一個表明這個類的Class對象。驗證是爲了確保Class文件中的字節流符合虛擬機的要求,而且不會危害虛擬機的安全。加載和驗證階段比較容易理解,這裏就再也不過多的解釋。解析階段比較特殊,解析階段是虛擬機將常量池中的符號引用轉換爲直接引用的過程。若是想明白解析的過程,得先了解一點class文件的一些信息。class文件採用一種相似C語言的結構體的僞結構來存儲咱們編碼的java類的各類信息。其中,class文件中常量池(constant_pool)是一個相似表格的倉庫,裏面存儲了咱們編寫的java類的類和接口的全限定名,字段的名稱和描述符,方法的名稱和描述符。在java虛擬機將class文件加載到虛擬機內存以後,class類文件中的常量池信息以及其餘的數據會被保存到java虛擬機內存的方法區。咱們知道class文件的常量池存放的是java類的全名,接口的全名和字段名稱描述符,方法的名稱和描述符等信息,這些數據加載到jvm內存的方法區以後,被稱作是符號引用。而把這些類的全限定名,方法描述符等轉化爲jvm能夠直接獲取的jvm內存地址,指針等的過程,就是解析。虛擬機實現能夠對第一次的解析結果進行緩存,避免解析動做的重複執行。在解析類的全限定名的時候,假設當前所處的類爲D,若是要把一個從未解析過的符號引用N解析爲一個類或者接口C的直接引用,具體的執行辦法就是虛擬機會把表明N的全限定名傳遞給D的類加載器去加載這個類C。這塊可能不太好理解,可是咱們能夠直接理解爲調用D類的ClassLoader來加載N,而後就完成了N--->C的解析,就能夠了。ide
3. 準備階段
之因此把在解析階段前面的準備階段,拿到解析階段以後講,是由於,準備階段已經涉及到了類數據的初始化賦值。和咱們本文講的初始化有關係,因此,就拿到這裏來說述。在java虛擬機加載class文件而且驗證完畢以後,就會正式給類變量分配內存並設置類變量的初始值。這些變量所使用的內存都將在方法區分配。注意這裏說的是類變量,也就是static修飾符修飾的變量,在此時已經開始作內存分配,同時也設置了初始值。好比在
Public static int value = 123 這句話中,在執行準備階段的時候,會給value 分配內存並設置初始值0, 而不是咱們想象中的123. 那麼何時 纔會將咱們寫的123 賦值給 value呢?就是咱們下面要講的初始化階段。
4. 初始化階段
類初始化階段是類加載過程的最後階段。在這個階段,java虛擬機才真正開始執行類定義中的java程序代碼。Java虛擬機是怎麼完成初始化的呢?這要從編譯開始講起。在編譯的時候,編譯器會自動收集類中的全部靜態變量(類變量)和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是根據語句在java代碼中的順序決定的。收集完成以後,會編譯成java類的 static{} 方法,java虛擬機則會保證一個類的static{} 方法在多線程或者單線程環境中正確的執行,而且只執行一次。在執行的過程當中,便完成了類變量的初始化。值得說明的是,若是咱們的java類中,沒有顯式聲明static{}塊,若是類中有靜態變量,編譯器會默認給咱們生成一個static{}方法。 咱們能夠經過javap -c 的命令,來看一下java字節碼中編譯器爲咱們生成或者合併的static{} 方法:
public class StaticValInitTest { public static int value = 123; }
上面咱們講述的是單類的狀況,若是出現繼承呢?若是有繼承的話,父類中的類變量該如何初始化?這點由虛擬機來解決:虛擬機會保證在子類的static{}方法執行以前,父類的static{}方法已經執行完畢。因爲父類的static{}方法先執行,也就意味着父類的靜態變量要優先於子類的靜態變量賦值操做。
上面講的都是靜態變量,實例變量怎麼解決呢?實例變量的初始化,實際上是和靜態變量的過程是相似的,可是時間和地點都不一樣哦。咱們如下面的Dog類爲例來說一講。
public class Dog { public String type = "tai di"; public int age = 3; }
1. 當用new Dog() 建立對象的時候,首先在堆上爲Dog對象分配足夠的空間。
2. 這塊存儲空間會被清零,這就是自動將Dog對象中的全部基本類型的數據都設置成了默認值,而引用類型則被設置成了null(相似靜態類的準備階段的過程)
3. Java收集咱們的實例變量賦值語句,合併後在構造函數中執行賦值語句。沒有構造函數的,系統會默認給咱們生成構造函數。
至此,java類初始化的理論基礎已經完成了,其中的大部分的理論和思想都出自《深刻理解java虛擬機》這本書。有了以上的理論基礎,再複雜的類初始化的狀況,咱們均可以應對了,下面就拿一個例子作一個具體的分析吧
public class Insect { private int i = 9; protected int j; protected static int x1 = printInit("static Insect.x1 initialized"); Insect() { System.out.println("基類構造函數階段: i = " + i + ", j = " + j); j = 39; } static int printInit(String s) { System.out.println(s); return 47; } } public class Beetle extends Insect { protected int k = printInit("Beetle.k initialized"); protected static int x2 = printInit("static Beetle.x2 initialized"); public static void main(String[] args) { Beetle b = new Beetle(); } }
上面例子來自《java編程思想》,以上代碼的執行結果是什麼呢?若是對上面咱們講的理論理解的話,很容易就知道結果是:
static Insect.x1 initialized
static Beetle.x2 initialized
基類構造函數階段: i = 9, j = 0
Beetle.k initialized
具體的執行結果過程是:在執行Beetle 類的 main方法的時候,由於該main方法是static方法,咱們在上面已經知道,在執行類的static方法的時候,若是該類沒有初始化,則要進行初始化,所以,咱們在執行main方法的時候,會執行加載--驗證--準備--解析---初始化這個過程。在進行最後的初始化的時候,又有一個約束:虛擬機會保證在子類的static{}方法執行以前,父類的static{}方法已經執行完畢。因此,在執行完解析以後,會先執行父類的初始化,在執行父類初始化的時候,輸出: static Insect.x1 initialized
而後接着初始化子類,輸出:static Beetle.x2 initialized
以上兩行輸出,是靜態變量的初始化,是在第一次調用靜態方法,即,在執行new、getstatic、putstatic、或者invokestatic 這4條字節碼指令時候觸發的。因此,你若是把上例中的static main 方法中的 Beetle b = new Beetle(); 註釋掉,上面兩行仍然會輸出出來。
而後就是執行Beetle b = new Beetle();這句代碼了。
咱們知道,在實例化子類對象的時候,會自動調用父類的構造函數。因此,接着就輸出:
基類構造函數階段: i = 9, j = 0
緊接着是執行本身的構造函數,在堆上建立類實例對象,實例對象空間清零,而後執行賦值語句k = printInit("Beetle.k initialized");
輸出: Beetle.k initialized
至此,整個類加載並初始化完畢,是否是理解起來就很簡單了,趁勝追擊,咱們仍是再來看一個例子吧:
public class Base { Base() { preProcess(); } void preProcess() { } } public class Derived extends Base { public String whenAmISet = "set when declared"; @Override void preProcess() { whenAmISet = "set in preProcess"; } public static void main(String[] args) { Derived d = new Derived(); System.out.println(d.whenAmISet); } }
這個例子來源與@左耳朵耗子的博客,我就是看了他博客中的這個例子,纔想起來寫篇文章裏理順下java類初始化的過程,纔有了這篇博客。廢話很少說,這個例子裏面,有一個地方比較繞:父類在執行構造函數的時候,調用了子類(導出類)重載過的方法,在子類的重載方法中,給實例變量作了一次賦值,正是此次賦值,干擾了咱們對類初始化的理解。
咱們無論類裏面是怎麼作的,還按照咱們上個例子中那樣進行分析:
1. 執行Derived 類 static main 方法的時候,執行類變量初始化,可是此例中父類和子類都沒有類變量,因此此步驟什麼都不作,進行實例變量初始化
2. 執行new Derived()的時候,先調用了父類的構造函數,由於子類的重載,調用了子類的preProcess方法,爲實例變量whenAmISet 賦值爲"set in preProcess"
3. 而後執行子類Derived 的構造函數,在構造函數中,有編譯器爲咱們收集生成的實例變量賦值語句,最終,又將實例變量whenAmISet 賦值爲"set when declared"
4. 因此最終的輸出是: set when declared
若是對這個還不太理解的話,能夠再Derived 類裏面添加註釋,改爲下面的樣子,輸出看看,是否是對這個執行過程更清晰了呢?
public class Derived extends Base { // 準備階段賦值 whenAmISet=null public String whenAmISet = "set when declared"; public Derived() { System.out.println("do son constructor"); } @Override void preProcess() { System.out.println("do son process"); System.out.println("whenAmISet:" + whenAmISet); whenAmISet = "set in preProcess"; System.out.println("whenAmISet:" + whenAmISet); System.out.println("set in preProcess end"); } public static void main(String[] args) { Derived d = new Derived(); System.out.println(d.whenAmISet); } }
參考資料:
《深刻理解java虛擬機》
《Java編程思想》
http://coolshell.cn/articles/1106.html
看博客的時候沒以爲什麼,寫博客的時候才知道還要組織語言,還要排版神馬的,因此還但願轉載的時候能給個連接說明,謝謝
本文先發布在博客園http://www.cnblogs.com/jimxz/p/3974939.html,後面瀏覽的人很少,我又轉到這裏。