一、運行時數據區
我們在編寫Java程序時,使用JVM的流程主要如下所示:
虛擬機在執行Java程序時,會把它所管理的內存劃分為不同的數據區域,即運行時數據區。有些數據區域是線程共享的,即這些區域會隨著虛擬機的啟動而創建,隨著虛擬機的關閉而銷毀。而另一些區域則是與線程對應,屬于線程私有的。這些區域會隨著線程開始而創建,隨著線程的結束而銷毀。
具體的劃分如下:
多個線程共享的:堆、方法區
每個線程私有的:程序計數器、Java虛擬機棧、本地方法棧
圖示如下:
關于Java線程:在虛擬機中,每個線程都與操作系統的本地線程是直接對應的。當Java的線程準備執行時,操作系統的線程也同時創建了。
二、程序計數器
程序計數器是一塊很小的內存空間,可以認為是當前線程所執行的字節碼的行號指示器,負責保存當前線程正在執行的字節碼地址。
1、程序計數器的作用
① Java是支持多線程的,這也意味著CPU會不停地切換線程。虛擬機的多線程是通過CPU時間片輪轉法來實現的,即每個線程占用CPU的時間是同等的,時間一到就會切換線程,如此重復,直到線程終止。這也意味著某個線程會因為時間片到而被掛起,所以當該線程再次獲得時間片時,需要知道從哪個地方繼續執行,此時虛擬機只需要讀取程序計數器的值就可以知道要執行的字節碼指令的位置了。這也是程序計數器是線程私有的原因,因為若程序計數器不是線程私有的話,當CPU切換線程時,會按照上一個線程的字節碼指令的位置來執行當前線程的字節碼指令,這顯然是不正確的
② JVM的字節碼解釋器也需要通過改變程序計數器的值來明確下一條字節碼指令。
2、程序計數器的特點
① 程序計數器是數據區中唯一沒有規定OutOfMemoryError
(內存溢出異常)的區域。
②如果一個線程執行的是Native本地方法,那么程序計數器的值為undefined。因為JVM在執行Native本地方法時,是通過JNI調用本地其他語言來實現的,而不是字節碼。
三、虛擬機棧
棧是運行時的單位,堆是存儲的單位。
棧解決程序的運行問題,即方法如何執行(或者如何處理數據)。
堆解決的是數據存儲問題,即數據怎么放,在那放。
1、什么是虛擬機棧
虛擬機棧在每個線程開始時隨之創建,虛擬機棧與數據結構中的棧一致,也是遵循先進后出的原則。
虛擬機棧進行出棧入棧操作的元素就是棧幀。每一個棧幀就對應著一個方法,也可以將棧幀理解為一個方法的運行空間。
一個棧幀入棧,意味著一個方法被調用,一個棧幀出棧,即一個方法執行完畢。則棧幀的入棧順序就是方法的調用順序。
有代碼:
public class VMStackTest {
private static int i = 1;
public static void main(String[] args) {
VMStackTest test = new VMStackTest();
test.add();
System.out.println(i);
}
public void add(){
i++;
}
}
代碼所示的虛擬機棧示例如下:
同一時刻,在同一線程中,只有位于虛擬機棧頂部的棧幀才是有效運行的,即只有位于棧頂的方法是正在執行的。執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作。 如果在被調用的A方法中,又調用了B方法,那么對應的B方法的棧幀就會被創建,并被放在虛擬機棧的棧頂,成為新的正在執行的方法。
對于虛擬機棧來說,不存在垃圾回收問題。
Java方法有兩種方法返回方式,一種是正常的方法返回,使用return指令;另一種是拋出異常。不管是哪一種,都會導致棧幀被彈出。
2、棧幀的內部結構
每個棧幀都存儲著:
① 局部變量表
②操作數棧
③方法返回地址
④動態鏈接
⑤一些附加信息
圖示如下:
(1)、局部變量表
局部變量表定義為一個數字數組,用于存放方法參數和方法內定義的局部變量。
局部變量表建立在線程的虛擬機棧上,是線程的私有數據,因此不存在數據安全問題。
虛擬機通過索引定位的方法查找相應的局部變量,索引的范圍是從0~局部變量表最大容量。
局部變量表所需的容量大小是在編譯期定下的,方法運行期間是不會該變局部變量表的大小的。
(2)、操作數棧
操作數棧也是一個棧的數據結構,操作數棧的最大深度也在編譯的時候定下。
操作數棧的每一個元素可以是任意Java數據類型,在方法執行過程中,操作數棧的深度都不會超過最大值。
操作數棧的作用是: 隨著方法執行和字節碼指令的執行,會從局部變量表或對象實例的字段中復制常量或變量寫入到操作數棧,再隨著計算的進行將棧中元素出棧到局部變量表或者返回給方法調用者,也就是出棧/入棧操作。一個完整的方法執行期間往往包含多個這樣出棧/入棧的過程。
(3)、方法返回地址
一個方法結束,有正常執行完畢退出和拋出異常(沒catch),非正常退出兩種。
無論是那種方式,在退出后都會返回到該方法被調用的位置。
正常退出時,調用者的程序計數器的值會作為返回地址,即調用該方法的指令的下一條指令的地址。但是異常退出時,返回地址要通過異常表來確定。
實際上,方法的退出就是當前棧幀出棧的過程。故需要恢復上一層棧幀的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧,并設置其程序計數器的值等,讓調用者棧幀繼續執行下去。
兩種方法結束的區別是:因拋出異常而退出的方法不會給調用他的調用者棧幀任何返回值
3、虛擬機棧的常見異常
《Java虛擬機規范》中,允許虛擬機棧的大小是動態擴展的或者固定不變的。
這意味著,虛擬機棧會出現兩種異常:
① StackOverflowError
異常(棧溢出):
如果線程請求分配的棧容量超過Java虛擬機的最大容量時,就會拋出棧溢出異常。最簡單的示例就是不停遞歸而不退出,就會報 StackOverflowError
。
當虛擬機棧的大小為固定時,易出現該異常。
②OutOfMemoryError
異常(內存溢出)
當虛擬機棧采用動態擴展時,一定程度上可以規避 StackOverflowError
,但是虛擬機給每個線程分配到的內存空間是有限的,這也意味著隸屬于線程的虛擬機棧的內存也是有限的,所以當虛擬機棧請求擴展的內存大小無法滿足時,就會報該異常。
但是并不意味著,采用固定大小的虛擬機棧就不會報該異常,如果某個棧幀太大,超出虛擬機棧所有的內存也是可能的。
四、本地方法棧
(1)、本地方法
一個Native Method就是一個Java調用非Java代碼的接口。在定義一個本地方法時,并不提供實現體(有些就像定義一個Java interface),因為實現體是由非Java語言在外面實現的。本地接口的作用是融合不同的編程語言為Java所用。
(2)、本地方法棧
虛擬機棧是用于管理Java方法的調用,本地方法棧是用于管理本地方法的調用。
①本地方法棧是線程私有的
②本地方法棧也是可實現固定或者可動態擴展的內存大小(這也意味著本地方法棧的異常也與虛擬機棧一致)
③本地方法棧存放著被native關鍵字標記的方法,在執行引擎執行時加載本地方法庫。
④并不是所有的JVM都支持本地方法,故也無需實現本地方法棧
⑤在HotSpot JVM中,直接將本地方法和虛擬機棧合二為一。
五、擴展--設置棧內存大小
有代碼如下:
public class StackOverError {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
在代碼中采用了遞歸,以count++
的次數來查看目前虛擬機棧的深度。其結果如下:
可見默認情況下,
count
的值為2467。我們可以使用
-Xss:來規定了每個線程虛擬機棧的內存大小
如下,
設置棧的大小:--Xss1m或者-Xss1k (設置方法IDEA:Run --> Edit Configuration --> VM option)
結果如下,
可見當設置虛擬機棧的內存大小為2m時,
count
的值為233424。
故可看出使用參數 -Xss可以設置線程的最大棧空間,且虛擬機棧的大小直接決定了函數調用的大小。