java一個方法調用的虛擬機實現


先隨便寫一個非常簡單的類Test

public class Test{
	private int i;
	public void testMethod(){
		i=i+1;
	}
}

然後被下面這個main方法生成並調用,期間我們通過期間發生的內容,來簡單瞭解一下JVM的內部機制。

public static void main(String[] args) {
		Test test =  new Test();
		test.testMethod();
	}

首先是編譯:

Test類被編譯成一個叫做Test.class的類文件,它的內容以字節碼的形式存在。即Ox0F這樣的形式存在。

這個類文件中依次包含了魔數、常量池、類索引、父類索引、接口索引集合、字段表集合、方法表集合、屬性表集合。

這裏我們來回答幾個問題:

1、對一個方法進行調用,入口地址在哪:常量池中的方法常量,在編譯期間記錄的是符號引用,裏面的nameAndType唯一確定了這個方法對應類中的哪一個,特徵爲參數和方法名,不指向代碼,在編譯期間,虛擬機通過這種方式已經解決了方法重載的問題,關於如何實現覆蓋(override)是運行時實現的。

2、一個方法的屬性、參數類型、返回值類型、名稱等在哪:方法表集合中記錄了所有屬於這個類的方法(不包括繼承來的),常量池中的方法常量中也有一個NameAndType的引用。

3、一個方法的Code在哪:屬性表集合中有一個叫做Code屬性的字段,它的內容是Code表集合,裏面有屬於這個類的各個方法Code(不包括繼承來的方法)。


所以一個方法在一個類中的存在,被三個地方記錄,一個是入口,一個是屬性,一個是code。

在完成編譯之後,幾樣東西已經確定:即main方法的code中,調用方法的指令指向了Test類的常量池中的testMethod方法,已經確定了調用的符號引用。


然後是類加載期間:

加載過程被分爲了5個階段,加載、校驗、準備、解析、初始化。

其中,加載通過classLoader把類加載進了內存中的方法區。方法區包括了類信息和運行時常量區兩個部分,其中class的常量池被加載入運行時常量區,其他信息加載入類信息處。到這裏,我們跟蹤一下:

1、調用入口在運行時常量池,內容還是符號引用,。

2、方法描述在方法區的類信息區。

3、code在方法區的屬性表集合的code屬性的code屬性表中。

此時,與上一步沒有本質的變化。


然後是校驗、準備、解析。每一步的功能可以查閱其他資料。

這裏解析這一步比較關鍵,它將常量池中的符號引用替換爲了真實的內存引用。但是注意了,這裏並沒有把所有的方法的符號引用都替換掉。這裏被替換掉的符號引用有:

靜態方法、私有方法、實例構造器方法、父類方法四類。他們有個共同特點:唯一確定,沒有override。

所以這裏的testMethod沒有被解析,還是一個符號引用。

而如果有構造方法,那麼在這裏已經被解析了。

這樣做實現了重載。因爲方法到這一步的時候,並不知道他的實際調用對象是誰,只知道他的靜態類型是Test,可能是Test,也可能是test的子類,如果是test的子類,事實上可能指向子類重載的方法,也可能還是指向父類的方法(子類沒有重載)。這就涉及到運行是的動態分派了。

最後初始化,這時沒有發生太大的根方法有關的變化。

針對testMethod,繼續跟蹤一下,發現沒有變化。

但如果假設他是靜態方法,那麼會是這樣:

1、入口在運行是常量池,內容是直接引用,指向code所在的內存(這裏可能不準確,實際可能還要帶上方法信息)。

2、其他內容位置不變。


最後是運行。

當運行到方法被調用的時候,這個時候就需要獲得這個類調用對象的信息了:


分析這裏的test,得到他的類型爲Test。同時根據調用信息,得到符號引用。

根據符號引用在Test的方法表集合中搜索符合符號引用的方法,如果找到了,則將調用地址指向Test中的testMethod的code。否則依次在父類中查找,找到則指向那個方法。如果查找失敗,則拋異常。通過這個方式實現了多態性。

隨後根據動態指向的code中的信息,生成棧幀。這裏需要根據code得到max_locals、max_stacks數據,這些數據是在編譯時計算好放在code屬性中的。

到這裏,就完成了一次方法的調用。由於涉及到jvm的知識點較多,閱讀前需要先做了解,如有錯誤,請指正。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章