JVM 的棧與寄存器
1. 前言
從本節開始,我們對運行時數據區進行講解,運行時數據區又可以細分為五個模塊:棧,堆,寄存器,方法區和本地方法棧,本節我們主要針對講解棧(包括 Java 棧與本地方法棧)與寄存器。本節主要知識點如下:
- 了解棧的基本概念及特點,為本節的基礎知識;
- 理解并掌握棧幀的概念以及棧幀的數據結構,并對棧幀結構中的局部變量表,操作數棧,動態鏈接以及返回地址做詳細的講解,為本節核心內容,需要重點學習;
- 理解并掌握寄存器的概念及作用,為本節重點內容。
2. 運行時數據區知識回顧
之前我們在講解 JVM 整體架構的過程中,對運行時數據區進行了總體的概括,運行時數據區又可以細分為五個模塊:棧,堆,寄存器,方法區和本地方法棧,如下圖所示。
本節我們主要針對講解棧(Java 棧與和地方法棧)與寄存器(程序計數器),其他 2 個模塊,方法區和堆會在后續的課程中進行講解。
3. 棧的基本介紹
基本概念:Java 棧有兩個,分別是虛擬機棧和本地方法棧。這里以虛擬機棧為例,本地方法棧和虛擬機?;鞠嗤?。
棧的特點:對于每個線程,將創建單獨的運行時棧。對于每個方法調用,將在棧存儲器中產生一個條目,稱為棧幀。所有局部變量將在棧內存中創建。棧區域是線程安全的,因為它不共享資源。
- Java 虛擬機棧是線程私有的,它的生命周期與線程相同(隨線程而生,隨線程而滅);
- 如果線程請求的棧深度大于虛擬機所允許的深度,將拋出 StackOverflowError 異常;如果虛擬機??梢詣討B擴展,如果擴展時無法申請到足夠的內存,就會拋出 OutOfMemoryError 異常;
- Java 虛擬機棧描述的是 Java 方法執行的內存模型:每個方法執行的同時會創建一個棧幀。對于我們來說,主要關注的棧內存,就是虛擬機棧中局部變量表部分。
Tips:從棧的特點的最后一點可以看到,開發者主要關注的是棧內存,而棧內存的消耗是因為每個方法執行的同時會創建一個棧幀,而占用空間最大的部分就是棧幀的局部變量表部分。后續我們會展開講解。
4. 棧幀
定義:棧幀(Stack Frame)是用于支持虛擬機進行方法調用和方法執行的數據結構。它是虛擬機運行時數據區中的 java 虛擬機棧的棧元素。棧幀存儲了方法的局部變量表、操作數棧、動態鏈接和方法返回地址等信息。
棧幀初始化大小:在編譯程序代碼的時候,棧幀中需要多大的局部變量表內存,多深的操作數棧都已經完全確定了。 因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決于具體的虛擬機實現。
棧幀結構:如下圖所示,在一個線程中,只有位于棧頂的棧幀才是有效的,稱為當前棧幀,與這個棧幀相關聯的方法稱為當前方法。每一個方法從調用開始至執行完成的過程,都對應著一個棧幀在虛擬機里面從入棧到出棧的過程。
從上圖中我們能夠看到,棧幀的組成結構,下文我們將對局部變量表,操作數棧,動態鏈表以及返回地址進行講解。
5. 棧幀 - 局部變量表
在棧幀中,局部變量表占用了大部分的空間,那么接下來我們看下局部變量表的基本概念與特點。
基本概念:每個棧幀中都包含一組稱為局部變量表的變量列表,用于存放方法參數和方法內部定義的局部變量。
特點:
- 局部變量表的容量以變量槽(Variable Slot)為最小單位;
- 在方法執行過程中,Java 虛擬機是使用局部變量表完成參數值到參數變量列表的傳遞過程;
- 局部變量表中的 Slot 是可重用的,方法體中定義的變量,其作用域并不一定會覆蓋整個方法體,如果當前字節碼程序計數器的值已經超過了某個變量的作用域,那么這個變量相應的 Slot 就可以交給其他變量去使用,節省棧空間。
6. 棧幀 - 操作數棧
操作數棧也是棧幀中非常重要的結構,操作數棧不需要占用很大的空間,那么我們一起來看下操作數棧的作用及特點。
- 操作數棧是一個后入先出(Last In First Out)棧,方法的執行操作在操作數棧中完成,每一個字節碼指令往操作數棧進行寫入和提取的過程,就是入棧和出棧的過程;
- 操作數棧的每一個元素可以是任意的 Java 數據類型,32 位數據類型所占的棧容量為 1,64 位數據類型所占的棧容量為 2;
- 當一個方法剛剛執行的時候,這個方法的操作數棧是空的,在方法執行的過程中,通過一些字節碼指令從局部變量表或者對象實例字段中復制常量或者變量值到操作數棧中。
7. 棧幀 - 動態鏈接與返回地址
動態鏈接的基本概念及作用如下:
- 每個棧幀都包含一個指向運行時常量池(JVM 運行時數據區域)中該棧幀所屬方法屬性的引用,持有這個引用是為了支持方法調用過程中的動態鏈接。
- 在 Class 文件格式的常量池(存儲字面量和符號引用)中存有大量的符號引用(1. 類的全限定名,2. 字段名和屬性,3. 方法名和屬性),字節碼中的方法調用指令就以常量池中指向方法的符號引用為參數。
這些符號引用一部分會在類加載過程的解析階段的時候轉化為直接引用(指向目標的指針、相對偏移量或者是一個能夠直接定位到目標的句柄),這種轉化稱為靜態解析。另外一部分將在每一次的運行期期間轉化為直接引用,這部分稱為動態鏈接。
返回地址:返回地址代表的是方法執行結束,方法執行結束有兩種方式,我們來具體看下棧幀中返回地址的作用:
- 當一個方法開始執行后,只有兩種方式可以退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的字節碼指令(例如:return),這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱為調用者),是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。
- 另外一種退出方式是,在方法執行過程中遇到了異常,并且這個異常沒有在方法體內得到處理,無論是 Java 虛擬機內部產生的異常,還是代碼中使用 throw 字節碼指令產生的異常,只要在本方法的異常處理器表中沒有搜索到匹配的異常處理器,就會導致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的上層調用者產生任何返回值的。
- 方法退出的過程實際上就等同于把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的局部變量表和操作數棧,把返回值(如果有的話)壓入調用者棧幀的操作數棧中,調整程序計數器的值以指向方法調用指令后面的一條指令等。
8. 寄存器簡介
寄存器( PC register )基本概念:每個線程啟動的時候,都會創建一個 PC(Program Counter,程序計數器)寄存器。PC 寄存器里保存有當前正在執行的 JVM 指令的地址。
寄存器簡介:
- 每一個線程都有它自己的 PC 寄存器,也是該線程啟動時創建的。保存下一條將要執行的指令地址的寄存器是:PC 寄存器。PC 寄存器的內容總是指向下一條將被執行指令的地址,這里的地址可以是一個本地指針,也可以是在方法區中相對應于該方法起始指令的偏移量;
- 每個線程都有一個寄存器,是線程私有的,其實就是一個指針,指向方法區中的方法字節碼(用來存儲指向下一條指令的地址,以及即將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不記;
- 這塊內存區域很小,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器通過改變這個計數器的值來選取下一條需要執行的字節碼指令。
- 如果執行的是一個 Native 方法,那這個計數器是空的。
9. 寄存器的特點
通過對寄存器的介紹,我們知道,寄存器器是用來存儲指向下一條指令的地址,以及即將要執行的指令代碼。我們來看下寄存器的特點:
- 它是一塊很小的內存空間,幾乎可以忽略不記。也是運行速度最快的存儲區域; -
- 在 JVM 規范中,每個線程都有它自己的程序計數器,是線程私有的,生命周期與線程的生命周期保持一致;
- 任何時間一個線程都只有一個方法在執行,也就是所謂的當前方法。程序計數器會存儲當前線程正在執行的 java 方法的 JVM 指令地址:或者,如果是在執行 native 方法,則是未指定值(undefined);
- 它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成;
- 字節碼解釋器工作時就是通過改變這個計數器的值來選取下一個條需要執行的字節碼指令;
- 它是唯一一個在 Java 虛擬機規范中沒有規定任何 OOM 情況的區域。
10. 小結
本節主要講解了運行時數據區的棧與寄存器,其中棧又包括了 Java 棧和本地方法棧,因為對于 Java 棧和本地方法棧,內存結構是十分相似的,因此放到一起講解。本節內容中的核心知識點 - 棧幀,有非常多的概念問題,需要學習者先做了解,在了解的基礎上,慢慢的消化。