1. 程式人生 > 實用技巧 >Java虛擬機器棧

Java虛擬機器棧

虛擬機器棧的背景

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

記憶體中的堆與棧

棧是執行時的單位,而堆是儲存的單位

  1. 棧解決程式的執行問題,即程式如何執行,或者說如何處理資料。堆解決的是資料儲存的問題,即資料怎麼放、放在哪兒。

  2. 一般來講,物件主要都是放在堆空間的,是執行時資料區比較大的一塊

  3. 棧空間存放 基本資料型別的區域性變數,以及引用資料型別的物件的引用

每個功能展示

區域性變量表
一個的32位4位元組、定長的連續的記憶體區儲存結構。
區域性變數i、j就是放在區域性變量表中。
當容量不夠時,區域性變量表會自動擴容。


運算元棧

執行反編譯命令,並轉換成txt檔案。 javap -c -v Test.class > TestCase.txt

在class檔案所在的資料夾中找到txt檔案。

圖中stack=2,就是一個棧,運算元棧。
比如運算元21就是執行列印方法。
iconst_0、istore_2這些都是將量壓入/存入棧的指令。
運算元0,iconst_0,即程式中壓入變數i。
運算元1,istore_2 ,即程式中存入區域性變數j。
運算元3和4,iload_1和iload_2,即載入變數i和j的值。
運算元4,iadd,即把i和j相加。
運算元5,istore_3,把相加的結果存入區域性變數三。
以上一一對應程式的執行過程,實際上就是將各個值壓入棧中,用匯編指令操作。
運算元棧中的obj,實際上是一個引用,指向堆中的實際物件。
...................



動態連線(常量池引用)
放在常量池中,但是比常量更大的量。
我們定義一個介面和實現類:





出口
指方法返回的地址:出棧。

虛擬機器棧與程式計數器一樣,每個執行緒都獨享一個虛擬機器棧。

摘自https://zhuanlan.zhihu.com/p/74784309

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異常

會出現的倆異常

  1. StackOverFlowError異常
    java虛擬機器規範允許Java棧的大小是動態的或者是固定不變的,如果採用固定大小的Java虛擬機器棧,那每一個執行緒的java虛擬機器棧容量可以線上程建立的時候獨立選定。如果執行緒請求分配的棧容量超過java虛擬機器棧允許的最大容量,java虛擬機器將會丟擲一個
/**
 * 演示棧中的異常
 */
public class StackErrorTest {
    public static void main(String[] args) {
        main(args);
    }
}
  1. OutOfMemoryError異常

如果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 . 分配的棧記憶體越大越好麼?

不是 會擠佔其他執行緒的空間
  1. 垃圾回收是否會涉及到虛擬機器棧?

不會

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

    }

}