深入理解JVM虛擬機與性能調優實踐—虛擬機棧知識梳理


Java虛擬機棧是線程私有的與程序計數器一樣,它的生命週期與線程相同。虛擬機棧描述的Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀(Stack Frame)用於存儲局部變量、操作數棧、動態鏈接、方法出口等信息。每一個方法調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。



如下圖就是表達棧幀的過程:

深入理解JVM虛擬機與性能調優實踐—虛擬機棧知識梳理


一、虛擬機棧特點

1)Java虛擬機棧(Java Virtual Machine Stack)也是線程私有的,它的生命週期與線程相同。

2)Java虛擬機棧有兩種異常拋出的可能:

(*) 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;

(*)如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常(當前大部分JVM都可以動態擴展,只不過JVM規範也允許固定長度的虛擬機棧)。

3)Java虛擬機棧描述的是Java方法執行的內存模型,每個方法執行的同時會創建一個棧幀。

4)對於我們來說,主要關注的stack棧內存,就是虛擬機棧中局部變量表部分。

二、虛擬機棧組成元素解釋

  • 棧幀
深入理解JVM虛擬機與性能調優實踐—虛擬機棧知識梳理

1)棧幀(Stack Frame)是用於支持虛擬機引擎進行方法調用和方法執行的數據結構,它是虛擬機運行時數據區中的java虛擬機棧的棧元素;

2)棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址returnAddress等信息;

3) 每一個方法從調用開始至執行完成的過程,都對應著一個棧幀在虛擬機裡面從入棧到出棧的過程。


  • 局部變量表Local Variable Table

每個棧幀中都包含一組稱為局部變量表的變量列表,用於存放方法參數和方法內部定義的局部變量。在 Java 程序編譯成 Class 文件時,在 Class 文件格式屬性表中 Code 屬性的 max_locals(局部變量表所需的存儲空間,單位是 Slot) 數據項中確定了需要分配的局部變量表的最大容量。

局部變量表的容量以變量槽(Variable Slot)為最小單位,不過 Java 虛擬機規範中並沒有明確規定每個 Slot 所佔據的內存空間大小,只是有導向性地說明每個 Slot 都應該存放的8種類型: byte、short、int、float、char、boolean、reference(對象引用就是存到這個棧幀中的局部變量表裡的,這裡的引用指的是局部變量的對象引用,而不是成員變量的引用。

局部變量表中的 Slot 是可重用的,方法體中定義的變量,其作用域並不一定會覆蓋整個方法體,如果當前字節碼程序計數器的值已經超過了某個變量的作用域,那麼這個變量相應的 Slot 就可以交給其他變量去使用,節省棧空間,但也有可能會影響到系統的垃圾收集行為。

局部變量無初始值(實例變量和類變量都會被賦予初始值),類變量有兩次賦初始值的過程,一次在準備階段,賦予系統初始值;另外一次在初始化階段,賦予開發者定義的值。因此即使在初始化階段開發者沒有為類變量賦值也沒有關係,類變量仍然具有一個確定的默認值。但局部變量就不一樣了,如果一個局部變量定義了但沒有賦初始值是不能使用的。

局部變量表相關參數計算,代碼演示如下:

關於stack、local、args_size 計算如下:

Stack: int a(1個棧深度)+ byte b(1個棧深度)=2

Locals: this(1 Slot)+ int a(1 Slot)+ byte b(1 Slot)+ int sum (1 slot)=4

args_size:(非 static 方法,this 隱含參數)=1


  • 操作棧

1)操作數棧是一個先進後出(First In Last Out)棧,方法的執行操作在操作數棧中完成,每一個字節碼指令往操作數棧進行寫入和提取的過程,就是入棧和出棧的過程。同局部變量表一樣,操作數棧的最大深度也是Java 程序編譯成 Class 文件時被寫入到 Class 文件格式屬性表的 Code 屬性的 max_stacks 數據項中。

2)操作數棧的每一個元素可以是任意的 Java 數據類型,32位數據類型所佔的棧容量為1 slot為基本單位,64位數據類型所佔的棧容量為2 slot ,基本數據類型中佔用1 slot容量的 有int、byte 、char、boolean、short;佔用2 slot容量的有long、double、float。在方法執行的任何時候,操作數棧的深度都不會超過在 max_stacks 數據項中設定的最大值(指的是進入操作數棧的 "同一批操作" 的數據類型的棧容量的和)。

3)當一個方法剛剛執行的時候,這個方法的操作數棧是空的,在方法執行的過程中,通過一些字節碼指令從局部變量表或者對象實例字段中複製常量或者變量值到操作數棧中,也提供一些指令向操作數棧中寫入和提取值,及結果入棧,也用於存放調用方法需要的參數及接受方法返回的結果。例如,整數加法的字節碼指令 iadd(使用 iadd 指令時,相加的兩個元素也必須是 int 型) 在運行的時候將操作數棧中最接近棧頂的兩個 int 數值元素出棧相加,然後將相加結果入棧。


  • 動態鏈接

每個棧幀都包含一個指向運行時常量池(JVM 運行時數據區域)中該棧幀所屬性方法的引用,持有這個引用是為了支持方法調用過程中的動態連接。

在 Class 文件格式的常量池(存儲字面量和符號引用)中存有大量的符號引用(1.類的全限定名,2.字段名和屬性,3.方法名和屬性),字節碼中的方法調用指令就以常量池中指向方法的符號引用為參數。這些符號引用一部分會在類加載過程的解析階段的時候轉化為直接引用(指向目標的指針、相對偏移量或者是一個能夠直接定位到目標的句柄),這種轉化稱為靜態解析。另外一部分將在每一次的運行期期間轉化為直接引用,這部分稱為動態連接。


  • 對象實例引用
深入理解JVM虛擬機與性能調優實踐—虛擬機棧知識梳理

深入理解JVM虛擬機與性能調優實踐—虛擬機棧知識梳理

  • 變量槽slot

1)局部變量表的容量以變量槽為最小單位,每個變量槽都可以存儲32位長度的內存空間,例如boolean、byte、char、short、int、float、reference。

2)對於64位長度的數據類型(long,double),虛擬機會以高位對齊方式為其分配兩個連續的Slot空間,也就是相當於把一次long和double數據類型讀寫分割成為兩次32位讀寫。

  • 方法退出

一個方法開始執行後,只有兩種方式可以退出這個方法:

(一) 正常方式退出

執行引擎遇到任意一個方法返回的字節碼指令(例如:areturn),這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱為調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)

(二)異常方式退出

在方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理,無論是Java虛擬機內部產生的異常,還是代碼中使用 athrow 字節碼指令產生的異常,只要在本方法的異常處理器表中沒有搜索到匹配的異常處理器,就會導致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的上層調用者產生任何返回值的。

無論採用何種退出方式,在方法退出之後,都需要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,調用者的程序計數器的值可以作為返回地址,棧幀中很可能會保存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。

方法退出的過程實際上就等同於把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的局部變量表和操作數棧,把返回值(如果有的話)壓入調用者棧幀的操作數棧中,調整程序計數器的值以指向方法調用指令後面的一條指令等。

  • 棧深度

方法的從調用到執行完成,對應了虛擬機棧的入棧到出棧的過程。

在編譯期就可以確認局部變量表的大小和操作數棧的深度,並且寫入到方法表的 code 屬性中,運行期間不會發生改變。所以在編譯器每個棧幀的需要大小就可以確定了。棧深度由運行期決定。

具體的棧深度受虛擬機棧大小和棧幀大小的影響,要看使用了多少棧幀,棧幀大小多少。每個棧幀的大小不一定一樣,取決於各棧幀對應方法的局部變量表和操作數棧大小等。

假設我們的虛擬機棧大小固定,棧幀數量達到最大值,也就是達到最大深度,深度大小和棧幀大小的示意圖如下:

深入理解JVM虛擬機與性能調優實踐—虛擬機棧知識梳理

JVM-虛擬機棧深度

上面的示意圖可以看出,在 Java 虛擬機棧大小固定的情況下,如果每個棧幀都很大,最大可用深度就會變小。

上面只是一個示意圖,實際上虛擬機棧深度沒這麼小。默認情況下 Java 虛擬機棧有 1M,平時開發時的棧幀也不會很大。

當線程請求的棧深度大於虛擬機的所允許的棧深度會發生 StackOverflowError 異常。畢竟如果一個線程不斷地往虛擬機棧中加入棧幀,會消耗掉大量的內存,影響到其他線程的執行。

· HotSpot參數設置

-Xss,設置虛擬機棧大小,JDK1.5 之後默認為 1M。棧深度受到這個堆棧大小的約束。在固定物理內存下減小 Java 虛擬機棧大小可以產生更多線程,但是一個進程的線程數量有約束,不能無限增加。


分享到:


相關文章: