1.java虛擬機棧
1. Java虛擬機棧也是線程私有的,它的生命周期與線程相同(隨線程而生,隨線程而滅)
2. 如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;
如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常;
(當前大部分JVM都可以動態擴展,只不過JVM規范也允許固定長度的虛擬機棧)
3. Java虛擬機棧描述的是Java方法執行的內存模型:每個方法執行的同時會創建一個棧幀。
對於我們來說,主要關注的stack棧內存,就是虛擬機棧中局部變量表部分。
2.棧幀(Stack Frame)
棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構。它是虛擬機運行時數據區中的java虛擬機棧的棧元素。
棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址等信息。
每一個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機里面從入棧到出棧的過程。
注意:
在編譯程序代碼的時候,棧幀中需要多大的局部變量表內存,多深的操作數棧都已經完全確定了。
因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。
棧結構圖如下:
注意:
在活動線程中,只有位於棧頂的棧幀才是有效的,稱為當前棧幀,與這個棧幀相關聯的方法稱為當前方法。
執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作。
3.局部變量表
1.局部變量表(Local Variable Table)是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。
並且在Java編譯為Class文件時,就已經確定了該方法所需要分配的局部變量表的最大容量。
2.局部變量表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)「String是引用類型」,
對象引用(reference類型) 和 returnAddress類型(它指向了一條字節碼指令的地址)
注意:
很多人說:基本數據和對象引用存儲在棧中。
當然這種說法雖然是正確的,但是很不嚴謹,只能說這種說法針對的是局部變量。
局部變量存儲在局部變量表中,隨着線程而生,線程而滅。並且線程間數據不共享。
但是,如果是成員變量,或者定義在方法外對象的引用,它們存儲在堆中。
因為在堆中,是線程共享數據的,並且棧幀里的命名就已經清楚的划分了界限 : 局部變量表!
4.變量槽(Variable Slot)
局部變量表的容量以變量槽為最小單位,每個變量槽都可以存儲32位長度的內存空間,例如boolean、byte、char、short、int、float、reference。
對於64位長度的數據類型(long,double),虛擬機會以高位對齊方式為其分配兩個連續的Slot空間,也就是相當於把一次long和double數據類型讀寫分割成為兩次32位讀寫。
擴展知識點:
Slot復用
為了盡可能節省棧幀空間,局部變量表中的Slot是可以重用的,
也就是說當PC計數器的指令指已經超出了某個變量的作用域(執行完畢),
那這個變量對應的Slot就可以交給其他變量使用。
優點 : 節省棧幀空間。
缺點 : 影響到系統的垃圾收集行為。
(如大方法占用較多的Slot,執行完該方法的作用域后沒有對Slot賦值或者清空設置null值,垃圾回收器便不能及時的回收該內存。)
5.reference(對象實例的引用)
我的理解是:一個超鏈接
一般來說,虛擬機都能從引用中直接或者間接的查找到對象的以下兩點 :
a.在Java堆中的數據存放的起始地址索引。
b.所屬數據類型在方法區中的存儲類型。
例如:我們在創建一個Student對象時的數據存儲結構:
6.動態連接
每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,
持有這個引用是為了支持方法調用過程中的動態連接(Dynamic Linking)。
在類加載階段中的解析階段會將符號引用轉為直接引用,這種轉化也稱為靜態解析。
另外的一部分將在每一次運行時期轉化為直接引用。這部分稱為動態連接。
這里簡單提一下動態連接的概念,后面在詳細講解.
7.方法出口
當一個方法開始執行后,只有2種方式可以退出這個方法 :
方法返回指令 : 執行引擎遇到一個方法返回的字節碼指令,這時候有可能會有返回值傳遞給上層的方法調用者,這種退出方式稱為正常完成出口。
異常退出 : 在方法執行過程中遇到了異常,並且沒有處理這個異常,就會導致方法退出。
無論采用任何退出方式,在方法退出之后,都需要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能需要在棧幀中保存一些信息。
一般來說,方法正常退出時,調用者的PC計數器的值可以作為返回地址,棧幀中會保存這個計數器值。
而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。
8.實戰案例
如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常

1 package com.wfd360.demo01; 2 3 /** 4 * @Copyright (C) 5 * @Author: LI DONG PING 6 * @Date: 2019-07-15 17:17 7 * @Description: 棧內存溢出測試 8 * <p> 9 * 測試代碼設計思路 10 * 修改默認堆棧大小后,利用遞歸調用一個方法,達到棧深度過大的異常目的,同時在遞歸調用過程中記錄調用此次,得出最大深度的數據 11 * jvm參數 12 * -Xss 180k:設置每個線程的堆棧大小(最小180k),默認是1M 13 */ 14 public class TestStackOverflowErrorDemo { 15 //棧深度統計值 16 private int stackLength = 1; 17 18 /** 19 * 遞歸方法,導致棧深度過大異常 20 */ 21 public void stackLeak() { 22 stackLength++; 23 stackLeak(); 24 } 25 26 /** 27 * 啟動方法 28 * 測試結果:當-Xss 180k為180k時,stackLength~=1544,隨着-Xss參數變大時stackLength值隨之變大 29 * @param args 30 */ 31 public static void main(String[] args) { 32 TestStackOverflowErrorDemo demo = new TestStackOverflowErrorDemo(); 33 try { 34 demo.stackLeak(); 35 } catch (Throwable e) { 36 System.out.println("當前棧深度:stackLength=" + demo.stackLength); 37 e.printStackTrace(); 38 } 39 } 40 }
測試結果:
完美!