JVM動態方法棧內存分配

 

目錄

運行時動態計算棧大小

Java類型實例訪問方式

參數在方法棧中的存在方式

生成機器指令

統計棧空間大小


      上一篇日誌最後寫了JVM在函數內部調用其它函數時棧空間的分配方式,也就是調用者函數和被調用者函數兩者的方法棧模型,是一種對接的方式,即被調用者分配的方法棧空間,會對接在調用者方法棧棧頂處,這樣被調用者函數可以通過ebp寄存器很方便找到調用者函數中壓棧的參數,這是方法棧分配方式問題,不過還有一個問題是,JVM怎麼知道要爲函數方法棧分配多大的空間呢?你可能會想,像C那樣,編譯器在編譯程序時期,通過對函數的入參,和局部變量的聲明來計算出需要分配的棧空間大小,然後爲其分配棧空間,這是一種方法。得益於C語言可以被編譯器直接編譯成機器碼,讓機器運行,所以計算機可以知道該程序中有多少個變量,它們都是什麼類型,一共需要多大的空間,但Java不同,前面日誌說過,Java程序因爲跨平臺性,運行時需要翻譯成一種中間語言-字節碼,再有JVM來決定哪些部分是解釋執行,哪些部分是編譯執行(JVM裏的JIT即時編譯器)。也就是說Java的變量類型不能直接被編譯成機器碼,Java面向對象編程語言有些自己獨特的類型,例如類,對象,所以無法像C那樣方便地直接編譯成機器能讀懂的機器碼,從而在運行前計算出調用函數所需要的棧空間大小。

 

運行時動態計算棧大小

      JVM運行時將Java程序加載進來,但是由於計算機並不能讀懂Java代碼,無法執行方法(或者說函數),所以,JVM通過call_stub()函數,其返回的CallStub函數指針,來執行Java方法,並將函數傳遞進去,前面也說過,Java主函數就必須通過call_stub()來執行,call_stub()函數的執行鏈中最後調用generate_call_stub()函數來初始化,得到需要執行函數的首地址,返回給_call_stub_entry變量。主函數需要的參數String[ ] args就是放在了generate_call_stub()函數的方法棧中。舉一個簡單的例子來說明,假設main()函數裏調用了run()函數,run()函數需要兩個參數a和b,那麼該兩個參數在main()函數中就需要完成壓棧,保存在了main()函數的方法棧內,而不是run()函數的方法棧裏,其實也很好理解,run()函數需要的參數在其運行前就要被保存好,因爲運行前JVM不會爲run()函數分配棧空間,只有當函數被調用後,被調用函數纔會得到空間分配,然後從調用者函數的壓棧中獲得需要的參數。

JVM執行最開始的主函數時也是如此,

public static void main(String[ ] args) {

      …………………

}

      參數String[ ] args被保存在了generate_call_stub()函數的方法棧中,不過在保存參數之前,爲了能知道函數調用需要多大的棧空間分配,該初始化函數還需要做一些事情,就是運行時動態計算參數的個數,需要的空間大小,在generate_call_stub()函數中對應的代碼部分如下:

address generate_call_stub(address& return_address) {
	StubCodeMark mark(this, "StubRoutines", "call_stub");
    address start = __ pc(); // 當前函數的入口地址
	
	assert(frame::entry_frame_call_wrapper_offset == 2, "adjust this code");
	bool sse_save = false;
	const Address rsp_after_call(rbp, -4 * wordSize);
	const int locals_count_in_bytes (4 * wordSize);
	const Address mxcsr_save (rbp, -4 * wordSize);
	
	// 部分源碼省略....
	_enter();
	_movptr(rcx, parameter_size);
	_shlptr(rcx, Interpreter::logStackElementSize);
	_addptr(rcx, locals_count_in_bytes);
	_subptr(rsp, rcx);
	_andptr(rsp, -(StackAlignmentInBytes));
	
	// 下面源碼省略....
}

      標亮部分就是JVM在對Java程序中函數的入參進行計算,可以看到,只需要傳入parameter_size,即入參數量,JVM就可以統計出函數所需要的棧空間大小。雖然不同的數據類型所佔的空間大小不同,例如int型佔4個字節,char型佔1個字節,JVM需要知道每一種類型的空間大小,才能進行累加求和,在Java程序中除了基本數據類型,還有一些類的實例對象這樣的數據類型,即使各個參數類型大小不同,JVM還是能做到統計需要的棧空間大小。還記得前面日誌有總結到,JVM會爲每一個Java類型對象建立內存模型,在CallStub函數指針中需要的八個參數裏,其中一個method()參數,它做的事就是爲當前調用的Java方法在JVM內部建立函數模型,模型裏包含有被調用函數的方法名,入參類型,入參數量和編譯後的字節碼指令等,使得JVM可以在程序運行時 動態獲取類和對象的信息。

      總的來說,JVM知道各種Java基本類型的大小,還爲各種類型的實例對象建立了內存模型,這樣就可以知道每一種變量所佔空間大小,最後根據參數數量統計出總的方法棧空間。

 

Java類型實例訪問方式

參數在方法棧中的存在方式

      有一點要注意的是,JVM的棧內存模型中,存放的是變量的引用,也就是指針,而不是數據,這個在前面的日誌裏也提到了一下,這樣做的好處是在被調用者函數中對入參進行尋址時很方便,可以通過寄存器地址+偏移量的方式找到需要的參數的位置,因爲方法棧中存放的就是地址。Java類型變量,例如類的實例對象中的成員變量和方法,都是這樣的訪問方式,通過指針+偏移量的方式訪問讀取,同樣,JVM在函數間傳參,傳的也是指針。因爲JVM這種傳遞引用(或者說指針)的方式,不管是int*型還是char*型變量,它們的寬度都是一樣的,32位系統下指針寬度就是4個字節,64位系統下寬度就是8個字節,即使是結構體類型的指針變量,它們的大小都是統一的,這也是爲什麼JVM在統計函數需要的棧空間時,只需要知道入參數量就可以的原因。

 

生成機器指令

回到generate_call_stub()函數中,先把圖片再貼上來一次:

      parameter_size在函數棧頂往上偏移32位(4個字節)處的地方,對應的代碼部分:

_movptr(rcx, parameter_size);

_shlptr(rcx, Interpreter::logStackElementSize);

      第一句的意思是將parameter_size參數的值傳到rcx寄存器中(32位系統下的ecx寄存器在64位下擴展爲rcx,ecx寄存器用來保存臨時變量),如果把第一句翻譯成彙編:

movl 0x20(%ebp), %ecx

      意思是將ebp寄存器往上0x20,十六進制換成十進制就是32,往上偏移32個字節處的地方,對應上面的圖,即4個字節處,parameter_size參數的值,放到ecx寄存器中,留意parameter_size的值表示參數的個數。第二句代碼翻譯成彙編如下:

shl $0x2, %ecx

      意思是將ecx寄存器中的值往左移兩位,也就是乘以4,在32位系統下每個指針寬度爲4個字節(64位系統下位8個字節,那麼就是乘以8,ecx寄存器的值是往左移三位),參數個數乘以指針寬度,最後計算出了函數總的需要的棧空間大小。

 

統計棧空間大小

      繼續跟着generate_call_stub()函數往下走,計算完需要的棧空間大小後,下一條語句是:

_addptr(rcx, locals_count_in_bytes);

      它的作用是保存調用者函數的現場,即在進入函數之前保存函數的基地址,具體的做法是保存一些寄存器的值,因爲這部分我當初沒有看透徹,所以不詳細展開,要注意的是保存現場,也就是保存一些寄存器的值,這裏需要的空間也是算進方法棧空間裏的,所以最後總的方法棧空間大小是入參所以需要的空間+保存現場寄存器佔用的空間。通常在32位系統下一個寄存器也是4個字節,最後,申請分配方法棧空間:_subptr(rsp, rcx);  

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