虛擬機棧的背景
由於跨平台性的設計,java的指令都是根據棧來設計的。不同平台CPU架構不同,所以不能設計為基於寄存器的。
根據棧設計的優點是跨平台,指令集小,編譯器容易實現,缺點是性能下降,實現同樣的功能需要更多的指令。
內存中的堆與棧
棧是運行時的單位,而堆是存儲的單位
1.棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎么放、放在哪兒。
2.一般來講,對象主要都是放在堆空間的,是運行時數據區比較大的一塊
3.棧空間存放 基本數據類型的局部變量,以及引用數據類型的對象的引用
Java虛擬機棧的特點
1.java虛擬機棧(Java Virtual Machine Stack),早期也叫Java棧。 每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應這個一次次的java方法調用。它是線程私有的
2.生命周期和線程是一致的
3.棧是一種快速有效的分配存儲方式,訪問速度僅次於PC寄存器(程序計數器)
4.作用:主管java程序的運行,它保存方法的局部變量、8種基本數據類型、對象的引用地址、部分結果,並參與方法的調用和返回。
局部變量:相較於成員變量(成員變量或稱屬性)
基本數據變量:8種基本數據類型
引用類型變量:類,數組,接口
(1)每個方法執行,伴隨着進棧(入棧,壓棧)
(2)執行結束后的出棧工作

6.對於棧來說不存在垃圾回收問題,但是肯定存在OOM異常
下面接着說Java虛擬機棧的異常
棧中可能出現的異常
java虛擬機規范允許Java棧的大小是動態的或者是固定不變的
如果采用固定大小的Java虛擬機棧,那每一個線程的java虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過java虛擬機棧允許的最大容量,java虛擬機將會拋出一個 StackOverFlowError異常
/** * 演示棧中的異常 */ public class StackErrorTest { public static void main(String[] args) { main(args); } }
如果java虛擬機棧可以動態拓展,並且在嘗試拓展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那java虛擬機將會拋出一個 OutOfMemoryError異常
設置棧的內存大小
我們可以使用參數-Xss選項來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度。 (IDEA設置方法:Run-EditConfigurations-VM options 填入指定棧的大小-Xss256k)
/** * 演示棧中的異常 * * 默認情況下:count 10818 * 設置棧的大小: -Xss256k count 1872 */ public class StackErrorTest { private static int count = 1; public static void main(String[] args) { System.out.println(count); count++; main(args); } }
Java虛擬機棧的存儲結構和運行原理
1.每個線程都有自己的棧,棧中的數據都是以棧幀(Stack Frame)的格式存在
2.在這個線程上正在執行的每個方法都對應各自的一個棧幀
3.棧幀是一個內存區塊,是一個數據集,維系着方法執行過程中的各種數據信息
4.JVM直接對java棧的操作只有兩個,就是對棧幀的壓棧和出棧,遵循先進后出/后進先出的和原則。
5.在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀(Current Frame),與當前棧幀對應的方法就是當前方法(Current Frame)
6.執行引擎運行的所有字節碼指令只針對當前棧幀進行操作
7.如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,成為新的當前棧幀。
8.不同線程中所包含的棧幀是不允許相互引用的,即不可能在另一個棧幀中引用另外一個線程的棧幀
9.如果當前方法調用了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀
10.Java方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另外一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出。
代碼示例:
/** * 棧幀 */ public class StackFrameTest { public static void main(String[] args) { StackFrameTest test = new StackFrameTest(); test.method1(); //輸出 method1()和method2()都作為當前棧幀出現了兩次,method3()一次 // method1()開始執行。。。 // method2()開始執行。。。 // method3()開始執行。。。 // method3()執行結束。。。 // method2()執行結束。。。 // method1()執行結束。。。 } public void method1(){ System.out.println("method1()開始執行。。。"); method2(); System.out.println("method1()執行結束。。。"); } public int method2(){ System.out.println("method2()開始執行。。。"); int i = 10; int m = (int) method3(); System.out.println("method2()執行結束。。。"); return i+m; } public double method3(){ System.out.println("method3()開始執行。。。"); double j = 20.0; System.out.println("method3()執行結束。。。"); return j; } }
虛擬機棧的相關面試題
1.舉例棧溢出的情況?(StackOverflowError)
- 遞歸調用等,通過-Xss設置棧的大小;
2.調整棧的大小,就能保證不出現溢出么?
- 不能 如遞歸無限次數肯定會溢出,調整棧大小只能保證溢出的時間晚一些,極限情況會導致OOM內存溢出(Out Of Memery Error)注意是Error
3.分配的棧內存越大越好么?
- 不是 會擠占其他線程的空間
- 不會

- 關於Error我們再多說一點,上面的討論不涉及Exception
-
首先Exception和Error都是繼承於Throwable 類,在 Java 中只有 Throwable 類型的實例才可以被拋出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。
- Exception和Error體現了JAVA這門語言對於異常處理的兩種方式。
-
Exception是java程序運行中可預料的異常情況,咱們可以獲取到這種異常,並且對這種異常進行業務外的處理。
-
Error是java程序運行中不可預料的異常情況,這種異常發生以后,會直接導致JVM不可處理或者不可恢復的情況。所以這種異常不可能抓取到,比如OutOfMemoryError、NoClassDefFoundError等。
-
其中的Exception又分為檢查性異常和非檢查性異常。兩個根本的區別在於,檢查性異常 必須在編寫代碼時,使用try catch捕獲(比如:IOException異常)。非檢查性異常 在代碼編寫使,可以忽略捕獲操作(比如:ArrayIndexOutOfBoundsException),這種異常是在代碼編寫或者使用過程中通過規范可以避免發生的。
5.方法中定義的局部變量是否線程安全?
- 要具體情況具體分析
/** * 面試題: * 方法中定義的局部變量是否線程安全?具體情況具體分析 * * 何為線程安全? * 如果只有一個線程可以操作此數據,則必定是線程安全的。 * 如果有多個線程操作此數據,則此數據是共享數據。如果不考慮同步機制的話,會存在線程安全問題 * * 我們知道StringBuffer是線程安全的源碼中實現synchronized,StringBuilder源碼未實現synchronized,在多線程情況下是不安全的
* 二者均繼承自AbstractStringBuilder
*
*/ public class StringBuilderTest { //s1的聲明方式是線程安全的,s1在方法method1內部消亡了 public static void method1(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); } //stringBuilder的操作過程:是不安全的,因為method2可以被多個線程調用 public static void method2(StringBuilder stringBuilder){ stringBuilder.append("a"); stringBuilder.append("b"); } //s1的操作:是線程不安全的 有返回值,可能被其他線程共享 public static StringBuilder method3(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); return s1; } //s1的操作:是線程安全的 ,StringBuilder的toString方法是創建了一個新的String,s1在內部消亡了 public static String method4(){ StringBuilder s1 = new StringBuilder(); s1.append("a"); s1.append("b"); return s1.toString(); } public static void main(String[] args) { StringBuilder s = new StringBuilder(); new Thread(()->{ s.append("a"); s.append("b"); }).start(); method2(s); } }