接下來我們繼續深入第二個環節,也就是JVM的內存結構,很多人想到BAT等大廠去面試,但是現在互聯網大廠面試幾乎都會考核JVM相關知識的積累,所在在了解完了JVM的類加載機制之后,我們有必要一起來學習下JVM的內存區域劃分。
其實我們通過類的加載過程也能知道,在準備階段我們的類以及靜態變量都會進行空間的分配,JVM在運行我們的代碼時,是必須要使用多塊內存空間的,不同空間里面存放不同的數據,然后配合我們的代碼流程,完整系統的運行起來。
5.1程序計數器
首先我們來看第一個內存區域:程序計數器
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所要執行的指令地址
通過之前的加載圖進行表示:
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方法的棧幀,同時記錄保存對應的局部變量:
而如果我們在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)
棧內存相關面試案例剖析
-
垃圾回收是否涉及棧內存?
棧幀每次執行結束自動彈棧,所以不會涉及到垃圾的產生,也就不會對棧內存進行垃圾回收
-
棧內存分配越大越好嗎?
并不是,假設分配的物理內存是100MB,每個線程棧大小是1MB,那么可以分配100個線程,但是如果提升了線程棧大小,那可以分配的對應線程數就變少了。
我們先來看官網給出的每個棧幀默認的大小分配:
Linux系統上默認就是1MB,當然我們可以通過-Xss進行大小的更改
-
方法內的局部變量是否線程安全?
- 如果方法內局部變量沒有逃離方法的作用訪問,它是線程安全的
- 如果是局部變量引用了對象,并逃離方法的作用范圍,需要考慮線程安全
參考以下示例代碼:
//方法內局部變量:線程安全 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; }
-
棧內存溢出
什么原因導致棧內存溢出(Stack Overflow)
1)棧幀過多導致內存溢出, 將拋出StackOverflowError異常。
常見的情況就是遞歸調用,不斷產生新的棧幀,前面的棧幀不釋放
我們可以通過以下代碼來測試和實驗:
/**
* @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;
}
}
}
打印結果:
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;
}
}
}
打印結果:
我們發現僅51次就撐爆了!
小結:
無論是由于棧幀太大還是虛擬機棧容量太小, 當新的棧幀內存無法分配的時候,HotSpot虛擬機拋出的都是StackOverflowError異常。