天天看點

Java虛拟機(五):Java虛拟機棧

一、虛拟機棧的背景

基于Java的語言的

跨平台性

設計,由因為CPU的架構不同,是以JVM不能設計為基于寄存器結構

根據棧設計

​ 優點:1. 跨平台 2.指令集小,編譯器容易實作

​ 缺點 :1.性能下降,效率低 2.實作同樣功能需要更多的指令

JVM的堆與棧

棧是運作時的機關,堆是儲存機關:棧管運作,堆管儲存

  1. 棧解決程式的運作問題,即程式如何執行,或者說如何處理資料。堆解決的是資料存儲的問題,即資料怎麼放、放在哪兒
  2. 一般來講,對象主要都是放在堆空間的,是運作時資料區占用記憶體較大的一部分
  3. 棧存放基本

    資料類型的局部變量

    引用資料類型對象的引用

二、Java虛拟機棧的特點與作用

1. Java虛拟機棧的特點

1.Java虛拟棧(Java Virtual Machine Stack),起初也叫Java棧,每個線程在建立是都會建立一個虛拟機棧,其内部儲存一個個的棧幀(Stack Frame),對應一個線程中每個方法的調用,

它是線程私有

2.聲明周期與建立它的線程一緻

3.棧是一種快速有效的配置設定存儲方式,通路速度僅次于

程式計數器

4.JVM對Java虛拟機棧的操作隻有兩個

  1. 每個方法執行前,伴随的入棧
  2. 方法執行完畢後的出棧

5.Java虛拟機棧沒有GC問題,但存在OutOfMemoryError與StackOverflowError

2. Java虛拟機棧的作用

負責Java程式的運作,它儲存方法的局部變量、8種基本資料類型、對象的引用位址、部分結果,并參與方法的調用和傳回

​ 局部變量是相對于成員變量(屬性):如不清楚,可以參考:

https://www.cnblogs.com/battlecry/p/9373993.html

三、Java虛拟機棧異常

Java虛拟機規範允許Java棧的大小是動态的或者是固定不變的

Java棧的大小是固定不變導緻:StackOverflowError

如果采用固定大小的Java虛拟機棧,那每一個線程的Java虛拟機棧容量可以線上程建立的時候獨立標明。如果線程請求配置設定的棧容量超過Java虛拟機棧允許的最大容量,Java虛拟機将會抛出一個 StackOverFlowError異常

/**
 * 示範棧中的異常:StackOverflowError
 */
public class StackErrorTest {
    public static void main(String[] args) {
        main(args);
    }
}           

解決辦法:我們可以使用參數-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棧的大小是動态導緻:OOM(OutOfMemoryError)

如果Java虛拟機棧可以動态拓展,并且在嘗試拓展的時候無法申請到足夠的記憶體,或者在建立新的線程時沒有足夠的記憶體去建立對應的虛拟機棧,那Java虛拟機将會抛出一個 OutOfMemoryError異常

四、Java虛拟棧的存儲結構和運作原理

1.每個線程都有自己的棧,棧中的資料都是以棧幀(Stack Frame)的格式存在

2.在這個線程上正在執行的每個方法都對應各自的一個棧幀

3.棧幀是一個記憶體區塊,是一個資料集,維系着方法執行過程中的各種資料資訊

4.JVM直接對Java棧的操作隻有兩個,就是對棧幀的壓棧和出棧,遵循先進後出(FILO)的和原則

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;
    }

}           

五、Java虛拟棧相關面試題

1.舉例棧溢出的情況?(StackOverflowError)

  • 遞歸調用等,通過-Xss設定棧的大小;

2.調整棧的大小,就能保證不出現溢出麼?

  • 不能 如遞歸無限次數肯定會溢出,調整棧大小隻能保證溢出的時間晚一些,極限情況會導緻OOM記憶體溢出(Out Of Memery Error)

3.配置設定的棧記憶體越大越好麼?

  • 不是 會擠占其他線程的空間

4.垃圾回收是否會涉及到虛拟機棧?

  • 不會

-

5.方法中定義的局部變量是否線程安全?

  • 要分析具體情況
    /**
     * 面試題:
     * 方法中定義的局部變量是否線程安全?具體情況具體分析
     *
     * 何為線程安全?
     *     如果隻有一個線程可以操作此資料,則必定是線程安全的。
     *     如果有多個線程操作此資料,則此資料是共享資料。如果不考慮同步機制的話,會存線上程安全問題
     *
     * 我們知道StringBuffer是線程安全的源碼中實作synchronized,StringBuilder源碼未實作synchronized,在多線程情況下是不安全的
     **/
    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);
    
        }
    }