一個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指令
建構函式先不看,直接看main方法。函式的執行是在棧幀中執行的,執行的時候由程式計數器記錄當前執行到哪個位置。棧幀存放在stack中,只有stack頂的棧幀當前有效,裡面存放了本地變量表,運算元棧,返回地址等,本地變量表的大小,以及從運算元棧的深度在編譯時就已經確定,執行時不會改變,如下圖: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 }
他們之間的呼叫關係是棧幀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的瞭解有限,後續有更好更深入的理解後,再回過頭來豐富下。