1. 程式人生 > >一個java程式是怎樣執行起來的(3)

一個java程式是怎樣執行起來的(3)

接上一篇 一個java程式是怎樣執行起來的(2),在jvm建立好後,就可以開始執行程式了。我們知道,程式執行的入口在main函式,所以我們首先得找到main函式,這得有個前提,main函式對應的類已經被jvm載入了,所以jvm做的第一件事就是去載入類。先來看下java類載入的機制,主要有以下幾個階段:

1,載入:

載入階段可以參考java.lang.ClassLoader中loadClass方法,採用的是雙親委託進位制進行載入,這個階段首先找到對應的class檔案,以二級制方式讀入記憶體,按照jvm規範解析出所表達的資料結構,在記憶體中生成一個代表該類的java.lang.Class物件.

2,驗證:

驗證是確保當前class檔案格式符合jvm規範,不會對jvm產生危害。驗證工作並不是在載入之後才開始的,比如從class檔案讀入到記憶體後,解析其代表的資料結構時,我們首先會去校驗魔數是否正確,以及版本號是否符合要求等

3,準備

準備階段主要是為類的靜態變數分配記憶體,設定初始值等工作

4,解析

常量池中的符號引用替換為直接引用,比如String str = "test",str指向常量池中"test"的地址

5,初始化

這個過程主要是執行類構造器的方法,靜態類的賦值,靜態程式碼塊的執行。如果初始化一個類時,發現父類還沒有初始化,則需要先初始化父類

根據一個java程式是怎樣執行起來的(1),類載入完成後,就可以找到main方法了,這時就可以開始執行main方法中的jvm指令了,下面以一個例子來解釋其執行過程。

測試程式碼如下:

public class TestAdd{
	public static void main(String[] args){
		int a = 1;
		int b = 2;
		int c = a+b;
		print(c);
	}
	
	public static void print(int c){
		System.out.println(c);
	}
}
javac編譯後,利用命令javap -c TestAdd,我們來看下在執行時究竟執行了哪些jvm指令
public class TestAdd {
  public TestAdd();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return


  public static void main(java.lang.String[]);
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iload_2
       6: iadd
       7: istore_3
       8: iload_3
       9: invokestatic  #2                  // Method print:(I)V
      12: return


  public static void print(int);
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: iload_0
       4: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
       7: return
}
建構函式先不看,直接看main方法。函式的執行是在棧幀中執行的,執行的時候由程式計數器記錄當前執行到哪個位置。棧幀存放在stack中,只有stack頂的棧幀當前有效,裡面存放了本地變量表,運算元棧,返回地址等,本地變量表的大小,以及從運算元棧的深度在編譯時就已經確定,執行時不會改變,如下圖:


他們之間的呼叫關係是棧幀1的函式呼叫了棧幀2中的函式,棧幀2中國的函式呼叫了棧幀3中的函式。有了這個基礎,接下來看下上面的指令是如何執行的,入口在main方法,此時分配新的棧幀我把它標記為棧幀1,棧幀1處在stack頂,即為當前棧幀,執行main方法中jvm指令之前,棧幀1中的有關資料結構如下,本地變量表index為0變數存放的是函式的引數args:


執行指令iconst_1,將int型別數字1push到運算元棧,此時棧幀1的資料結構為:


執行指令istore_1,將棧幀1中運算元棧執行退棧操作,所得的值放入到本地變量表第1個變數中,此時棧幀1的資料結構:


iconst_2,istore_2與上面同理,執行後棧幀:


iload_1和iload_2分別把本地變量表中第一個和第二個變數的值壓入到運算元棧,


iadd指令,連續兩次運算元棧執行退棧操作,將所得的值相加得到結果3再次壓入運算元棧

istore_3,將運算元棧棧頂元素退棧,所得的值存入第3個本地變數中


iload_3,將本地變量表中第三個變數的值壓入運算元棧中

invokestaic 呼叫靜態方法,此處存在方法呼叫,需要新開闢一個棧幀壓入stack,並把變數值3入本地變量表,其返回地址為main方法中即將執行的指令return的地址,這個暫按照指令集的排列,標記為frame1_12吧,此時當前棧幀為棧幀2,執行時資料結構為:


接下來看下print方法的執行,

getstatic獲取指定的field,

iload_0,將變數0的值3壓入運算元棧,此時資料結構為:


invokevirtual執行列印方法,會新開闢一個棧幀棧幀3,將變數值3入棧幀3的本地變量表,其返回地址為frame2_7,執行return後,當前棧幀棧幀3退棧,棧幀2變成當前棧幀,發現當前執行的指令為return執行退棧操作,當前棧幀為棧幀1,此時棧幀1要執行的指令為return,退棧,至程式結束退出。

至此,把java程式的執行過程簡單過了一遍,過程非常粗糙,我目前對jvm的瞭解有限,後續有更好更深入的理解後,再回過頭來豐富下。