JVM-- 棧幀

2020-08-14 19:09:37

棧幀的內部結構

在这里插入图片描述
每個棧幀中儲存着

  • 區域性變數表(Local Variables)
  • 運算元棧(Operand Stack)(或表達式棧)
  • 動態鏈接(Dynamic Linking)(或執行"執行時常數池"的方法參照)----深入理解Java多型特性必讀!!
  • 方法返回地址(Return Adress)(或方法正常退出或者異常退出的定義)
  • 一些附加資訊

部分參考書目上,稱方法返回地址、動態鏈接、附加資訊爲幀數據區
在这里插入图片描述

1 區域性變數表

1.區域性變數表也被稱之爲區域性變數陣列或本地變數表,他是一個一維表,跟數據庫中的二維表不一樣

2.定義爲一個數字陣列,主要用於儲存方法參數和定義在方法體內的區域性變數這些數據型別包括各類基本數據型別、物件參照(reference),以及returnAddressleixing

3.由於區域性變數表是建立線上程的棧上,是執行緒私有的數據,因此不存在數據安全問題

4.區域性變數表所需的容量大小是在編譯期確定下來的,並儲存在方法的Code屬性的maximum local variables數據項中。在方法執行期間是不會改變區域性變數表的大小的

5.方法巢狀呼叫的次數由棧的大小決定。一般來說,棧越大,方法巢狀呼叫次數越多。對一個函數而言,他的參數和區域性變數越多,使得區域性變數表膨脹,它的棧幀就越大,以滿足方法呼叫所需傳遞的資訊增大的需求。進而函數呼叫就會佔用更多的棧空間。

6.區域性變數表中的變數只在當前方法呼叫中有效。在方法執行時,虛擬機器通過使用區域性變數表完成參數值到參數變數列表的傳遞過程。當方法呼叫結束後,隨着方法棧幀的銷燬,區域性變數表也會隨之銷燬。

檢視棧幀的區域性變數表

利用javap命令對位元組碼檔案進行解析檢視main()方法對應棧幀的區域性變數表,如圖:
在这里插入图片描述
也可以在IDEA 上安裝jclasslib byte viewcoder外掛檢視方法內部位元組碼資訊剖析,以main()方法爲例

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

變數槽slot的理解與演示

1.參數值的存放總是在區域性變數陣列的index0開始,到陣列長度-1的索引結束

2.區域性變數表,最基本的儲存單元是Slot(變數槽)

3.區域性變數表中存放編譯期可知的各種基本數據型別(8種),參照型別(reference),returnAddress型別的變數。

4.在區域性變數表裏,32位元以內的型別只佔用一個slot(包括returnAddress型別),64位元的型別(long和double)佔用兩個slot。

  • byte、short、char、float在儲存前被轉換爲int,boolean也被轉換爲int,0表示false,非0表示true;
  • long和double則佔據兩個slot。

在这里插入图片描述
5.JVM會爲區域性變數表中的每一個slot都分配一個存取索引,通過這個索引即可成功存取到區域性變數表中指定的區域性變數值

6.當一個實體方法被呼叫的時候,它的方法參數和方法體內部定義的區域性變數將會按照宣告順序被複制到區域性變數表中的每一個slot上

7.如果需要存取區域性變數表中一個64bit的區域性變數值時,只需要使用前一個索引即可。(比如:存取long或者double型別變數)

8.如果當前幀是由構造方法或者實體方法建立的(意思是當前幀所對應的方法是構造器方法或者是普通的實體方法),那麼該物件參照this將會存放在index爲0的slot處,其餘的參數按照參數表順序排列。

9.靜態方法中不能參照this,是因爲靜態方法所對應的棧幀當中的區域性變數表中不存在this

slot的重複利用

棧幀中的區域性變數表中的槽位是可以重複利用的,如果一個區域性變數過了其作用域,那麼在其作用域之後申明的新的區域性變數就很有可能會複用過期區域性變數的槽位,從而達到節省資源的目的。

private void test2() {
        int a = 0;
        {
            int b = 0;
            b = a+1;
        }
        //變數c使用之前以及經銷燬的變數b佔據的slot位置
        int c = a+1;
    }

問題:
上述程式碼對應的棧幀中區域性變數表中一共有多少個slot,或者說區域性變數表的長度是幾?
答案:3

補充說明

  • 在棧幀中,與效能調優關係最爲密切的部分就是區域性變數表。在方法執行時,虛擬機器使用區域性變數表完成方法的傳遞
  • 區域性變數表中的變數也是重要的垃圾回收根節點,只要被區域性變數表中直接或間接參照的物件都不會被回收

2 運算元棧

1.棧 :可以使用陣列或者鏈表來實現

2.每一個獨立的棧幀中除了包含區域性變數表以外,還包含一個後進先出的運算元棧,也可以稱爲表達式棧

3.運算元棧定義:在方法執行過程中,根據位元組碼指令,往棧中寫入數據或提取數據,即入棧(push)或出棧(pop)

程式碼舉例
在这里插入图片描述

運算元棧特點

  • 運算元棧,主要用於儲存計算過程的中間結果,同時作爲計算過程中變數臨時的儲存空間。
  • 運算元棧就是jvm執行引擎的一個工作區,當一個方法開始執行的時候,一個新的棧幀也會隨之被建立出來,這個方法的運算元棧是空的
  • 每一個運算元棧都會擁有一個明確的棧深度用於儲存數值,其所需的最大深度在編譯前就定義好了,儲存在方法的code屬性中,爲max_stack的值。
  • 棧中的任何一個元素都可以是任意的java數據型別
    • 32bit的型別佔用一個棧單位深度
    • 64bit的型別佔用兩個棧深度單位
  • 運算元棧並非採用存取索引的方式來進行數據存取的,而是隻能通過標準的入棧push和出棧pop操作來完成一次數據存取
  • 如果被呼叫的方法帶有返回值的話,其返回值將會被壓入下一個棧幀的運算元棧中,並更新PC暫存器中下一條需要執行的位元組碼指令。
  • 運算元棧中的元素的數據型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類載入過程中的類驗證階段的數據流分析階段要再次驗證。
  • 另外,我們說Java虛擬機器的解釋引擎是基於棧的執行引擎,其中的棧指的就是運算元棧。

運算元棧程式碼追蹤

結合上圖結合下面 下麪的圖來看一下一個方法(棧幀)的執行過程

①15入棧;②儲存15,15進入區域性變數表
注意:區域性變數表的0號位被構造器佔用,這裏的15從區域性變數表1號開始

在这里插入图片描述
③壓入8;④8出棧,儲存8進入區域性變數表;
在这里插入图片描述

⑤從區域性變數表中把索引爲1和2的是數據取出來,放到運算元棧;⑥iadd相加操作
在这里插入图片描述

⑦iadd操作結果23出棧⑧將23儲存在區域性變數表索引爲3的位置上istore_3
在这里插入图片描述

棧頂快取技術ToS(Top-of-Stack Cashing)

  • 基於棧式架構的虛擬機器所使用的零地址指令(即不考慮地址,單純入棧出棧)更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味着將需要更多的指令分派(instruction dispatch)次數和記憶體讀/寫次數
  • 由於運算元是儲存在記憶體中的,因此頻繁地執行記憶體讀/寫操作必然會影響執行速度。爲了解決這個問題,HotSpot JVM的設計者們提出了棧頂快取技術,將棧頂元素全部快取在物理CPU的暫存器中,以此降低對記憶體的讀/寫次數,提升執行引擎的執行效率

3 動態鏈接

執行時常數池位於方法區(注意: JDK1.7 及之後版本的 JVM 已經將執行時常數池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放執行時常數池。)

位元組碼中的常數池結構如下:
在这里插入图片描述
爲什麼需要常數池呢?
常數池的作用,就是爲了提供一些符號和常數,便於指令的識別。下面 下麪提供一張測試類的執行時位元組碼檔案格式

在这里插入图片描述
在这里插入图片描述
2.每一個棧幀內部都包含一個指向執行時常數池Constant pool或該棧幀所屬方法的參照。包含這個參照的目的就是爲了支援當前方法的程式碼能夠實現動態鏈接。比如invokedynamic指令

3.在Java原始檔被編譯成位元組碼檔案中時,所有的變數和方法參照都作爲符號參照(symbolic Refenrence)儲存在class位元組碼檔案(javap反編譯檢視)的常數池裏。比如:描述一個方法呼叫了另外的其他方法時,就是通過常數池中指向方法的符號參照來表示的,那麼動態鏈接的作用就是爲了將這些符號參照(#)最終轉換爲呼叫方法的直接參照。

在这里插入图片描述

方法的呼叫(重點)

在JVM中,將符號參照轉換爲呼叫方法的直接參照與方法的系結機制 機製相關

  • 靜態鏈接
    當一個 位元組碼檔案被裝載進JVM內部時,如果被呼叫的目標方法在編譯期可知,且執行期保持不變時。這種情況下將呼叫方法的符號參照轉換爲直接參照的過程稱之爲靜態鏈接。
  • 動態鏈接
    如果被呼叫的方法在編譯期無法被確定下來,也就是說,只能夠在程式執行期將呼叫方法的符號參照轉換爲直接參照,由於這種參照轉換過程具備動態性,因此也就被稱之爲動態鏈接。

對應的方法的系結機制 機製爲:早期系結(Early Binding)和晚期系結(Late Bingding)。

  • 早期系結
    早期系結就是指被呼叫的目標方法如果在編譯期可知,且執行期保持不變時,即可將這個方法與所屬的型別進行系結,這樣一來,由於明確了被呼叫的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號參照轉換爲直接參照。
  • 晚期系結
    如果被呼叫的方法在編譯期無法被確定下來,只能夠在程式執行期根據實際的型別系結相關的方法,這種系結方式也就被稱之爲晚期系結。

隨着高階語言的橫空出世,類似於java一樣的基於物件導向的程式語言如今越來越多,儘管這類程式語言在語法風格上存在一定的差別,但是它們彼此之間始終保持着一個共性,那就是都支援封裝,整合和多型等物件導向特性,既然這一類的程式語言具備多型特性,那麼自然也就具備早期系結和晚期系結兩種系結方式。

Java中任何一個普通的方法其實都具備虛擬函式的特徵,它們相當於C++語言中的虛擬函式(C++中則需要使用關鍵字virtual來顯式定義)。如果在Java程式中不希望某個方法擁有虛擬函式的特徵時,則可以使用關鍵字final來標記這個方法。

虛方法與非虛方法

  • 非虛方法

    如果方法在編譯器就確定了具體的呼叫版本,這個版本在執行時是不可變的。這樣的方法稱爲非虛方法
    靜態方法、私有方法、final方法、範例構造器(範例已經確定,this()表示本類的構造器)、父類別方法(super呼叫)都是非虛方法

  • 虛方法
    其他所有體現多型特性的方法稱爲虛方法

子類物件的多型性使用前提:

  • ①類的繼承關係(父類別的宣告)②方法的重寫(子類的實現)

實際開發編寫程式碼中用的介面,實際執行是匯入的的三方jar包已經實現的功能

位元組碼中方法呼叫指令

普通呼叫指令:

  • invokestatic:呼叫靜態方法,解析階段確定唯一方法版本;
  • invokespecial:呼叫方法、私有及父類別方法,解析階段確定唯一方法版本;
  • invokevirtual呼叫所有虛方法;
  • invokeinterface:呼叫介面方法;

動態呼叫指令(Java7新增)

  • invokedynamic:動態解析出需要呼叫的方法,然後執行 .

前四條指令固化在虛擬機器內部,方法的呼叫執行不可人爲幹預,而invokedynamic指令則支援由使用者確定方法版本。

其中invokestatic指令和invokespecial指令呼叫的方法稱爲非虛方法

其中invokevirtual(final修飾的除外,JVM會把final方法呼叫也歸爲invokevirtual指令,但要注意final方法呼叫不是虛方法)、invokeinterface指令呼叫的方法稱稱爲虛方法。

關於invokedynamic指令

  • JVM位元組碼指令集一直比較穩定,一直到java7才增加了一個invokedynamic指令,這是Java爲了實現【動態型別語言】支援而做的一種改進
  • 但是java7中並沒有提供直接生成invokedynamic指令的方法,需要藉助ASM這種底層位元組碼工具來產生invokedynamic指令.直到Java8的Lambda表達式的出現,invokedynamic指令的生成,在java中纔有了直接生成方式
  • Java7中增加的動態語言型別支援的本質是對java虛擬機器規範的修改,而不是對java語言規則的修改,這一塊相對來講比較複雜,增加了虛擬機器中的方法呼叫,最直接的受益者就是執行在java平臺的動態語言的編譯器

動態型別語言和靜態型別語言

  • 動態型別語言和靜態型別語言兩者的區別就在於對型別的檢查是在編譯期還是在執行期,滿足前者就是靜態型別語言,反之則是動態型別語言。
  • 直白來說 靜態語言是判斷變數自身的型別資訊;動態型別語言是判斷變數值的型別資訊,變數沒有型別資訊,變數值纔有型別資訊,這是動態語言的一個重要特徵
  • Java是靜態型別語言(儘管lambda表達式爲其增加了動態特性),js,python是動態型別語言.
Java:String info = "硅谷";//靜態語言

JS:var name = "硅谷「;var name = 10;//動態語言

Pythom: info = 130;//更加徹底的動態語言

方法重寫的本質

  • 1.找到運算元棧的第一個元素所執行的物件的實際型別,記作C。
  • 2.如果在型別C中找到與常數池中的描述符、簡單名稱都相符的方法,則進行存取許可權校驗,如果通過則返回這個方法的直接參照,查詢過程結束;如果不通過,則返回java.lang.IllegalAccessError異常。
  • 3.否則,按照繼承關係從下往上依次對c的各個父類別進行第二步的搜尋和驗證過程。
  • 4.如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。 IllegalAccessError介紹 程式檢視存取或修改一個屬性或呼叫一個方法,這個屬性或方法,你沒有許可權存取。一般的,這個會引起編譯器異常。這個錯誤如果發生在執行時,就說明一個類發生了不相容的改變。

虛方法表

  • 在物件導向程式設計中,會很頻繁期使用到動態分派,如果在每次動態分派的過程中都要重新在累的方法元數據中搜尋合適的目標的話就可能影響到執行效率。因此,爲了提高效能,jvm採用在類的方法區建立一個虛方法表(virtual method table)(非虛方法不會出現在表中)來實現。使用索引表來代替查詢。
  • 每個類中都有一個虛方法表,表中存放着各個方法的實際入口。
  • 那麼虛方法表什麼時候被建立? 虛方法表會在類載入的鏈接階段被建立 並開始初始化,類的變數初始值準備完成之後,jvm會把該類的虛方法表也初始化完畢。

例子

下圖即爲dog類的虛方法表,由於Dog重寫了toString和sayHello方法,當執行String和sayHello方法時,直接呼叫Dog類裏面的方法;而對於其他方法來說,直接呼叫Dog的父類別Object的對應方法。如果沒有虛方法表,則會通過Dog類向上尋找,直到滿足執行情況,纔會停止,會浪費資源或時間。
在这里插入图片描述

3 方法返回地址(Return Address)

  • 存放呼叫該方法的PC暫存器的值。

  • 一個方法的結束,有兩種方式:

    • 正常執行完成
    • 出現未處理的異常,非正常退出
  • 無論通過哪種方式退出,在方法退出後都返回到該方法被呼叫的位置。方法正常退出時,呼叫者(方法的呼叫者可能也是一個方法)的pc計數器的值作爲返回地址,即呼叫該方法的指令的下一條指令的地址。而通過異常退出時,返回地址是要通過異常表來確定,棧幀中一般不會儲存這部分資訊。

  • 本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的區域性變數表、運算元棧、將返回值入呼叫者棧幀的運算元棧、設定PC暫存器值等,讓呼叫者方法繼續執行下去。

  • 正常完成出口和異常完成出口的區別在於:通過異常完成出口退出的不會給他的上層呼叫者產生任何的返回值。

當一個方法開始執行後,只有兩種方式可以退出這個方法

1.執行引擎遇到任意一個方法返回的位元組碼指令(return),會有返回值傳遞給上層的方法呼叫者,簡稱正常完成出口;

  • 一個方法在正常呼叫完成之後究竟需要使用哪一個返回指令還需要根據方法返回值的實際數據型別而定
  • 在位元組碼指令中,返回指令包含ireturn(當返回值是boolena、byte、char、short和int型別時使用)、lreturn、freturn、dreturn以及areturn(參照型別的)
  • 另外還有一個return指令供宣告爲void的方法、範例初始化方法、類和介面的初始化方法使用

在这里插入图片描述
2.在方法執行的過程中遇到了異常(Exception),並且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜素到匹配的例外處理器,就會導致方法退出,簡稱異常完成出口
方法執行過程中拋出異常時的例外處理,儲存在一個異常處理表,方便在發生異常的時候找到處理異常的程式碼。

我們寫一個demo演示:
在这里插入图片描述

位元組碼當中的異常處理表:下表的行號不是上圖的程式碼的行號,而是其對應位元組碼當中的行號
在这里插入图片描述
在位元組碼當中的4~8行是可能存在異常的程式碼,11代表位元組碼中能夠處理該異常的位置是第11行也就是上圖中的第72行

4 一些附加資訊

棧幀中還允許攜帶與java虛擬機器實現相關的一些附加資訊。例如,對程式偵錯提供支援的資訊。(很多資料都忽略了附加資訊)

come from https://www.cnblogs.com/yanl55555/p/12615658.html
and https://www.bilibili.com/video/BV1PJ411n7xZ?p=48