面試篇:虛擬機器棧5連問,一聽心裡就樂了

阿Q說程式碼發表於2021-05-17

面試路上

“滴,滴滴......”師傅我們到哪了?我還要趕著面試呢。

師傅: 快了快了,下個路口就到了。真是服了這幫人了,不會開車淨往裡湊。

聽著司機師傅的抱怨聲,不禁想起首打油詩:滿目尾燈紅,耳盈刺笛聲。心憂遲到久,頹首似雷轟。

一下車趕緊小跑就進了富麗堂皇的酒店,不不不,是商務樓,這大廳有點氣派,讓我有點想入非非呀。

面試經過

“咚咚咚”,“請進”。

面試官: 小夥子長得挺帥呀,年輕人就是有活力,來先做個簡單的自我介紹吧。

阿Q: 面試官你好,My name is “影流之主”,來自艾歐尼亞,是LOL中的最強中單(不接受反駁),論單殺沒有服過誰。我的口頭禪是“無形之刃,最為致命”,當然你也可以叫我阿Q,這是我的簡歷。
簡歷

面試官: 阿Q,那我們也不寒暄了,直接切正題吧。看你jvm寫的知識點最多,那就先說一下你對虛擬機器棧的理解吧。

阿Q: 內心OS:這波可以吹X了。咳...咳...虛擬機器棧早期也叫java棧,是在jvm的執行時資料區存在的一塊記憶體區域。它是執行緒私有的,隨執行緒建立而建立,隨執行緒消亡而結束。

嗯。。。假裝想一下?

眾所周知,棧只有進棧和出棧兩種操作,所以它是一種快速有效的分配儲存方式。對於它來說,它不存在垃圾回收問題,但是它的大小是動態的或者固定不變的,因此它會存在棧溢位或者記憶體溢位問題......

面試官: 打斷一下啊,你剛才說會存在棧溢位和記憶體溢位問題,那你能分別說一下為什麼會出現這種情況嗎?

阿Q: 可以可以,我們知道虛擬機器棧由棧幀組成,每一個方法的呼叫都對應著一個棧幀的入棧。我們可以通過-Xss引數來設定棧的大小,假設我們設定的虛擬機器棧大小很小,當我們呼叫的方法過多,也就是棧幀過多的話,就會出現StackOverflowError,即棧溢位問題。

假如我們的棧幀不固定,設定為動態擴充套件的,那在我們的記憶體不足時,也就沒有足夠的記憶體來支援棧的擴充套件,這個時候就會出現OOM異常,即記憶體溢位問題。

面試官: 嗯嗯(點頭狀),示意小夥子思路很清晰呀,那你剛才說到棧幀設定的太小會導致棧幀溢位問題,那我們設定的大點不就可以完全避免棧溢位了嘛。

阿Q: 一聽就是要給我挖坑呀,像我們一般都比較崇尚中庸之道,所以一聽到這種絕對的問題,必須機靈點:不不不,調整棧的大小隻可以延緩棧溢位的時間或者說減少棧溢位的風險。

舉個?吧

  1. 假如一個業務邏輯的方法呼叫需要5000次,但是此時丟擲了棧溢位的錯誤。我們可以通過設定-Xss來獲取更大的棧空間,使得呼叫在7000次時才會溢位。此時調整棧大小就變得很有意義,因為這樣就會使得業務能正常支援。

  2. 那假如是有死遞迴的情況則無論怎麼提高棧大小都會溢位,這樣也就沒有任何意義了。

面試官: 好的,那你看一下這個簡單的小程式,你能大體說一下它在記憶體中的執行過程嗎?

 public void test() {
      byte i = 15;
      int j = 8;
      int k = i + j;
}

來張圖,便於大家更好地理解

阿Q: 先把該程式碼編譯一下,然後檢視它的位元組碼檔案。如上圖中左邊所示,執行過程如下:

  1. 首先將要執行的指令地址0存放到PC暫存器中,此時,區域性變數表和運算元棧的資料為空;
  2. 當執行第一條指令bipush時,將運算元15放入運算元棧中,然後將PC暫存器的值置為下一條指令的執行地址,即2
  3. 當執行指令地址為2的操作指令時,將運算元棧中的資料取出來,存到區域性變數表的1位置,因為該方法是例項方法,所以0位置存的是this的值,PC暫存器中的值變為3;
  4. 同步驟2和3將8先放入運算元棧,然後取出來存到區域性變數表中,PC暫存器中的值也由3->5->6
  5. 當執行到地址指令為678時,將區域性變數表中索引位置為12的資料重新載入到運算元棧中並進行iadd加操作,將得到的結果值存到運算元棧中,PC暫存器中的值也由6->7->8->9
  6. 執行操作指令istore_3,將運算元棧中的資料取出存到區域性變數表中索引為3的位置,執行return指令,方法結束。

面試官: 內心OS:這小子貌似還可以呀。說的還不錯,那你能說一下方法中定義的區域性變數是否執行緒安全嗎?

阿Q: 那我再用幾個例子來說一下吧。

public class LocalParaSafeProblem {


    /**
     * 執行緒安全的
     * 雖然StringBuilder本身執行緒不安全,
     * 但s1 變數只存在於這個棧幀的區域性變數表中,
     * 因為棧幀是每個執行緒獨立的一份,
     * 所以這裡的s1是執行緒安全的
     */
    public static void method01() {
        // 執行緒內部建立的,屬於區域性變數
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
    }

    /**
     * 執行緒不安全
     * 因為此時StringBuilder是作為引數傳入,
     * 外部的其他執行緒也可以訪問,所以執行緒不安全
     */
    public static void method02(StringBuilder stringBuilder) {
        stringBuilder.append("a");
        stringBuilder.append("b");
    }

    /**
     * 執行緒不安全
     * 此時StringBuilder被多個執行緒同時操作
     */
    public static void method03() {
        StringBuilder stringBuilder = new StringBuilder();
        new Thread(() -> {
            stringBuilder.append("a");
            stringBuilder.append("b");
        }, "t1").start();

        method02(stringBuilder);
    }

    /**
     * 執行緒不安全
     * 因為此時方法將StringBuilder返回出去了
     * 外面的其他執行緒可以直接修改StringBuilder這個引用了所以不安全
     */
    public static StringBuilder method04() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("a");
        stringBuilder.append("b");
        return stringBuilder;
    }


    /**
     * StringBuilder是執行緒安全的
     * 此時stringBuilder值在當前棧幀的區域性變數表中存在,
     * 其他執行緒無法訪問到該引用,
     * 方法執行完成之後此時區域性變數表中的stringBuilder的就銷燬了
     * 返回的stringBuilder.toString()執行緒不安全
     * 最後的返回值將toString返回之後,其他執行緒可以操作而String本身是執行緒不安全的。
     */
    public static String method05() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("a");
        stringBuilder.append("b");
        return stringBuilder.toString();
    }
}

看到這估計會有點繞,那我就總結一下吧:如果物件是在方法內部產生且在內部消亡,不會返回到外部就不存線上程安全問題;反之如果類本身執行緒不安全的話就存線上程安全問題。

面試官: 不錯不錯,有理有據,那你再說說你對堆記憶體的理解吧。

阿Q: 唉,今天太累了,說了一天這個了,不想說了。

面試官: 那好吧,那我們今天先到這吧,回去等通知吧。

如果你還有什麼困惑的話,可以關注gzh“阿Q說程式碼”,也可以加阿Q好友qingqing-4132,阿Q期待你的到來!

相關文章