虛擬機棧 筆記記錄
- 1. 定義
- 1.1 演示棧幀
- 2. 特點
- 3. 線程運行診斷
- 3.1 案例1 cpu占用過多&解決
- 3.2 案例2 程序運行很長時間沒有結果
- 4. 拓展知識&問題辨析
- 4.1 棧的內存越大越好嘛?(不是)
- 4.2 方法內的局部變量是否線程安全?(是線程安全的)
- 4.2.1 局部變量全在方法中
- 4.2.2 局部變量可能逃離方法
- 4.3 可能拋出的異常?
- 4.4 什么情況下會發生棧內存溢出?
- 4.5 如何設置棧的大小?
- 4.6 補充棧幀的內部結構
- 重要的兩個(局部變量表&操作數棧)
- 4.6.1 局部變量表
- 4.6.2 操作數棧
學習資料來源-b站黑馬JVM& 尚硅谷JVM精講與GC調優
1. 定義
- 虛擬機棧:線程運行需要的內存空間。棧中放的是多個棧幀。
- 棧幀:每個方法運行時需要的內存。(比如參數,局部變量,返回地址等。)
- 每個線程只能有1個活動棧幀,對應著當前正在執行的那個方法。
- Java虛擬機棧(Java Virtual Machine Stack),早期也叫Java棧。每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應著一次次的Java方法調用。是線程私有的,生命周期和線程一致。
- 虛擬機棧中放的一個個棧幀。
1.1 演示棧幀
public static void main(String[] args) {method1();}private static void method1() {method2(1,2);}private static int method2(int a, int b) {int c=a+b;return c;}
2. 特點
- 棧是一種快速有效的分配存儲方式,訪問速度僅次于程序計數器。
- 垃圾回收是否涉及棧內存?(不會,一次次方法執行后彈出棧自動被回收掉。)不存在GC ; 但是會存在OOM。
3. 線程運行診斷
3.1 案例1 cpu占用過多&解決
- 首先通過top命令定位哪個進程對cpu占用過高
- 根據pid 32655 我們只直到這個進程占用過高,但是我們需要知道那個線程導致的。使用ps H -eo pid,tid,%cpu | grep 32655 (用ps命令進一步定位哪個線程占用cpu過高)
對Linux忘記的話借用deepseek回顧一下。
- 使用jdk提供的工具,jstack 進程id
我們已經知道是線程32655的導致的,可以直接根據線程編號,找到對應的輸出信息。但是注意,32655是10進制的,但是jstack輸出的信息是16進制的,所以先換算一下。
這里得到7F99,找到對應的代碼。根據線程id7f99找到對應的包下的java代碼,第8行。
- 發現原來是代碼寫了while(true) 死循環,導致CPU高。
- 當然這里只是一個舉例,實際情況要看線上的具體問題和代碼。
3.2 案例2 程序運行很長時間沒有結果
一直滑倒最后看到一個死鎖問題。這也是為啥不輸出的原因。
再看具體的Java代碼,其實也是很好理解。如果不理解也沒關系,重溫鎖的知識或者死鎖的產生等即可。
4. 拓展知識&問題辨析
4.1 棧的內存越大越好嘛?(不是)
棧越大 內存分配的棧越少,棧越少 線程越少。所以棧別太大,越小線程越多,也不能太小,太小棧溢出。【一般采用系統默認就好】
4.2 方法內的局部變量是否線程安全?(是線程安全的)
4.2.1 局部變量全在方法中
不會有線程安全問題,每個局部變量都在各自的棧幀中,線程不共享。
static void m1(){int x=0;for (int i=0;i<1000;i++){x++;}System.out.println(x);}
4.2.2 局部變量可能逃離方法
總結就是具體問題具體分析,要注意局部變量逃離本方法的問題。
//線程安全public static void m1() {StringBuilder sb = new StringBuilder();sb.append(1);sb.append(2);sb.append(3);System.out.println(sb);}//線程不安全,StringBuilder作為參數傳遞進來,就意味著有可能有其他線程能訪問到它。就不再是線程私有的了。public static void m2(StringBuilder sb) {sb.append(1);sb.append(2);sb.append(3);System.out.println(sb);}//線程不安全,返回值也是StringBuilder,所以其他線程能訪問到它。public static StringBuilder m3() {StringBuilder sb = new StringBuilder();sb.append(1);sb.append(2);sb.append(3);return sb;}
4.3 可能拋出的異常?
StackOverFlowError?OutOfMemoryError?
- Java 虛擬機規范允許Java棧的大小是動態的或者是固定不變的。 如果采用固定大小的Java虛擬機棧,那每一個線程的Java虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量,Java虛擬機將會拋出一個
StackOverflowError 異常。- 如果Java虛擬機棧可以動態擴展,并且在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出—個
OutOfMemoryError 異常。
4.4 什么情況下會發生棧內存溢出?
一、局部數組過大。當函數內部的數組過大時,有可能導致堆棧溢出。(棧幀過大)
二、遞歸調用層次太多。遞歸函數在運行時會執行壓棧操作,當壓棧次數太多時,也會導致堆棧溢出。(棧幀過多)對于第1種情況可能會發生在對象轉換JSON的時候,如Dept中有Emp,Emp中有Dept,無線循環,可以使用
@JsonIgnore
private Dept dept; 忽略即可。
4.5 如何設置棧的大小?
-Xss size (即:-XX:ThreadStackSize)
-Xss256k或者-XX:ThreadStackSize=256k都可以
我們看棧大小window下可能會顯示0
- 設置10124K
添加VM options
4.6 補充棧幀的內部結構
重要的兩個(局部變量表&操作數棧)
可以IDEA插件中下載jclasslib插件來看代碼
4.6.1 局部變量表
局部變量表(local variables)
● 局部變量表也被稱之為局部變量數組或本地變量表。
● 定義為一個數字數組,主要用于存儲方法參數和定義在方法體內的局部變量,這些數據類型包括各類基本數據類型(8種)、對象引用(reference),以及returnAddress類型。
● 局部變量表所需的容量大小是在編譯期確定下來的,并保存在方法的Code屬性的maximum local variables數據項中。在方法運行期間是不會改變局部變量表的大小的。
● 方法嵌套調用的次數由棧的大小決定。一般來說,棧越大,方法嵌套調用次數越多。對一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以滿足方法調用所需傳遞的信息增大的需求。進而函數調用就會占用更多的棧空間,導致其嵌套調用次數就會減少。
● 局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束后,隨著方法棧幀的銷毀,局部變量表也會隨之銷毀。
4.6.2 操作數棧
操作數棧(Operand Stack)
● 我們說Java虛擬機的解釋引擎是基于棧的執行引擎,其中的棧指的就是操作數棧。
● 每一個獨立的棧幀中除了包含局部變量表以外,還包含一個后進先出(Last-In-First-Out)的操作數棧,也可以稱之為表達式棧(Expression Stack)。
● 操作數棧就是JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被創建出來,這個方法的操作數棧是空的。
● 每一個操作數棧都會擁有一個明確的棧深度用于存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,為max_stack的值。
棧中的任何一個元素都是可以任意的Java數據類型。
32bit的類型占用一個棧單位深度
64bit的類型占用兩個棧單位深度
● 操作數棧,在方法執行過程中,根據字節碼指令,并非采用訪問索引的方式來進行數據訪問的,而是只能通過標準的入棧(push)和出棧(pop)操作,往棧中寫入數據或提取數據來完成一次數據訪問。
某些字節碼指令將值壓入操作數棧,其余的字節碼指令將操作數取出棧。使用它們后再把結果壓入棧。比如:執行復制、交換、求和等操作
如果被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數棧中,并更新PC寄存器中下一條需要執行的字節碼指令。
- 操作數棧,主要用于保存計算過程的中間結果,同時作為計算過程中變量臨時的存儲空間。
這里做一下簡單的介紹,具體對于棧的深度,以及有過slot的復用,每個類型占多少大小等后續復習繼續更新吧。