Java虛擬機運行時棧幀結構(周志明書上P237頁)
棧幀是什么?
棧幀是一種數據結構,用於虛擬機進行方法的調用和執行。
棧幀是虛擬機棧的棧元素,也就是入棧和出棧的一個單元。
2018.1.2更新(在網上看到一個更好的解釋):
棧幀(Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接 (Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)。
棧幀在什么地方?
內存 -> 運行時數據區 -> 某個線程對應的虛擬機棧 -> 這里就是棧幀了
棧幀的含義?
每個方法的執行和結束對應着棧幀的入棧和出棧。
入棧表示被調用,出棧表示執行完畢或者返回異常。
一個虛擬機棧對應一個線程,當前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