Java虛擬機堆疊是什么?
- Java虛擬機堆疊(Java Virtual Machine Stack),早期也叫Java堆疊,每個執行緒在創建時都會創建一個虛擬機堆疊,其內部保存一個個的堆疊幀(Stack Frame),對應著一次次的Java方法呼叫
生命周期
- 生命周期和執行緒是一致的
作用
- 主管Java程式的運行,他保存方法的區域變數表、部分結果、并參與方法的呼叫和回傳,
記憶體中的堆疊與堆
堆疊是運行時的單位,而堆是存盤的單位,
- 堆疊解決程式的運行問題,及程式如何執行,或者說如何處理資料,
- 堆解決的是資料存盤的問題,即資料怎么放,放在哪里
堆疊的特點
- 堆疊是一種快速有效的分配存盤方式,訪問速度僅此于程式計數器
- JVM直接對Java堆疊的操作只有兩個:
- 每個方法執行,伴隨著進堆疊(入堆疊、出堆疊)
- 執行結束后的出堆疊作業
- 對于堆疊來說不存在垃圾回收問題

虛擬機堆疊可能出現的例外
- Java 虛擬機規范允許Java堆疊的大小是動態的或者固定不變的,
- 如果采用固定大小的Java虛擬機堆疊,那每一個執行緒的Java虛擬機堆疊容量可以在執行緒創建的時候獨立選定,如果執行緒請求分配的堆疊容量超過Java虛擬機允許的最大容量,Jav虛擬機將會拋出StackOverflowError例外,
- 如果Java虛擬機堆疊可以動態擴展,并且在嘗試擴展的時候無法申請到足夠的記憶體,或者在創建新的執行緒時,沒有足夠的記憶體去創建對應的虛擬機堆疊,那Java虛擬機將會拋出一個OutOfMemoryError例外
舉例說明

設定堆疊記憶體大小
我們可以使用引數-Xss選項來設定執行緒的最大堆疊空間,堆疊的大小直接決定了函式呼叫的最大可達深度,

堆疊中存盤了什么?
- 每個執行緒都有自己的堆疊,堆疊中的資料都是以堆疊幀(Stack Frame)的格式存在
- 在這個執行緒上正在執行的每個方法都各自對應一個堆疊幀
- 堆疊幀是一個記憶體區塊,是一個資料集,維系著方法執行程序中各種資料資訊
堆疊運行原理
- JVM直接對Java堆疊的操作只有兩個,就是對堆疊幀的壓堆疊和出堆疊,遵循“先進后出”/"后進先出"原則
- 在一潭訓動執行緒中,一個時間點上,只會有一個活動的堆疊幀,即只有當前正在執行的方法的堆疊幀(堆疊頂堆疊幀)是有效的,這個堆疊幀被稱為當前堆疊幀,與當前堆疊幀相對應的方法稱為當前方法,定義這個方法的類就是當前類,
- 執行引擎運行的所有位元組碼指令只針對當前堆疊幀進行操作
- 如果在該方法中呼叫了其他方法,對應的新的堆疊幀會被創建出來,放在堆疊的頂端,成為新的當前堆疊
- 不同執行緒中所包含的堆疊幀是不允許存在相互參考的,即不可能在一個堆疊幀中參考另外一個執行緒的堆疊幀,
- 如果當前方法呼叫了其他方法,方法回傳之際,當前堆疊幀會傳回此方法的執行結果給前一個堆疊幀,接著,虛擬機會丟棄當前堆疊幀,使得前一個堆疊幀重新成為當前堆疊幀,

堆疊幀的內部結構
- 區域變數表(Local Variables)
- 運算元堆疊(Operand Stack)(或運算式堆疊)
- 動態鏈接(Dynamic Linking)(或者執行運行時常量池的方法參考)
- 方法回傳值(Returan Address)(或方法正常退出或者例外退出的定義)
- 一些附加i資訊


區域變數表
- 區域變數表也被稱之為區域變數陣列或本地變數表
- 定義為一個數字陣列,主要用于存盤方法引數合定義在方法引數和定義在方法體內的區域變數,這些資料型別包括各類基本資料型別,物件參考(reference),以及returnAddress型別.
- 由于區域變數是建立在執行緒的堆疊上,是執行緒的私有資料,因此不存在資料安全問題.
- 區域變數表所需的容量大小是在編譯期確定下來的,并保存在方法的Code屬性的maximum local variables 資料項中. 在方法運行期間是不會改變區域變數表的大小的.
- 方法嵌套呼叫的次數由堆疊的大小決定.一般來說,堆疊越大,方法嵌套呼叫次數越多.對一個函式而言,他的引數和區域變數越多,使得區域變數表膨脹,他的堆疊幀越大,以滿足方法呼叫所需傳遞的資訊增大的需求. 進而函式呼叫酒會占用更多的空間,導致期嵌套呼叫次數就會減少.
- 區域變數表中變數只在當前方法呼叫中有效,在方法執行中,虛擬機通過使用區域變數表完成引數值到引數變數串列的傳遞程序.當方法隨著堆疊幀的銷毀,區域變數表也隨之銷毀.
關于Slot的理解
- 引數值的存放是在區域變數陣列的index0開始,到陣列長度-1結束.
- 區域變數表,最基本的存盤單元是slot(變數槽)
- 區域變數表中存放編譯器可知的各種基本資料型別(8種),參考型別(reference),returnAddress型別的變數.
- 在區域變數表里,32位以內的型別只占用一個slot(包括returnAddress型別),64位的型別(long和double)占用兩個slot
- byte、short、char在存盤前被轉換為int,boolean也被轉為int,0表示false,非0表示true
- long 和 double則占用兩個slot
- JVM會為區域變數表中的每一個slot都分配一個訪問索引,通過這個索引即可成功訪問到區域變數表中指定的區域變數值
- 當一個實體方法被呼叫的時候,他的方法引數和方法體內部定義的區域變數將會按順序被復制到區域變數表中的每一個slot上
- 如果需要訪問區域變數表中一個64bit的區域變數值時,只需要使用前一個索引即可.(比如訪問long或doubkle型別變數)
- 如果當前堆疊幀時由構造器或者實體方法創建的,那么該物件參考this將會存放在index為0的slot處,其余的引數按照引數表順序繼續排列

slot的重復利用
堆疊幀中的區域變數表中的槽位是可以重用的,如果一個區域變數過了其作用域,那么在其作用域之后申明的新的區域變數很有可能會復用過期區域變數的槽位,從而達到節省資源的目的.

- 在堆疊幀中,與性能調優關系最為密切的部分就是前面提到的區域變數表,在方法執行時,虛擬機使用區域變數表完成方法的傳遞
- 區域變數表中的變數也是重要的垃圾回收根節點,只要被區域變數表中直接或間接參考的物件都不會被回收
- 和類變數初始化不同,區域變數表不存在系統初始化的程序,這意味著一旦定義了區域變數則必須認為的初始化,否則無法使用.
#### 靜態變數和區域變數的對比 - 引數表分配完畢之后,再根據方法體內定義的變數的順序和作用域分配,
- 類變數有兩次初始化的時機,第一次是在“準備階段”,執行系統初始化,對壘變數設定零值,另一次則是在“初始化”階段,賦予程式員在代碼中定義的初始值,
- 和類初始化不同的是,區域變數表不存在系統初始化的程序,這意味著一旦定義了區域變數則必須認為的初始化,否則無法使用,

這樣的代碼是錯誤的,沒有賦值是不能使用的,
補充說明
- 在堆疊幀中,與性能調優關系最為密切的部分就是前面提到的區域變數,
- 在方法執行時,虛擬機使用區域變數表完成方法的傳遞
- 區域變數表中的變數也是重要的垃圾回收根節點,只要被區域變數中直接或間接參考的物件都不會被回收,
運算元堆疊
- 每一個獨立的堆疊幀中除了包含區域變數表以外,還包含了一個后進先出(Last-In-First-Out)的運算元堆疊,也可以被稱之為運算式堆疊(Expression Stack),
- 運算元堆疊,在方法執行程序中,根據位元組碼指令,往堆疊中寫入資料或提取資料,,即入堆疊(push)/出堆疊(pop),
- 某些位元組碼指令將值壓入運算元堆疊,其余的位元組碼指令將運算元占取出堆疊,使用他們后再把結果壓入堆疊,
- 比如:執行復制、交換、求和等操作

圖片舉例說明:

運算元堆疊的作用、特點
- 運算元堆疊,主要用于保存計算程序的中間見過,同時作為計算程序中變數臨時的存盤空間,
- 運算元占就是JVM執行引擎的一個作業區,當一個方法剛開始執行的時候,一個新的堆疊幀也會隨著被創建出來,這個方法的運算元堆疊是空的,
- 每一個運算元堆疊都會擁有一個明確的堆疊深度用于存盤數值,其所需的最大深度在編譯器就定義好了,保存在方法的Code屬性中,為max_stack的值,
- 堆疊中的任何一個元素都可以是任意的Java資料型別
- 32bit的型別占用一個堆疊單位深度
- 64bit的型別占用兩個堆疊單位深度
- 運算元堆疊并發采用訪問索引的方式進行資料訪問的,而是只能通過標準的入堆疊(push)和出堆疊(pop)操作來完成一次資料訪問,
- 如果被呼叫的方法帶有回傳值的話,其回傳值將會被壓入當前堆疊幀的運算元堆疊中,并更新PC暫存器中下一條需要執行的位元組碼指令,
- 運算元堆疊中元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載程序中的類檢驗階段的資料列分析階段要再次驗證,
- 另外,我們說Java虛擬機的決議引擎是基于堆疊的執行引擎,其中的堆疊指的是運算元堆疊,
動態鏈接(指向運行時常量池的方法參考)
- 每一個堆疊幀內部包含一個執行運行時常量池中的該堆疊幀所屬方法的參考,包含這個參考的目的就是為了支持當前方法的代碼能夠實作動態鏈接(Dynamic Linking),比如: invokednamic指令
- 在Java源檔案被編譯到位元組碼檔案中時,所有的變數和方法參考都作為符號參考(Symbolic Reference)保存在class檔案的常量池中,比如:描述一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號參考來表示的,那么動態鏈接的作用就是為了將這些符號參考轉換為呼叫方法的直接參考,

為什么需要常量池?
常量池的作用,就是為了提供一些符號和常量,便于指令的識別,
方法呼叫
在JVM中,將符號參考轉換為呼叫方法的直接參考與方法的系結機制相關,
-
靜態鏈接:
- 當一個位元組碼檔案被裝載進JVM內部時,如果被呼叫的目標方法在編譯器可知,且運行期間保持不變時,這種情況下降呼叫方法的符號參考轉換為直接參考的程序稱為靜態鏈接
-
動態鏈接:
- 如果被呼叫的方法在編譯器無法確定下來的,也就是說,只能夠在程式運行期間將呼叫方法的符號參考轉換為直接參考,由于這種參考轉換程序局別動態性,因此也就被稱之為動態鏈接,
-
對應的方法的系結機制為: 早期系結(Early Binding)和晚期系結(Late Binding),
-
系結是一個欄位、方法或者類在符號參考被替換為直接參考的程序,這僅僅發生一次
- 早期系結:
- 早期系結就是指被呼叫的目標方法如果在編譯期可知,且運行期間保持不變時,即可將這個方法與所屬的型別進行系結,這樣一來,由于明確了被呼叫的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號參考轉換為直接參考
- 早期系結:
-
晚期系結
如果被呼叫的方法在編譯期間無法被確定下來,只能夠在程式運行期根據實際的型別系結相關的方法,這種系結關系也就被稱之為晚期系結,
Java中任何一個普通的方法其實都具備虛函式的特征,他們相當于C++語言中的虛函式(C++中則需要使用關鍵字virtual來顯式定義).如果在Jaa程式中不希望某個方法擁有虛函式的特征時,則可以使用關鍵字final來標記這個方法.
虛方法和非虛方法
- 非虛方法
- 如果方法在編譯期間就確定了具體的呼叫版本,這個版本在運行時時不可變的.這樣的方法稱為非虛方法.
- 靜態方法、私有方法、final方法、構造器方法、父類方法都是非虛方法
- 其他方法稱為虛方法
舉例說明非虛方法

虛擬機中提供了以下幾條方法呼叫指令:
- 普通呼叫指令
- invokestatic: 呼叫靜態方法,決議階段確定唯一方法版本
- invokespecial: 呼叫方法、私有方法、決議階段確定唯一方法版本
- invokevirtual: 呼叫所有虛方法
- invokeinterface: 呼叫介面方法
- invokedynamic: 動態決議出需要呼叫的方法,然后執行
- 說明: 前四條指令固化在虛擬機內部,方法的呼叫執行不可認為干預,而invokedynamic指令則支持用戶確定方法版本,其中invokestatic指令和invokespecial指令呼叫的非虛方法,其余的(final修飾的除外)稱為虛方法.
動態型別語言和靜態型別語言
- 動態型別語言和靜態型別語言兩者的區別就在于對型別的檢查是在編譯期還是在運行期,滿足前者就是靜態語言,滿足后者就是動態語言
- 說的直白一點,靜態型別語言是判斷變數自身的型別資訊,**動態型別語言是判斷變數值的型別語言,**變數沒有型別資訊,變數值才有型別資訊,這是動態語言的一個重要特征
方法重寫的本質 - 找到運算元堆疊頂的第一個元素所執行的物件的實際型別,記作C.
- 如果在型別C中找到與常量中的描述符合簡單名稱相符合的方法,則進行訪問權限校驗,如果通過則回傳這個方法的直接參考,查找程序結束;如果不通過,則回傳java.lang.illegalAccessError例外.
- 否則,按照繼承關系從下往上依次對C的各個型別進行第2步的搜索合驗證程序
- 如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError例外.
虛方法表
- 在面向物件的編程中,會很頻繁的使用到動態分派,如果在每次動態分派的程序中都要重新在類的方法元資料中搜索合適的目標方法的話就可能影響到執行效率.因此,為了提高性能,JVM采用在類的方法建立一個虛方法表來實作(非虛方法不會出現在表中),使用索引來代替查找.每個類中都有一個虛方法表,表中存放著各個方法的實際入口
- 那么虛方法表什么時候創建?
- 虛方法表會在類加載的鏈接階段被創建并開始初始化,類的變數初始化值準備完成之后,JVM會把該類的方法表也初始化完畢
方法回傳地址
- 存放呼叫該方法的PC暫存器的值
- 一個方法的結束,有兩種方式:
- 正常執行結束
- 出現未處理的例外,非正常退出
- 無論通過那種方式退出,在方法退出后都回傳到該方法被呼叫的位置.方法正常退出時,呼叫者的pc計數器的值作為回傳地址,即呼叫該方法的指令的下一條指令的地址.而通過例外退出的,回傳地址是要通過例外表來確定,堆疊幀中一般不會保存這部分資訊.
退出方法的兩種方式
- 執行引擎遇到任意一個方法回傳的位元組碼指令(return),會有回傳值傳遞給上一層的方法呼叫者,簡稱正常完成出口
- 一個方法在正常呼叫完成之后,究竟需要使用哪一個回傳值指令還需要根據方法回傳值的實際資料型別而定.
- 在位元組碼指令中,回傳指令包含ireturn(當回傳值是boolean、byte、char、short和int型別時使用)、lreturn、freturn、dreturn以及areturn,另外還有一個return指令供宣告為void的方法、實體初始化方法、類和介面的初始化方法
- 在方法執行的程序中遇到了例外(Exception),并且這個例外沒有在方法內進行處理,也就是只要在本方法的例外表中沒有搜索到匹配的例外處理器,就會導致方法退出,簡稱例外完成出口 .

方法回傳值補充說明
- 本質上,方法的退出就是當前堆疊幀出堆疊的程序.此時,需要恢復上層方法的區域變數表、運算元堆疊、將回傳值壓入呼叫者堆疊幀的運算元堆疊、設定PC暫存器值等,讓呼叫者方法繼續執行下去.
- 正常完成出口和例外完成出口的區別在于: 通過例外完成出口退出的不會給他的上層呼叫者產生任何的回傳值.
一些附帶資訊
- 堆疊幀中還允許攜帶與Java虛擬機實作相關的一些附帶資訊.例如,對程式除錯提供支持的資訊.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/263886.html
標籤:區塊鏈
上一篇:CROSS Mystery Box盲盒游戲手機版+網頁版操作教程
下一篇:Filecoin 家用挖礦可行性
