面試路上
“滴,滴滴......”師傅我們到哪了?我還要趕着面試呢。
「師傅:」 快了快了,下個路口就到了。真是服了這幫人了,不會開車淨往裏湊。
聽着司機師傅的抱怨聲,不禁想起首打油詩:滿目尾燈紅,耳盈刺笛聲。心憂遲到久,頹首似雷轟。
一下車趕緊小跑就進了富麗堂皇的酒店,不不不,是商務樓,這大廳有點氣派,讓我有點想入非非呀。
面試經過
“咚咚咚”,“請進”。
「面試官:」 小夥子長得挺帥呀,年輕人就是有活力,來先做個簡單的自我介紹吧。
「阿Q:」 面試官你好,My name is “影流之主”,來自艾歐尼亞,是LOL
中的最強中單(不接受反駁),論單殺沒有服過誰。我的口頭禪是“無形之刃,最爲致命”,當然你也可以叫我阿Q,這是我的簡歷。
「面試官:」 阿Q,那咱也不寒暄了,直接切正題吧。看你jvm
寫的知識點最多,那就先說一下你對虛擬機棧的理解吧。
「阿Q:」 內心OS:這波可以吹X了。咳...咳...虛擬機棧早期也叫java
棧,是在jvm
的運行時數據區存在的一塊內存區域。它是線程私有的,隨線程創建而創建,隨線程消亡而結束。
嗯。。。假裝想一下😄
衆所周知,棧只有進棧和出棧兩種操作,所以它是一種快速有效的分配存儲方式。對於它來說,它不存在垃圾回收問題,但是它的大小是動態的或者固定不變的,因此它會存在棧溢出或者內存溢出問題......
「面試官:」 打斷一下啊,你剛纔說會存在棧溢出和內存溢出問題,那你能分別說一下爲什麼會出現這種情況嗎?
「阿Q:」 可以可以,我們知道虛擬機棧由棧幀組成,每一個方法的調用都對應着一個棧幀的入棧。我們可以通過-Xss
參數來設置棧的大小,假設我們設置的虛擬機棧大小很小,當我們調用的方法過多,也就是棧幀過多的話,就會出現StackOverflowError
,即棧溢出問題。
假如我們的棧幀不固定,設置爲動態擴展的,那在我們的內存不足時,也就沒有足夠的內存來支持棧的擴展,這個時候就會出現OOM
異常,即內存溢出問題。
「面試官:」 嗯嗯(點頭狀),示意小夥子思路很清晰呀,那你剛纔說到棧幀設置的太小會導致棧幀溢出問題,那我們設置的大點不就可以完全避免棧溢出了嘛。
「阿Q:」 一聽就是要給我挖坑呀,像我們一般都比較崇尚中庸之道,所以一聽到這種絕對的問題,必須機靈點:不不不,調整棧的大小隻可以「延緩」棧溢出的時間或者說減少棧溢出的風險。
舉個🌰吧
假如一個業務邏輯的方法調用需要
5000
次,但是此時拋出了棧溢出的錯誤。我們可以通過設置-Xss
來獲取更大的棧空間,使得調用在7000
次時纔會溢出。此時調整棧大小就變得很有意義,因爲這樣就會使得業務能正常支持。那假如是有「死遞歸」的情況則無論怎麼提高棧大小都會溢出,這樣也就沒有任何意義了。
「面試官:」 好的,那你看一下這個簡單的小程序,你能大體說一下它在內存中的執行過程嗎?
public void test() {
byte i = 15;
int j = 8;
int k = i + j;
}
來張圖,便於大家更好地理解
「阿Q:」 先把該代碼編譯一下,然後查看它的字節碼文件。如上圖中左邊所示,執行過程如下:
- 首先將要執行的指令地址
0
存放到PC
寄存器中,此時,局部變量表和操作數棧的數據爲空; - 當執行第一條指令
bipush
時,將操作數15
放入操作數棧中,然後將PC
寄存器的值置爲下一條指令的執行地址,即2
; - 當執行指令地址爲
2
的操作指令時,將操作數棧中的數據取出來,存到局部變量表的1
位置,因爲該方法是實例方法,所以0
位置存的是this
的值,PC
寄存器中的值變爲3
; - 同步驟2和3將
8
先放入操作數棧,然後取出來存到局部變量表中,PC
寄存器中的值也由3
->5
->6
; - 當執行到地址指令爲
6
、7
、8
時,將局部變量表中索引位置爲1
和2
的數據重新加載到操作數棧中並進行iadd
加操作,將得到的結果值存到操作數棧中,PC
寄存器中的值也由6
->7
->8
->9
; - 執行操作指令
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期待你的到來!