JVM運行時數據區--Java虛擬機棧


虛擬機棧的背景

由於跨平台性的設計,java的指令都是根據棧來設計的。不同平台CPU架構不同,所以不能設計為基於寄存器的。
根據棧設計的優點是跨平台,指令集小,編譯器容易實現,缺點是性能下降,實現同樣的功能需要更多的指令。

內存中的堆與棧

棧是運行時的單位,而堆是存儲的單位
1.棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎么放、放在哪兒。

2.一般來講,對象主要都是放在堆空間的,是運行時數據區比較大的一塊

3.棧空間存放 基本數據類型的局部變量,以及引用數據類型的對象的引用

 

Java虛擬機棧的特點

1.java虛擬機棧(Java Virtual Machine Stack),早期也叫Java棧。 每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應這個一次次的java方法調用。它是線程私有的

2.生命周期和線程是一致的

3.棧是一種快速有效的分配存儲方式,訪問速度僅次於PC寄存器(程序計數器)

4.作用:主管java程序的運行,它保存方法的局部變量、8種基本數據類型、對象的引用地址、部分結果,並參與方法的調用和返回。

局部變量:相較於成員變量(成員變量或稱屬性)

基本數據變量:8種基本數據類型

引用類型變量:類,數組,接口

5.JVM直接對java棧的操作只有兩個

(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.分配的棧內存越大越好么?

  • 不是 會擠占其他線程的空間
4.垃圾回收是否會涉及到虛擬機棧?
  • 不會

 

  • 關於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); } }

 

 

 




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM