JVM【带着问题去学习 02】数据结构栈+本地方法栈+虚拟机栈+JVM栈运行原理
1.數據結構棧
棧是一種比較簡單的數據結構,后進先出。棧本身是一個線性表,但是這個表中只有一端允許數據的進出。棧的常用操作包括入棧push和出棧pop,對應于數據的壓入和彈出。由于棧后進先出的特性,??梢宰鳛閿祿僮鞯呐R時容器,對數據的順序進行調控。
2.本地方法棧
2.1 本地方法(Native Method)
本地方法是由非 Java 語言編寫的,編譯成和處理器相關的機器代碼。它保存在動態鏈接庫中,Windows 系統即 .dll 文件中,格式是各個平臺專有的。Java 方法是與平臺無關的,但是本地方法不是,運行中的 Java 方法調用本地方法時虛擬機會裝載包含這個本地方法的動態庫,并調用這個本地方法。
通過本地方法,Java 程序可以直接訪問底層操作系統的資源,調用本地方法會使程序變得與平臺相關,因為本地方法的動態庫是與平臺相關的,此外使用本地方法還可能把程序變得和特定的 Java 平臺實現相關。而本地方法接口(Java Native Interface JNI)使得本地方法可以在特定主機系統的任何一個 Java 平臺實現上運行。如果希望使用特定主機上的資源,它們又無法通過 Java API 訪問,那么可以寫一個平臺相關的 Java 程序來調用本地方法,如果希望保證程序的平臺無關性,那么只能通過 Java API 來訪問底層系統資源。
標識符 native 可以與所有其它的 Java 標識符連用,但是 abstrat 除外。因為 native 表示這些方法是有實現體的,只不過這些實現體是非 Java 的,但是abstract 卻顯然的指明這些方法無實現體。native 與其它 Java 標識符連用時,其意義同非本地并無差別,比如 native static 表明這個方法可以在不產生類的實例時直接調用,比如用一個native method 去調用一個 C 的類庫時。
2.2 本地方法棧(Native Method Stack)
本地方法棧用于管理本地方法的調用是線程私有的,并不是所有 JVM 都支持本地方法。因為 JVM 規范并沒有明確要求本地方法棧的使用語言、具體實現方式、數據結構等。如果 JVM 產品不打算支持 native 方法,也可以無需實現本地方法棧。
-
本地方法是使用C語言實現的
-
它的具體做法是 Native Method Stack 中登記native方法,在 Execution Engine 執行時加載本地方法庫。線程調用本地方法就不再受虛擬機限制了,它和虛擬機擁有同樣的權限。
-
本地方法可以通過 JNI 來訪問虛擬機內部的運行時數據區,它甚至可以直接使用本地處理器中的寄存器,直接從本地內存的堆中分配任意數量的內存。
-
在 Hotspot JVM 中,直接將本地方棧和虛擬機棧合二為一。
本地方法棧與虛擬機棧的作用是相似的,都會拋出OutOfMemoryError和StackOverFlowError,都是線程私有的,主要的區別在于:
- 虛擬機棧執行的是 Java 方法
- 本地方法棧執行的是 native 方法
3.虛擬機棧
虛擬機棧(Java Virtual Machine Stacks)。每個線程在創建的時候都會創建一個虛擬機棧,其內部保存的棧幀(Stack Frame),對應著 Java 方法調用,是線程私有的,生命周期和線程一致。主管 Java 程序的運行,它保存方法的局部變量、部分結果,并參與方法的調用和返回。特點:
- 是一種快速有效的分配存儲方式,訪問速度僅次于程序計數器。
- JVM 直接對虛擬機棧的操作只有兩個:每個方法執行,伴隨著入棧(進棧/壓棧),方法執行結束出棧。
- 棧不存在垃圾回收問題。
棧中可能出現的異常:
Java 虛擬機規范允許 Java虛擬機棧的大小是動態的或者是固定不變的:
- 如果采用固定大小的 Java 虛擬機棧,那每個線程的 Java 虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過 Java 虛擬機棧允許的最大容量,Java 虛擬機將會拋出一個 StackOverflowError 異常
- 如果 Java 虛擬機??梢詣討B擴展,并且在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那 Java 虛擬機將會拋出一個OutOfMemoryError異常
可以通過參數-Xss來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度。
每個**棧幀(Stack Frame)**中存儲著:
- 局部變量表(Local Variables)
- 操作數棧(Operand Stack)(或稱為表達式棧)
- 動態鏈接(Dynamic Linking):指向運行時常量池的方法引用
- 方法返回地址(Return Address):方法正常退出或異常退出的地址
- 一些附加信息
3.1 局部變量表
局部變量表也被稱為局部變量數組或者本地變量表,是一組變量值存儲空間,主要用于存儲方法參數和定義在方法體內的局部變量,包括編譯器可知的各種 Java 虛擬機基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它并不等同于對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此相關的位置)和returnAddress 類型(指向了一條字節碼指令的地址,已被異常表取代)
由于局部變量表是建立在線程的棧上,是線程的私有數據,因此不存在數據安全問題;局部變量表所需要的容量大小是編譯期確定下來的,并保存在方法的 Code 屬性的maximum local variables 數據項中。在方法運行期間是不會改變局部變量表的大小的
方法嵌套調用的次數由棧的大小決定。一般來說,棧越大,方法嵌套調用次數越多。對一個函數而言,它的參數和局部變量越多,使得局部變量表膨脹,它的棧幀就越大,以滿足方法調用所需傳遞的信息增大的需求。進而函數調用就會占用更多的棧空間,導致其嵌套調用次數就會減少。局部變量表中的變量只在當前方法調用中有效。在方法執行時,虛擬機通過使用局部變量表完成參數值到參數變量列表的傳遞過程。當方法調用結束后,隨著方法棧幀的銷毀,局部變量表也會隨之銷毀。
參數值的存放總是在局部變量數組的 index0 開始,到數組長度 -1 的索引結束
局部變量表最基本的存儲單元是Slot(變量槽),在局部變量表中,32位以內的類型只占用一個Slot(包括returnAddress類型),64位的類型(long和double)占用兩個連續的 Slot
- byte、short、char 在存儲前被轉換為int,boolean也被轉換為int,0 表示 false,非 0 表示 true
- long 和 double 則占據兩個 Slot
JVM 會為局部變量表中的每一個 Slot 都分配一個訪問索引,通過這個索引即可成功訪問到局部變量表中指定的局部變量值,索引值的范圍從 0 開始到局部變量表最大的 Slot 數量,當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量將會按照順序被復制到局部變量表中的每一個 Slot 上,如果需要訪問局部變量表中一個64bit的局部變量值時,只需要使用前一個索引即可。(比如:訪問 long 或double 類型變量,不允許采用任何方式單獨訪問其中的某一個 Slot)
如果當前幀是由構造方法或實例方法創建的,那么該對象引用 this 將會存放在 index 為 0 的 Slot 處,其余的參數按照參數表順序繼續排列(這里就引出一個問題:靜態方法中為什么不可以引用 this,就是因為this 變量不存在于當前方法的局部變量表中)棧幀中的局部變量表中的槽位是可以重用的,如果一個局部變量過了其作用域,那么在其作用域之后申明的新的局部變量就很有可能會復用過期局部變量的槽位,從而達到節省資源的目的。
- 在棧幀中,與性能調優關系最為密切的就是局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞
- 局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收
3.2 操作數棧
- 每個獨立的棧幀中除了包含局部變量表之外,還包含一個后進先出(Last-In-First-Out)的操作數棧,也可以稱為表達式棧(Expression Stack)
- 操作數棧,在方法執行過程中,根據字節碼指令,往操作數棧中寫入數據或提取數據,即入棧(push)、出棧(pop)
- 某些字節碼指令將值壓入操作數棧,其余的字節碼指令將操作數取出棧。使用它們后再把結果壓入棧。比如,執行復制、交換、求和等操作
概述
-
操作數棧,主要用于保存計算過程的中間結果,同時作為計算過程中變量臨時的存儲空間
-
操作數棧就是 JVM 執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被創建出來,此時這個方法的操作數棧是空的
-
每一個操作數棧都會擁有一個明確的棧深度用于存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的 Code 屬性的 max_stack 數據項中
-
棧中的任何一個元素都可以是任意的 Java 數據類型
-
- 32bit 的類型占用一個棧單位深度
- 64bit 的類型占用兩個棧單位深度
-
操作數棧并非采用訪問索引的方式來進行數據訪問的,而是只能通過標準的入棧和出棧操作來完成一次數據訪問
-
如果被調用的方法帶有返回值的話,其返回值將會被壓入當前棧幀的操作數棧中,并更新PC寄存器中下一條需要執行的字節碼指令
-
操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證
-
另外,我們說Java虛擬機的解釋引擎是基于棧的執行引擎,其中的棧指的就是操作數棧
棧頂緩存(Top-of-stack-Cashing)
HotSpot 的執行引擎采用的并非是基于寄存器的架構,但這并不代表 HotSpot VM 的實現并沒有間接利用到寄存器資源。寄存器是物理 CPU 中的組成部分之一,它同時也是 CPU 中非常重要的高速存儲資源。一般來說,寄存器的讀/寫速度非常迅速,甚至可以比內存的讀/寫速度快上幾十倍不止,不過寄存器資源卻非常有限,不同平臺下的CPU 寄存器數量是不同和不規律的。寄存器主要用于緩存本地機器指令、數值和下一條需要被執行的指令地址等數據。
基于棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味著將需要更多的指令分派(instruction dispatch)次數和內存讀/寫次數。由于操作數是存儲在內存中的,因此頻繁的執行內存讀/寫操作必然會影響執行速度。為了解決這個問題,HotSpot JVM設計者們提出了棧頂緩存技術,將棧頂元素全部緩存在物理 CPU 的寄存器中,以此降低對內存的讀/寫次數,提升執行引擎的執行效率
3.3 動態鏈接(指向運行時常量池的方法引用)
- 每一個棧幀內部都包含一個指向運行時常量池中該棧幀所屬方法的引用。包含這個引用的目的就是為了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking)。
- 在 Java 源文件被編譯到字節碼文件中時,所有的變量和方法引用都作為符號引用(Symbolic Reference)保存在 Class 文件的常量池中。比如:描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那么動態鏈接的作用就是為了將這些符號引用轉換為調用方法的直接引用
JVM 是如何執行方法調用的
方法調用不同于方法執行,方法調用階段的唯一任務就是確定被調用方法的版本(即調用哪一個方法),暫時還不涉及方法內部的具體運行過程。Class 文件的編譯過程中不包括傳統編譯器中的連接步驟,一切方法調用在 Class文件里面存儲的都是符號引用,而不是方法在實際運行時內存布局中的入口地址(直接引用)。也就是需要在類加載階段,甚至到運行期才能確定目標方法的直接引用。
在 JVM 中,將符號引用轉換為調用方法的直接引用與方法的綁定機制有關
- 靜態鏈接:當一個字節碼文件被裝載進 JVM 內部時,如果被調用的目標方法在編譯期可知,且運行期保持不變時。這種情況下將調用方法的符號引用轉換為直接引用的過程稱之為靜態鏈接
- 動態鏈接:如果被調用的方法在編譯期無法被確定下來,也就是說,只能在程序運行期將調用方法的符號引用轉換為直接引用,由于這種引用轉換過程具備動態性,因此也就被稱之為動態鏈接
對應的方法的綁定機制為:早期綁定(Early Binding)和晚期綁定(Late Binding)。綁定是一個字段、方法或者類在符號引用被替換為直接引用的過程,這僅僅發生一次。
- 早期綁定:早期綁定就是指被調用的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的類型進行綁定,這樣一來,由于明確了被調用的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號引用轉換為直接引用。
- 晚期綁定:如果被調用的方法在編譯器無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式就被稱為晚期綁定。
虛方法和非虛方法
- 如果方法在編譯器就確定了具體的調用版本,這個版本在運行時是不可變的。這樣的方法稱為非虛方法,比如靜態方法、私有方法、final方法、實例構造器、父類方法都是非虛方法
- 其他方法稱為虛方法
虛方法表
在面向對象編程中,會頻繁的使用到動態分派,如果每次動態分派都要重新在類的方法元數據中搜索合適的目標有可能會影響到執行效率。為了提高性能,JVM 采用在類的方法區建立一個虛方法表(virtual method table),使用索引表來代替查找。非虛方法不會出現在表中。
每個類中都有一個虛方法表,表中存放著各個方法的實際入口。
虛方法表會在類加載的連接階段被創建并開始初始化,類的變量初始值準備完成之后,JVM 會把該類的方法表也初始化完畢。
3.4 方法返回地址(return address)
用來存放調用該方法的 PC 寄存器的值。
一個方法的結束,有兩種方式
- 正常執行完成
- 出現未處理的異常,非正常退出
無論通過哪種方式退出,在方法退出后都返回到該方法被調用的位置。方法正常退出時,調用者的 PC 計數器的值作為返回地址,即調用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定的,棧幀中一般不會保存這部分信息。
當一個方法開始執行后,只有兩種方式可以退出這個方法:
執行引擎遇到任意一個方法返回的字節碼指令,會有返回值傳遞給上層的方法調用者,簡稱正常完成出口
一個方法的正常調用完成之后究竟需要使用哪一個返回指令還需要根據方法返回值的實際數據類型而定
在字節碼指令中,返回指令包含 ireturn(當返回值是boolean、byte、char、short和int類型時使用)、lreturn、freturn、dreturn以及areturn,另外還有一個 return 指令供聲明為 void 的方法、實例初始化方法、類和接口的初始化方法使用。
在方法執行的過程中遇到了異常,并且這個異常沒有在方法內進行處理,也就是只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出。簡稱異常完成出口
方法執行過程中拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找到處理異常的代碼。
本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。
正常完成出口和異常完成出口的區別在于:通過異常完成出口退出的不會給他的上層調用者產生任何的返回值
3.5 附加信息
棧幀中還允許攜帶與 Java 虛擬機實現相關的一些附加信息。例如,對程序調試提供支持的信息,但這些信息取決于具體的虛擬機實現。
4.棧運行原理
- JVM 直接對 Java 棧的操作只有兩個,對棧幀的壓棧和出棧,遵循“先進后出/后進先出”原則
- 在一條活動線程中,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀(Current Frame),與當前棧幀對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)
- 執行引擎運行的所有字節碼指令只針對當前棧幀進行操作
- 如果在該方法中調用了其他方法,對應的新的棧幀會被創建出來,放在棧的頂端,稱為新的當前棧幀
- 不同線程中所包含的棧幀是不允許存在相互引用的,即不可能在一個棧幀中引用另外一個線程的棧幀
- 如果當前方法調用了其他方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接著,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成為當前棧幀
- Java 方法有兩種返回函數的方式,一種是正常的函數返回,使用 return 指令,另一種是拋出異常,不管用哪種方式,都會導致棧幀被彈出
IDEA 在 debug 時候,可以在 debug 窗口看到 Frames 中各種方法的壓棧和出棧情況
public class Test {public static void main(String[] args) {test1();}private static void test1() {test2();}private static void test2() {int i = test3();System.out.println(i);}private static int test3() {return 3;} }5.相關問題
為什么要將堆和棧分開,棧不是也可以存儲數據嗎?
1、從軟件設計角度分析,棧代表了處理邏輯,堆代表了數據,這樣分開,使得處理邏輯更清晰。分而治之的思想,這種隔離、模塊化的思想體現在軟件中的很多地方。
2、堆和棧的分離,使得堆的內容可以被多個棧共享(即多個線程訪問同一個對象)。這種共享的收益很多,這種共享提供了一種有效的數據交互方式(共享內存),另一方面,堆中共享的常量和緩存可以被所有棧訪問,節省了內存。
3、棧因為運行是需要,比如保存系統運行的上下文,需要地址段的劃分,由于棧只能向上增長,因此限制住棧存儲內容的能力,而堆是根據需要可以動態增長的,因此棧和堆的拆分,使得堆動態增長成為可能,相應棧只需要記住堆中的一個地址即可。
4、面向對象就是堆和棧的完美結合。其實,面向對象方式的程序與以前結構化的程序在執行上沒有任何區別。但是,面向對象的引入,使得對待問題的思考方式發生了改變,而更接近于自然方式的思考。當我們把對象拆開,你會發現,對象的屬性其實就是數據,存放在堆中;而對象的行為(方法),就是運行邏輯,放在棧中。我們在編寫對象的時候,其實即編寫了數據結構,也編寫的處理數據的邏輯。
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的JVM【带着问题去学习 02】数据结构栈+本地方法栈+虚拟机栈+JVM栈运行原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JVM【带着问题去学习 01】什么是JV
- 下一篇: 【SQL编程】Greenplum 实现树