Java虛擬機運行時棧幀結構--《深刻理解Java虛擬機》學習筆記及我的理解(二)

Java虛擬機運行時棧幀結構(周志明書上P237頁)

棧幀是什麼?

棧幀是一種數據結構,用於虛擬機進行方法的調用和執行。java

棧幀是虛擬機棧的棧元素,也就是入棧和出棧的一個單元。編程


2018.1.2更新(在網上看到一個更好的解釋):segmentfault

棧幀(Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態連接 (Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)。數組

棧幀在什麼地方?

內存 -> 運行時數據區 -> 某個線程對應的虛擬機棧 -> 這裏就是棧幀了數據結構

棧幀的含義?

每一個方法的執行和結束對應着棧幀的入棧和出棧。app

入棧表示被調用,出棧表示執行完畢或者返回異常。jvm

一個虛擬機棧對應一個線程,當前CPU調度的那個線程叫作活動線程;一個棧幀對應一個方法,活動線程的虛擬機棧裏最頂部的棧幀表明了當前正在執行的方法,而這個棧幀也被叫作‘當前棧幀’。函數

棧幀既然是個數據結構,都有哪些數據?

局部變量表、操做數棧、動態連接、方法返回地址、附加信息。學習

棧幀的大小是何時肯定的?

編譯程序代碼的時候,就已經肯定了局部變量表和操做數棧的大小,並且在方法表的Code屬性中寫好了。不會受到運行期數據的影響。優化

什麼是局部變量表

 是一片邏輯連續的內存空間,最小單位是Slot,用來存放方法參數和方法內部定義的局部變量。我以爲能夠想成Slot數組....JVMS7:「any parameters are passed in consecutive local variables starting from local variable 0」

虛擬機沒有明確指明一個Slot的內存空間大小。可是boolean、byte、char、short、int、float、reference、returnAddress類型的數據均可以用32位空間或更小的內存來存放。這些類型佔用一個Slot。Java中的long和double類型是64位,佔用兩個Slot。(只有double和long是jvms裏明確規定的64位數據類型)

虛擬機如何調用這個局部變量表?

局部變量表是有索引的,就像數組同樣。從0開始,到表的最大索引,也就是Slot的數量-1。

要注意的是,方法參數的個數 + 局部變量的個數 ≠ Slot的數量。由於Slot的空間是能夠複用的,當pc計數器的值已經超出了某個變量的做用域時,下一個變量沒必要使用新的Slot空間,能夠去覆蓋前面那個空間。(這部份內容在P183頁)

特別地,JVMS7:

On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language)

手動翻譯:在一個實例方法的調用時,局部變量表的第0位是一個指向當前對象的引用,也就是Java裏的this。

局部變量表Slot複用對垃圾收的影響(書上的P239頁)

先了解一下System.gc()機制:

public class Main{
	public static void main(String [] args){
		byte[] placeholder = new byte[64*1024*1024];
		System.gc();
	}
}

對於上面dos輸出的結果,我是這樣理解的:

第一行,Allocation Filure(空間分配失敗)引發了Minor GC。由於建立的對象太大,新生代裝不下,因此進行了一次GC。

第二行,因爲新生代GC完了後,仍是裝不下,這時就應該把它直接放到老年代,爲了老年代又足夠的空間來迎接這個大對象,因此老年代進行一次Full GC。

第三行,是代碼中的手動gc,發現此次手動gc並無回收掉這個大對象。由於,placeholder這個對象,還在做用域....就不應回收....


 這回System.gc()該回收掉placeholder了吧?

public class Main{
	public static void main(String [] args){
		{
			byte[] placeholder = new byte[64*1024*1024];
		}
		System.gc();
	}
}

要不是回收時間不同...還真看不出什麼區別...

明顯,仍是沒有回收掉這個placeholder大對象。

爲何呢?

由於虛擬機並不急着讓placeholder回收掉,由於,在我這個程序中,對虛擬機來講,回不回收placeholder,對內存沒有絲毫影響,剩餘的空間同樣都是浪費(空閒)着,回收了反倒還浪費時間。


 這樣作才能成功回收:

public class Main{
	public static void main(String [] args){
		{
			byte[] placeholder = new byte[64*1024*1024];
		}
		int a = 0;
		System.gc();
	}
}

其實服用以前,雖然placeholder退出了做用域,可是虛擬機並無作什麼事,只是知道pc指針已經超出了placeholder的做用域,知道placeholder過時了。因此placeholder仍保持者GC Roots之間的關聯。

當a=0複用了前面對象的空間時,就打斷了GC Roots與局部變量表中的placeholder之間的關聯。由於a複用了這片空間(雖然只是用了一小部分)。此時GC Root沒法達到placeholder對象,知足回收條件。

而後System.gc()就成功回收了。


也就是說在複用以前並不會斷定爲‘垃圾’,在複用後纔會被斷定爲‘垃圾’。剛纔使用一個int a來複用,這個複用看起來很輕量。

若是使用一個新的大對象來複用,那麼GC是如何發生的呢?看下面代碼:

public class Main{
	public static void main(String [] args)throws InterruptedException{
		{
			byte[] placeholder = new byte[64*1024*1024];
		}
		byte[]arr= new byte[20*1024*1024];
		System.gc();
	}
} 

解讀dos下的輸出:

第一行,由於即將建立的placeholder太大,新生代裝不下,因此進行一次GC。

第二行, 由於GC以後仍是裝不下placeholder,因此把這個大對象直接放進老年代裏。迎接這個大對象以前,先清一清本身的空間(Full GC),怕本身裝不下。

第三行,由於即將建立的arr太大,新生代裝不下,因此進行一次GC。

第四行,由於GC以後仍是裝不下arr, 因此把這個大對象直接放進老年代裏。迎接這個大對象以前,先清一清本身的空間(Full GC),怕本身裝不下。

可是,能夠看到這一次Full GC並無把placeholder清理掉,由於還沒開始複用呢。

隨後建立好了arr, 也就是複用了placeholder的空間。這時才把placeholder斷定爲垃圾。

第五行,是代碼裏手寫的System.gc()方法。這時把placeholder這個垃圾清理掉。


有沒有發現這個Full GC來的不是很恰到好處?由於沒有及時清理掉placeholder。

爲何沒有清理掉呢?由於局部變量表裏的placeholder數據還和GC Root連着,致使沒有斷定它爲垃圾。

能不能及時斷開這個鏈接,讓這個Full GC起到它該起的做用呢?

能夠巧用null來解決,看下面代碼:

public class Main{
	public static void main(String [] args)throws InterruptedException{
		{
			byte[] placeholder = new byte[64*1024*1024];
			placeholder = null;
		}
		byte[]arr= new byte[20*1024*1024];
		System.gc();
	}
}  

解讀dos下的輸出:

第一行,由於即將建立的placeholder太大,新生代裝不下,因此進行一次GC。

第二行, 由於GC以後仍是裝不下placeholder,因此把這個大對象直接放進老年代裏。迎接這個大對象以前,先清一清本身的空間(Full GC),怕本身裝不下。

隨後placeholder= null;

第三行,由於即將建立的arr太大,新生代裝不下,因此進行一次GC。

第四行,由於GC以後仍是裝不下arr, 因此把這個大對象直接放進老年代裏。迎接這個大對象以前,先清一清本身的空間(Full GC),怕本身裝不下。

能夠看到這一次Full GC把placeholder清理掉了。

隨後建立好了arr,複用了placeholder。

第五行,是代碼裏手寫的System.gc()方法。

什麼事

什麼是操做數棧(參考JVMS7)

Each frame (§2.6) contains a last-in-first-out (LIFO) stack known as its operand stack. 

翻譯:每一個棧幀都包含一個被叫作操做數棧的後進先出的棧。叫操做棧,或者操做數棧。

 Where it is clear by context, we will sometimes refer to the operand stack of the current frame as simply the operand stack.

 翻譯:一般狀況下,操做數棧指的就是當前棧楨的操做數棧。

操做數棧有什麼用?

The operand stack is empty when the frame that contains it is created. The Java virtual machine supplies instructions to load constants or values from local variables or fields onto the operand stack. Other Java virtual machine instructions take operands from the operand stack, operate on them, and push the result back onto the operand stack. The operand stack is also used to prepare parameters to be passed to methods and to receive method results.

翻譯+概括:

1.棧楨剛建立時,裏面的操做數棧是空的。

2.Java虛擬機提供指令來讓操做數棧對一些數據進行入棧操做,好比能夠把局部變量表裏的數據、實例的字段等數據入棧。

3.同時也有指令來支持出棧操做。

4.向其餘方法傳參的參數,也存在操做數棧中。

5.其餘方法返回的結果,返回時存在操做數棧中。

操做數棧自己就是一個普通的棧嗎?

其實棧就是棧,再加上數據結構所支持的一些指令和操做。

可是,這裏的棧也是有約束的。

操做數棧是區分類型的,操做數棧中嚴格區分類型,並且指令和類型也好嚴格匹配。

棧楨和棧楨是徹底獨立的嗎?

原本棧楨做爲虛擬機棧的一個單元,應該是棧楨之間徹底獨立的。

可是,虛擬機進行了一些優化:爲了不過多的 方法間參數的複製傳遞、方法返回值的複製傳遞 等一些操做,就讓一部分數據進行棧楨間共享。

什麼是動態連接?

 一個方法調用另外一個方法,或者一個類使用另外一個類的成員變量時,總得知道被調用者的名字吧?(你能夠不認識它自己,但調用它就須要知道他的名字)。符號引用就至關於名字,這些被調用者的名字就存放在Java字節碼文件裏。

名字是知道了,可是Java真正運行起來的時候,真的能靠這個名字(符號引用)就能找到相應的類和方法嗎?

須要解析成相應的直接引用,利用直接引用來準確地找到。

 

舉個例子,就至關於我在0X0300H這個地址存入了一個數526,爲了方便編程,我把這個給這個地址起了個別名叫A, 之後我編程的時候(運行以前)能夠用別名A來暗示訪問這個空間的數據,但其實程序運行起來後,實質上仍是去尋找0X0300H這片空間來獲取526這個數據的。

 

這樣的符號引用和直接引用在運行時進行解析和連接的過程,叫動態連接。

動態連接的前提

每個棧幀內部都要包含一個指向運行時常量池的引用,來支持動態連接的實現。

附加信息

JVMS裏沒看到啊....可是書裏提了,而後說"JVM裏沒有明文規定"....

 

方法返回地址

方法正常調用完成

返回一個值給調用它的方法,方法正常完成發生在一個方法執行過程 中遇到了方法返回的字節碼指令(§2.11.8)的時候,使用哪一種返回指令取決於方法返回值的數 據類型(若是有返回值的話)。

JVMS7中的2.6.4 Normal Method Invocation Completion中寫道:

This occurs when the invoked method executes one of the return instructions (§2.11.8), the choice of which must be appropriate for the type of the value being returned (if any).

手動翻譯+理解:Java虛擬機根據不一樣數據類型有不一樣的底層return指令。當被調用方法執行某條return指令時,會選擇相應的return指令來讓值返回(若是該方法有返回值的話)。

The current frame (§2.6) is used in this case to restore the state of the invoker, including its local variables and operand stack, with the program counter of the invoker appropriately incremented to skip past the method invocation instruction. Execution then continues normally in the invoking method's frame with the returned value (if any) pushed onto the operand stack of that frame.

手動翻譯:在這種狀況,當前棧楨就被用來恢復調用者的狀態,都恢復哪些呢?恢復局部變量表、操做數棧 和 程序計數器(pc指針),而這個程序計數器要適當地增長,來指向下一條指令(也就是調用函數的下一句)。使調用者方法可以正常地繼續執行下去,並且返回值push到了調用方法的操做數棧中。

方法異常調用完成

異常時不會返回值給調用者。

 未完待續。。。這方面我再學習學習。。

 

 

 

參考博客:

英中繁簡編程術語對照http://www.moon-soft.com/doc/30155.htm

http://blog.csdn.net/dd864140130/article/details/49515403

http://blog.csdn.net/u013678930/article/details/51980460

https://www.zhihu.com/question/29056872

https://segmentfault.com/a/1190000010648021

http://blog.csdn.net/captian_900331/article/details/52512204

https://www.zhihu.com/question/53822079/answer/136699108

http://hllvm.group.iteye.com/group/topic/33366

http://blog.csdn.net/renfufei/article/details/49230943

http://blog.csdn.net/newhappy2008/article/details/7596027

相關文章
相關標籤/搜索