概述:
對於從事C、C++開發的程式設計師來說,在記憶體管理領域,他們既是擁有最高權力的“皇帝”,又是從事最基礎工作的勞動人民——既擁有每個物件的“所有權”,
又擔負著每一個物件從開始到終結的維護職責。
對於java程式設計師來說,在虛擬機器自動記憶體管理機制的幫助下,不再需要為沒一個new操作去配對的free/delete(C、C++語言對物件的刪除和記憶體釋放操作),
不容易出現記憶體洩漏和記憶體溢位問題,看起來由虛擬機器管理記憶體一切看起來很美好。不過,也正是java把控制記憶體的權力交給了java虛擬機器,一旦出現記憶體洩漏
和記憶體溢位方面的問題,如果不瞭解虛擬機器是怎麼使用記憶體的,那排查錯誤、修正問題將會成為一項異常艱難的工作。
執行時資料區:
java虛擬機器在執行java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。這些區域有各自的用途,以及建立和銷燬的時間,有些區域會隨著
虛擬機器程式的啟動而一直存在,有些區域則是依賴使用者執行緒的啟動和結束而建立和銷燬。如下圖所示:
我們知道JVM也屬於一種特殊的作業系統,那這些資料區域跟我們最常用的windows哪些部分相對應呢。我們可以吧windows的CPU+快取+主記憶體和JVM的執行引擎+
運算元棧+(棧、堆)對應起來,這樣更加利於我們去理解JVM。
虛擬機器棧:
從上圖可見,java虛擬機器棧是執行緒私有的,它的生命週期和執行緒相同。虛擬機器棧描述的是java方法執行的執行緒記憶體模型:每個方法被執行的時候,java虛擬機器都會
同步建立一個棧幀用於儲存區域性變數表、運算元棧、動態連線、返回地址等資訊。每一個方法被呼叫直至執行完畢的過程,就對應著一個棧幀在虛擬機器棧中從入棧到
出棧的過程。我們來通過一段非常簡短的程式碼來演示虛擬機器棧的作用:
/** * @ClassName StackTest * @description: * @author:liuyi * @Date:2020/11/23 23:45 */ public class StackTest { public static void main(String[] args) { A(); } static void A(){ B(); } static void B(){ C(); } static void C(){ } }
當我們執行main方法,虛擬機器會開啟一個執行緒,同時為當前執行緒劃分一塊記憶體區域作為當前執行緒的虛擬機器棧。同時在執行每個方法的時候都會打包成一個棧幀。
比如 main 開始執行,打包一個棧幀送入到虛擬機器棧。C 方法執行完了,C 方法出棧,接著 B 方法執行完了,B 方法出棧、接著 A 方法執行完了,A 方法出棧,
最後 main 方法執行完了,main 方法這個棧幀就出棧了。這個就是 Java 方法執行對虛擬機器棧的一個影響。虛擬機器棧就是用來儲存執行緒執行方法中的資料的。而
每一個方法對應一個棧幀。入棧過程如下圖所示:
上圖描述了整個main方法呼叫的入棧和出棧的過程,需要注意的是棧幀出棧之後就沒了,棧幀沒得GC的說法。
棧幀詳解:
棧幀大體都包含四個區域:(區域性變數表、運算元棧、動態連線、返回地址)
- 區域性變數表:顧名思義就是區域性變數的表,用於存放我們的區域性變數的(方法中的變數)。首先它是一個 32 位的長度,主要存放我們的 Java 的八大基礎資料
- 運算元棧:存放 java 方法執行的運算元的,它就是一個棧,先進後出的棧結構,運算元棧,就是用來操作的,操作的的元素可以是任意的 java 資料型別,所
- 動態連線:Java 語言特性多型(後續章節細講,需要結合 class 與執行引擎一起來講)。
- 方法出口:正常返回(呼叫程式計數器中的地址作為返回)、異常的話(通過異常處理器表<非棧幀中的>來確定)。
我們來通過分析一個簡單的方法來理解棧幀中各個區域是如何運作的,程式碼如下:
/**
* @ClassName User
* @description:
* @author:liuyi
* @Date:2020/11/25 20:51
*/
public class User {
public static int work(){
int a = 2;
int b = 3;
int c = a*b;
return c;
}
public static void main(String[] args) {
System.out.println(work());
}
}
當該程式執行的時候,JVM會為其分配虛擬機器棧,並生成對應的棧幀,如下圖所示:
我們通過反彙編命令檢視work方法的位元組碼如下:
我們看到work方法一共由10條位元組碼組成,我們來逐步分析。
開啟 https://cloud.tencent.com/developer/article/1333540檢視位元組碼指令
現來看iconst_2對應的含義,如圖
所以第1個位元組碼是將一個值為2的數字載入到運算元棧。再來看 istore_0的含義,如圖
所以第2個位元組碼的含義就是將第一步中放入到運算元棧的數字放到區域性變數表中,位置為0。所以前面兩個位元組碼對應的java程式碼就是int a = 2;那麼顯而易見3和4兩個位元組碼對應的
就是int b = 3;到這裡,大家心裡肯定會有疑問,為什麼不直接將值放到區域性變數表呢?我們接著分析,你就明白了。
繼續來看第5和第6兩個位元組碼:iload_0和iload_1,它們的含義是將區域性變數表中位置0和1的兩個數載入到運算元棧中,接著我們來看關鍵的第7個位元組碼:imul,它代表的意思
是相乘,就是將運算元棧中的數字進行乘法運算,我們知道相乘是需要運算的,所以此時要交給執行引擎運算,運算完成之後再將運算的結果返回到運算元棧。所以運算元棧的作用
就是為jvm高速的計算提供緩衝區。
接著來看第8個位元組碼:istore_2,它的含義就是將計算的結果放入區域性變數表,到這裡int c = a*b;就執行完了。然後再來看第9和第10個位元組碼,它們的含義是將區域性變數表的值再
壓入運算元棧,最後返回。至此,整個方法執行結束,以上就是棧幀中各個區域在方法執行中的運作流程。
虛擬機器棧大小的設定:
虛擬機器棧的大小預設為 1M,可用引數 –Xss 調整大小,例如-Xss256k。
我們可以看到linux的建議配置為1M,至於windows為啥沒有,博主大膽猜想可能跟微軟和Oracel兩家公司競爭有關吧,畢竟微軟開發.net就是和java競爭的。
虛擬機器棧相關的程式異常:
-
StackOverflowError異常:如果執行緒請求的棧深入大於虛擬機器所允許的深度,將丟擲StackOverflowError異常,通常是由無線遞迴導致的,如下面的程式碼
- OutOfMemoryError:如果java虛擬機器的容量可以動態擴充套件,當棧擴充套件時無法申請到足夠的記憶體會丟擲OutOfMemoryError異常。這種情況基本很少出現,也很難模擬,這裡就不演示了。
程式計數器:
與虛擬機器棧一樣,程式計數器也是執行緒私有的。程式計數器是一塊很小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器,就如上面反彙編User.class看到的一樣。每一個位元組碼都有自己的序號:
如上圖所示,雖然這些序號是由順序的,但是並不一定是依次遞增,如果某給位元組碼佔用的空間很大,那麼它的序號相較於前一個序號就差距更大。
在java虛擬機器的概念模型裡,位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,它是程式控制流的執行器,分支、
迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器完成。
它還有另外一個作用,我們知道在java中可以開啟成百上千個執行緒,但是我們一般的電腦CPU也就8個左右。java虛擬機器的多執行緒是通過執行緒輪流切換、
分配處理器執行時間方式來實現的,那麼切換後虛擬機器是怎麼知道以前執行的位置,繼續執行的呢?這個時候,程式計數器就起到了決定性的作用,因為
程式計數器是執行緒獨有的,所以不會相互影響,當切回到當前執行緒,根據程式計數器記錄的序號,繼續執行對應的位元組碼即可。
在JVM中,只有執行java方法的時候,程式計數器才會記錄正在執行的虛擬機器位元組碼指令的地址,如果正在執行的是本地(Native)方法,這個計數器
則應為空(Undefined)。但是這裡會產生一個疑問,如果剛好在執行Native方法的時候執行緒切換了,那切回來之後該怎麼找到對應的位置呢?這裡,我猜測
JVM可能規定了 在執行Native本地方法的時候,禁止切換當前執行緒(如不正確,請指正)。xianc
本地方法棧:
本地方法棧與虛擬機器棧的作用非常相似,其區別只是虛擬機器棧為java方法服務,而本地方法棧專門為Native本地方法服務。需要注意的是,HotSpot直接把
本地方法棧和虛擬機器棧合併了。
總結:
本篇文章介紹了JVM的記憶體區域之執行緒私有區域,主要介紹了虛擬機器棧的各個組成部分以及java方法是怎麼通過虛擬機器棧來實現執行的,接著介紹了程式計數器的作用
最後簡述了本地方法棧。下一章,我們將要分析JVM記憶體區域的執行緒共享資料區,主要包括堆、方法區、執行時常量池以及直接記憶體等內容。