Java虛拟機, JVM, 位元組碼執行引擎, 運作時棧幀
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();
}
}
随後placeholder= null;
可以看到這一次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
---------------------------------------------------------
學如不及,猶恐失之