08-JVM的內存結構之程序計數器和虛擬機棧

接下來我們繼續深入第二個環節,也就是JVM的內存結構,很多人想到BAT等大廠去面試,但是現在互聯網大廠面試幾乎都會考核JVM相關知識的積累,所在在了解完了JVM的類加載機制之后,我們有必要一起來學習下JVM的內存區域劃分。

其實我們通過類的加載過程也能知道,在準備階段我們的類以及靜態變量都會進行空間的分配,JVM在運行我們的代碼時,是必須要使用多塊內存空間的,不同空間里面存放不同的數據,然后配合我們的代碼流程,完整系統的運行起來。

5.1程序計數器

image

首先我們來看第一個內存區域:程序計數器

Program Counter Register 程序計數器(PC寄存器)

  • 作用,是記住下一條jvm指令的執行地址
  • 特點
    • 是線程私有的
    • 不會存在內存溢出

首先我們來看一段非常簡單的代碼:

public class Demo1 {
    public static void main(String[] args) {
        int num1 = 1;
        System.out.println(num1);
        int num2 = 2;
        System.out.println(num2);
    }
}

這個代碼大家都能看懂,但是JVM能看懂嗎?答案是:NO!

JVM是不識別我們寫的代碼的,我們的java代碼會被編譯為.class字節碼文件,而字節碼文件中的代碼才是JVM能識別和執行的,這些代碼我們也叫【字節碼指令】,它對應了一條一條的機器指令,JVM通過將這些指令再解釋翻譯為機器指令,來操作我們的計算器進行執行。

上述的代碼對應的字節碼指令如下:

 0 iconst_1
 1 istore_1
 2 getstatic #2 <java/lang/System.out>
 5 iload_1
 6 invokevirtual #3 <java/io/PrintStream.println>
 9 iconst_2
10 istore_2
11 getstatic #2 <java/lang/System.out>
14 iload_2
15 invokevirtual #3 <java/io/PrintStream.println>
18 return

具體的指令含義后續再做講解,我們需要知道的是【程序計數器】就是記錄下一條JVM所要執行的指令地址

通過之前的加載圖進行表示:

image

5.2虛擬機棧

[圖片上傳失敗...(image-b43cd3-1628654314293)]_20210721223011.png)

定義

Java Virtual Machine Stacks (Java 虛擬機棧)

  • 每個線程運行時所需要的內存,稱為虛擬機棧
  • 每個棧由多個棧幀(Frame)組成,對應著每次方法調用時所占用的內存
  • 每個線程只能有一個活動棧幀,對應著當前正在執行的那個方法
解釋

1.每個線程運行時所需要的內存,稱為虛擬機棧 ---> 每個線程都有自己的Java虛擬機棧

Java代碼的執行一定是由線程來執行某個方法中的代碼,哪怕就是我們的main()方法也是有一個主線程來執行的,在main線程執行main()方法的代碼指令的時候,就會通過main線程對應的程序計數器來記錄自己執行的指令位置。

main()方法本質上是一個方法,在main()中也可以調用其他的方法,而每個方法中也有自己的局部變量數據,因此JVM提供了一塊內存區域用來保存每個方法內的局部變量等數據,這個區域就是Java虛擬機棧

2.每個棧由多個棧幀(Frame)組成,對應著每次方法調用時所占用的內存

當我們在線程中調用了一個方法,就會對該方法創建一個對應的棧幀,比如我們如下的代碼:

public class Demo1 {
    public static void main(String[] args) {
        int num1 = 1;
        System.out.println(num1);
        int num2 = 2;
        System.out.println(num2);
    }
}

這時在虛擬機棧內存中就會先創建對應main方法的棧幀,同時記錄保存對應的局部變量:

image

而如果我們在main()方法中調用一個其他的方法:

public class Demo1 {
    public static void main(String[] args) {
        int num1 = 1;
        System.out.println(num1);
        int num2 = 2;
        System.out.println(num2);
        method1();
    }

    public static void method1(){
        int num3 = 20;
        System.out.println("哈哈哈哈");
    }
}

對應的虛擬機棧:

[圖片上傳失敗...(image-ded26f-1628654314293)]_20210721223107.jpg)

并且當method1方法執行完畢后會彈出該棧隊列,最后彈出main()方法棧幀,代表整個main方法代碼執行完畢。這也對應了棧的特點:先進后出

流程圖小結

[圖片上傳失敗...(image-ca8c47-1628654314293)]_20210721223149.png)


棧內存相關面試案例剖析
  1. 垃圾回收是否涉及棧內存?

    棧幀每次執行結束自動彈棧,所以不會涉及到垃圾的產生,也就不會對棧內存進行垃圾回收

  2. 棧內存分配越大越好嗎?

    并不是,假設分配的物理內存是100MB,每個線程棧大小是1MB,那么可以分配100個線程,但是如果提升了線程棧大小,那可以分配的對應線程數就變少了。

    我們先來看官網給出的每個棧幀默認的大小分配:

image

Linux系統上默認就是1MB,當然我們可以通過-Xss進行大小的更改

  1. 方法內的局部變量是否線程安全?

    • 如果方法內局部變量沒有逃離方法的作用訪問,它是線程安全的
    • 如果是局部變量引用了對象,并逃離方法的作用范圍,需要考慮線程安全

    參考以下示例代碼:

      //方法內局部變量:線程安全
        public static void method1(){
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb);
        }
        //方法內局部變量引用對象:線程不安全
        public static void method2(StringBuilder sb){
            sb.append(1);
            sb.append(2);
            sb.append(3);
            System.out.println(sb);
        }
        //方法內局部變量引用對象提供暴露:線程不安全
        public static StringBuilder method3(){
            StringBuilder sb = new StringBuilder();
            sb.append(1);
            sb.append(2);
            sb.append(3);
            return sb;
        }
    
  2. 棧內存溢出

    什么原因導致棧內存溢出(Stack Overflow)

    1)棧幀過多導致內存溢出, 將拋出StackOverflowError異常。

image

常見的情況就是遞歸調用,不斷產生新的棧幀,前面的棧幀不釋放

我們可以通過以下代碼來測試和實驗:

/**
 * @Description:   VM Args: -Xss128k
 對于不同版本的Java虛擬機和不同的操作系統, 棧容量最小值可能會有所限制, 這主要取決于操作系統內存分頁大小。 譬如上述方法中的參數-Xss128k可以正常用于32位Windows系統下的JDK 6, 但是如果用于64位Windows系統下的JDK 11, 則會提示棧容量最小不能低于180K, 而在Linux下這個值則可能是228K, 如果低于這個最小限制, HotSpot虛擬器啟動時會提示:The Java thread stack size specified is too small. Specify at least 228k
 */
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

打印結果:

image

2)棧幀過大導致內存溢出, 將拋出StackOverflowError異常。

我們這次可以嘗試將每一個棧幀的局部變量給多占用一點空間,這樣每個棧幀的大小就會變大,我們還是設定每個線程棧空間為128K,看看以下代碼運行后,多少次就會撐滿內存:

/**
 * @Description: VM Args: -Xss128k
 */
public class JavaVMStackSOF2 {
    private static int stackLength = 0;
    public static void test() {
        long unused1, unused2, unused3, unused4, unused5,
                unused6, unused7, unused8, unused9, unused10,
                unused11, unused12, unused13, unused14, unused15,
                unused16, unused17, unused18, unused19, unused20,
                unused21, unused22, unused23, unused24, unused25,
                unused26, unused27, unused28, unused29, unused30,
                unused31, unused32, unused33, unused34, unused35,
                unused36, unused37, unused38, unused39, unused40,
                unused41, unused42, unused43, unused44, unused45,
                unused46, unused47, unused48, unused49, unused50,
                unused51, unused52, unused53, unused54, unused55,
                unused56, unused57, unused58, unused59, unused60,
                unused61, unused62, unused63, unused64, unused65,
                unused66, unused67, unused68, unused69, unused70,
                unused71, unused72, unused73, unused74, unused75,
                unused76, unused77, unused78, unused79, unused80,
                unused81, unused82, unused83, unused84, unused85,
                unused86, unused87, unused88, unused89, unused90,
                unused91, unused92, unused93, unused94, unused95,
                unused96, unused97, unused98, unused99, unused100;
        stackLength++;
        test();
    }

    public static void main(String[] args) throws Throwable {
        try {
            test();
        }catch (Error e){
            System.out.println("stack length:" + stackLength);
            throw e;
        }
    }
}

打印結果:

image

我們發現僅51次就撐爆了!

小結:

無論是由于棧幀太大還是虛擬機棧容量太小, 當新的棧幀內存無法分配的時候,HotSpot虛擬機拋出的都是StackOverflowError異常。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內容