1. 程式人生 > >【虛擬機器棧】虛擬機器棧詳解

【虛擬機器棧】虛擬機器棧詳解

前言

Java 虛擬機器的記憶體模型分為兩部分:一部分是執行緒共享的,包括 Java 堆和方法區;另一部分是執行緒私有的,包括虛擬機器棧和本地方法棧,以及程式計數器這一小部分記憶體。

JVM 是基於棧的。但是這個“棧” 具體指的是什麼?難道就是虛擬機器棧?想要回答這個問題我們先要從虛擬機器棧的結構談起。

虛擬機器棧

何為虛擬機器棧

虛擬機器棧的棧元素是棧幀,當有一個方法被呼叫時,代表這個方法的棧幀入棧;當這個方法返回時,其棧幀出棧。因此,虛擬機器棧中棧幀的入棧順序就是方法呼叫順序。什麼是棧幀呢?棧幀可以理解為一個方法的執行空間。它主要由兩部分構成,一部分是區域性變量表,方法中定義的區域性變數以及方法的引數就存放在這張表中;另一部分是運算元棧,用來存放運算元。我們知道,Java 程式編譯之後就變成了一條條位元組碼指令,其形式類似彙編,但和彙編有不同之處:彙編指令的運算元存放在資料段和暫存器中,可通過儲存器或暫存器定址找到需要的運算元;而 Java 位元組碼指令的運算元存放在運算元棧中,當執行某條帶 n 個運算元的指令時,就從棧頂取 n 個運算元,然後把指令的計算結果(如果有的話)入棧。因此,當我們說 JVM 執行引擎是基於棧的時候,其中的“棧”指的就是運算元棧。舉個簡單的例子對比下彙編指令和 Java 位元組碼指令的執行過程,比如計算 1 + 2,在彙編指令是這樣的:

12mov ax, 1 ;把 1 放入暫存器 axadd ax, 2 ;用 ax 的內容和 2 相加後存入 ax

而 JVM 的位元組碼指令是這樣的:

123iconst_1 //把整數 1 壓入運算元棧iconst_2 //把整數 2 壓入運算元棧iadd //棧頂的兩個數相加後出棧,結果入棧

由於運算元棧是記憶體空間,所以位元組碼指令不必擔心不同機器上暫存器以及機器指令的差別,從而做到了平臺無關。

注意,區域性變量表中的變數不可直接使用,如需使用必須通過相關指令將其載入至運算元棧中作為運算元使用。比如有一個方法 void foo(),其中的程式碼為:int a = 1 + 2; int b = a + 3;,編譯為位元組碼指令就是這樣的:

123456789iconst_1 //把整數 1 壓入運算元棧iconst_2 //把整數 2 壓入運算元棧iadd //棧頂的兩個數出棧後相加,結果入棧;實際上前三步會被編譯器優化為:iconst_3istore_1 //把棧頂的內容放入區域性變量表中索引為 1 的 slot 中,也就是 a 對應的空間中iload_1 // 把區域性變量表索引為 1 的 slot 中存放的變數值(3)載入至運算元棧iconst_3 iadd //棧頂的兩個數出棧後相加,結果入棧istore_2 // 把棧頂的內容放入區域性變量表中索引為 2 的 slot 中,也就是 b 對應的空間中return // 方法返回指令,回到呼叫點

需要說明的是,區域性變量表以及運算元棧的容量的最大值在編譯時就已經確定了,執行時不會改變。並且區域性變量表的空間是可以複用的,例如,當指令的位置超出了局部變量表中某個變數 a 的作用域時,如果有新的區域性變數 b 要被定義,b 就會覆蓋 a 在區域性變量表的空間。

盜用別人的圖以讓大家對虛擬機器棧有個直觀的認識(其中小字型 Stack 指的的是虛擬機器棧,Frame 是棧幀,Local variables 是區域性變量表,Operand Stack 是運算元棧):

虛擬機器棧

由虛擬機器棧引出的問題

看完上面的程式碼大家可能會有幾點疑惑:什麼是 slot?那些指令是什麼意思?為什麼 a 對應的 slot 的索引值不是從零開始的,它明明是第一個定義的變數啊?

對於這些問題我們一個個來解決。

什麼是 slot

首先什麼是 slot?slot 是區域性變量表中的空間單位,虛擬機器規範中有規定,對於 32 位之內的資料,用一個 slot 來存放,如 int,short,float 等;對於 64 位的資料用連續的兩個 slot 來存放,如 long,double 等。引用型別的變數 JVM 並沒有規定其長度,它可能是 32 位,也有可能是 64 位的,所以既有可能佔一個 slot,也有可能佔兩個 slot。

JVM 位元組碼指令

第二個問題,那些指令是什麼意思?

指令格式

首先我們要理解 Java 指令的格式,Java 的指令以位元組為單位,也就是一個位元組代表一條指令。比如 iconst_1 就是一條指令,它佔一個位元組,那麼自然 Java 指令不會超過 256 條。實際上 Java 指令目前定義了 200 多條。指令雖然是一個位元組,但是它也可以帶自己的運算元。JVM 中有這樣一條指令 putstatic,其作用是給特定的的靜態欄位賦值。但是給哪個欄位賦值呢?僅僅通過這條指令並不能說明,那麼只有通過運算元來指定了。緊跟在 putstatic 後面的兩個位元組就是它的運算元,這個運算元是一個索引值,指向執行時常量池中該靜態欄位對應的符號引用。由於符號引用包含了該欄位的基本資訊,如所屬類、簡單名稱以及描述符,因此 putstatic 指令就知道是給哪個類的哪個欄位賦值了。

指令的運算元分兩種:一種是嵌入在指令中的,通常是指令位元組後面的若干個位元組;另一種是存放在運算元棧中的。為了區別,我們把前者叫做嵌入式運算元,把後者叫做棧內運算元。這兩者的區別是:嵌入式運算元是在編譯時就已經確定的,執行時不會改變,它和指令一樣存放於類檔案方法表的 Code 屬性中;而運算元是執行時確定的,即程式在執行過程中動態生成的。拿 putstatic 指令來說,它有一個嵌入式運算元,該運算元是一個索引值(前面已經提到),它由兩個位元組組成,緊跟在 putstatic 對應的位元組之後;同時它還有一個棧內運算元,位於運算元棧的棧頂,這個運算元就是要賦給靜態欄位的值,其對應的位元組數根據靜態欄位的型別決定。如果靜態欄位的型別是 short、int、boolean、char 或者 byte,那麼這個運算元就必須是 int 型別,即由棧頂的 4 個位元組組成;如果是 float、double 或者 long 型別,那麼運算元就是相應的型別,即由棧頂的 4 個、8 個 或者 8 個 位元組組成;如果靜態欄位是引用型別,那麼這個運算元的型別也必須是引用型別,即由棧頂的 8 個位元組組成。

再舉一個例子。iconst_<i> 代表了一個指令族,它的意思是把整數 i 放入運算元棧中,i 的範圍是(m1, 0, 1, 2, 3, 4, 5),其中 m1 代表的是 -1。注意,這裡的 i 並不是指令的運算元(即非嵌入式運算元,也非棧內運算元),如 iconst_1、iconst_2 和 iconst_3 都是由一個位元組組成的位元組碼指令。我們可以把 i 可以看作是指令的 “隱含運算元”,即指令本身就蘊含了運算元。如果整數 i 超過 [-1, 5] 這個範圍,就不能用 iconst_<i> 表示了,因為僅一個位元組的位元組碼指令不可能蘊含所有的整數。此時就需要 bipush 這條指令了,這條指令有一個嵌入式運算元,由一個位元組組成,用來表示要放入棧頂的那個整數,該整數放入棧頂時通過擴充套件符號位變為 32 位的整型。但是一個位元組也表示不了所有的整數,如果整數值超過一個位元組所能表示的範圍,就只能通過 ldc 這條指令了,這條指令帶有一個位元組的嵌入式運算元,它代表的是一個指向執行時常量池中 Constant_Integer_info 型別常量的索引,通過索引的方式引用執行時常量池中的整數,再大的整數也不怕了。

閱讀指令文件

授之以魚不如授之以漁,在這裡不可能將所有的指令都講解一番,因此我和大家分享一下如何閱讀 oracle 官網關於位元組碼指令的文件吧。文件的地址是:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

我們拿 astore 指令來說: 關於它的文件描述如下:

說明和翻譯:

  • 第一行的粗體字是指令的名稱;
  • Operation 是指令的功能:把引用存入本地變數中;
  • Format 是指令的格式:它的第一個位元組是指令,名稱為 astore,第二個位元組是指令的嵌入式運算元,名稱為 index;Forms 指的是指令的十進位制(十六進位制)碼,astore 的十進位制(十六進位制)碼是 58(0x3a);
  • Operation Stack 是指令執行前後的運算元棧的狀態:第一行代表的是指令執行前運算元的狀態,第二行是指令執行後運算元棧的狀態,箭頭是棧頂方向。astore 執行前棧頂是物件引用 objectRef,它是 astore 的棧內運算元,執行後 objectRef 被彈出並存入區域性變量表中;
  • Description 是對這條指令的描述:index 是無符號位元組,這個 index 必須指向當前棧幀的區域性變量表的某個位置。運算元棧的棧頂的那個引用值必須是 returnAddress(方法返回地址)或者是 reference (物件引用)。這個引用會被彈出,其值會被存入區域性變量表中索引為 index 的 slot 中;
  • Notes 是注意事項:實現 Java 中的 finally 子句時,astore 指令使用的運算元型別是一個 returnAddress,與 astore 對應的 aload 指令(將區域性變量表的的引用值壓棧)不能將型別為 returnAddress 型別的值載入到運算元棧,而只能是 reference 型別。aload 和 astore 這種不對稱的設計是有意而為之的。astore 指令可以和 wide 指令配合使用以用無符號雙位元組型別的索引來獲取區域性變量表中的變數。

區域性變量表的第一個變數

從 Java 語言的層面講,靜態方法和例項方法的本質區別在於是否是物件所共享的。而從 JVM 的角度來看,方法(無論靜態方法還是例項方法)其實都是物件共享的,例項變數才是物件私有的。對 JVM 而言,靜態方法和例項方法的本質區別在於是否需要和具體物件關聯:靜態方法可以通過類名來呼叫,它不需要和具體物件關聯;而例項方法必須通過物件來進行呼叫,它需要和具體物件關聯。那麼,例項方法和具體物件是如何產生關聯的呢?其實很簡單,編譯器在編譯時會將方法接收者作為一個隱含引數傳入該例項方法,這個引數在方法中有一個很熟悉的名字,叫做 “this”。之所以例項方法可以訪問該類的例項變數和其它例項方法,正是因為它有 “this” 這個隱含引數。舉個例子,類 A 中的某個方法 b 需要訪問例項變數 x,由於例項變數是物件私有的,如果 b 是靜態方法,由於它沒有具體物件的引用,它並不知道該訪問哪個物件的例項變數 x;如果 b 是例項方法,通過隱含引數 this 就能確定要訪問的例項變數是 this.x。那麼,為什麼靜態方法也不能呼叫該類的例項方法呢?本質原因也是沒有 this 引用。因為呼叫例項方法的前提是要傳入一個隱含引數,例項方法本來就有這個引用,所以能夠把它作為隱含引數傳入另一個例項方法;靜態方法沒有 this 引用,無法給例項方法提供指向方法接收者的隱含引數,因此不能呼叫例項方法。

如果看懂了上面說的那些,第三個問題也就迎刃而解了。因為我們定義的方法是 void foo(),它是例項方法,因此會有一個指向具體物件的隱含引數 this,this 就存放在區域性變量表的第一個位置,即存放在索引為 0 的 slot 中,又由於它的作用域從方法開始一直到方法結束,因此它在區域性變量表中的位置不會被其他變數覆蓋,從而使得我們在方法中定義的變數只能放在區域性變量表後面的位置中。需要注意的是,如果方法有引數(非隱含引數),那麼引數會按順序緊接著 this 存放在區域性變量表中,由於引數作用域也是整個方法體,所以方法中定義的區域性變數就只能放在引數後面了。總的來說區域性變量表中變數的存放順序為: this(如果是例項方法)=> 引數(如果有的話)=> 定義的區域性變數(如果有的話)。

-----------------------------------------------------------------------------------------------------------------------------------------------------

虛擬機器棧

    其中,虛擬機器棧是一個後入先出的棧。棧幀是儲存在虛擬機器棧中的,棧幀是用來儲存資料和儲存部分過程結果的資料結構,同時也被用來處理動態連結(Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)。執行緒執行過程中,只有一個棧幀是處於活躍狀態,稱為“當前活躍棧幀”,當前活動棧幀始終是虛擬機器棧的棧頂元素。如下圖所示: 棧流程

棧幀

    運算元棧,動態連線,方法返回地址和一些額外的附加資訊。 如下圖所示:

棧幀詳情

1.區域性變量表

    區域性變量表是一組區域性變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數。在Java檔案編譯為Class檔案時,就在方法表的Code屬性的max_locals資料項中確定了該方法需要分配的最大區域性變量表的容量。

2.運算元棧

    運算元棧也常被稱為操作棧,它是一個後入先出棧。JVM底層位元組碼指令集是基於棧型別的,所有的操作碼都是對運算元棧上的資料進行操作,對於每一個方法的呼叫,JVM會建立一個運算元棧,以供計算使用。和區域性變數一樣。運算元棧的最大深度也是編譯的時候寫入到方法表的code屬性的max_stacks資料項中。運算元棧的每一個元素可以是任意的Java資料型別,包括long、double。32位資料型別所佔的棧容量為1,64位資料型別所佔的棧容量為2。棧容量的單位為“字寬”,對於32位虛擬機器來說,一個“字寬”佔4個位元組,64位虛擬機器來說,一個“字寬”佔8個位元組。當一個方法剛剛執行的時候,這個方法的運算元棧是空的,在方法執行的過程中,會有各種位元組碼指向運算元棧中寫入和提取值,也就是入棧與出棧操作。例如,在做算術運算的時候就是通過運算元棧來進行的,又或者呼叫其它方法的時候是通過運算元棧來行引數傳遞的。 另外,在概念模型中,兩個棧幀作為虛擬機器棧的元素,相互之間是完全獨立的,但是大多數虛擬機器的實現裡都會作一些優化處理,令兩個棧幀出現一部分重疊。讓下棧幀的部分運算元棧與上面棧幀的部分區域性變量表重疊在一起,這樣在進行方法呼叫返回時就可以共用一部分資料,而無須進行額外的引數複製傳遞了。

3.動態連線

    每個棧幀都包含一個指向執行時常量池中該棧幀所屬性方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線。在Class檔案的常量池中存有大量的符號引用,位元組碼中的方法呼叫指令就以常量池中指向方法的符號引用為引數。這些符號引用一部分會在類載入階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜態解析。另外一部分將在每一次的執行期期間轉化為直接引用,這部分稱為動態連線

4.方法返回地址

    當一個方法被執行後,有兩種方式退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的位元組碼指令,這時候可能會有返回值傳遞給上層的方法呼叫者(呼叫當前方法的的方法稱為呼叫者),是否有返回值和返回值的型別將根據遇到何種方法返回指令來決定,這種退出方法方式稱為正常完成出口(Normal Method Invocation Completion)。另外一種退出方式是,在方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理,無論是Java虛擬機器內部產生的異常,還是程式碼中使用athrow位元組碼指令產生的異常,只要在本方法的異常表中沒有搜尋到匹配的異常處理器,就會導致方法退出,這種退出方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的呼叫都產生任何返回值的。     無論採用何種方式退出,在方法退出之前,都需要返回到方法被呼叫的位置,程式才能繼續執行,方法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,呼叫者PC計數器的值就可以作為返回地址,棧幀中很可能會儲存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會儲存這部分資訊。 方法退出的過程實際上等同於把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的區域性變量表和運算元棧,把返回值(如果有的話)壓入呼叫都棧幀的運算元棧中,呼叫PC計數器的值以指向方法呼叫指令後面的一條指令等。

5.附加資訊

    虛擬機器規範允許具體的虛擬機器實現增加一些規範裡沒有描述的資訊到棧幀中,例如與高度相關的資訊,這部分資訊完全取決於具體的虛擬機器實現。在實際開發中,一般會把動態連線,方法返回地址與其它附加資訊全部歸為一類,稱為棧幀資訊。


相關推薦

機器學習梯度下降法

一、導數 導數 就是曲線的斜率,是曲線變化快慢的一個反應。 二階導數 是斜率變化的反應,表現曲線的 凹凸性 y

機器學習主成分分析

一、PCA簡介 1. 相關背景 主成分分析(Principal Component Analysis,PCA), 是一種統計方法。通過正交變換將一組可能存在相關性的變數轉換為一組線性不相關的變數,轉換後的這組變數叫主成分。 上完陳恩紅老師的《機器學習與知識發現》和季

強化學習篇--強化學習案例

AC 沒有 技術 技術分享 ron png strong http mage 一、前述 本文通過一個案例來講解Q-Learning 二、具體 1、案例 假設我們需要走到5房間。 轉變為如下圖:先構造獎勵,達到5,即能夠走得5的action則說明獎勵比較高設置成100,沒有

小家javaBlockingQueue阻塞佇列以及5大實現(ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue...)

相關閱讀 【小家java】java5新特性(簡述十大新特性) 重要一躍 【小家java】java6新特性(簡述十大新特性) 雞肋升級 【小家java】java7新特性(簡述八大新特性) 不溫不火 【小家java】java8新特性(簡述十大新特性) 飽受讚譽 【小家java】java9

系統學習SpringBootSpringBoot定時任務

強大的SpringBoot對定時任務這種常用的功能做了很好的封裝,,只需三步即可完成 一、新增依賴 pom.xml檔案中新增如下依賴: <dependencies> <dependency> <

大數據分析學習之路

聯網 相同 規劃 mach 漏鬥圖分析 環境 分析方法 hbase reduce 以大數據分析師為目標,從數據分析基礎、JAVA語言入門和linux操作系統入門知識學起,系統介紹Hadoop、HDFS、MapReduce和Hbase等理論知識和hadoop的生態環境   一

虛擬機器虛擬機器

前言Java 虛擬機器的記憶體模型分為兩部分:一部分是執行緒共享的,包括 Java 堆和方法區;另一部分是執行緒私有的,包括虛擬機器棧和本地方法棧,以及程式計數器這一小部分記憶體。JVM 是基於棧的。但是這個“棧” 具體指的是什麼?難道就是虛擬機器棧?想要回答這個問題我們先要

深入理解JVM虛擬機器讀書筆記第八章虛擬機器位元組碼執行引擎

8.1 概述 8.2 執行時棧幀結構 8.2.1 區域性變量表 8.2.2 運算元棧 8.2.3 動態連線 8.2.4 方法返回地址

深入理解JVM虛擬機器讀書筆記第七章虛擬機器類載入機制

7.1 概述 7.2 類載入的時機 7.3 類載入的過程 7.3.1 載入 7.3.2 驗證 1.檔案格式驗證 2.元資料驗證 3.位元組碼驗證

深入理解 Java 虛擬機器筆記虛擬機器位元組碼執行引擎

7.虛擬機器位元組碼執行引擎 執行引擎是 Java 虛擬機器最核心的組成部分之一。在 Java 虛擬機器規範中制定了虛擬機器位元組碼執行引擎的概念模型,這個概念模型成為各種虛擬機器執行引擎的統一外觀(Facade)。不同的虛擬機器實現,執行引擎可能會有解釋執行和編譯執行兩種,有可能兩

深入理解 Java 虛擬機器筆記虛擬機器效能監控與故障處理工具

3.虛擬機器效能監控與故障處理工具 定位問題時,知識和經驗是關鍵基礎、資料(執行日誌、異常堆疊、GC日誌、執行緒快照、堆轉儲快照)是依據、工具是運用知識處理資料的手段。 思維導圖 JDK的命令列工具 jps: 虛擬機器程序狀況工具 jps(JVM Proce

深入Java虛擬機器之記憶體區域(Eden Space、Survivor Space、Old Gen、Code Cache和Perm Gen)

1.記憶體區域劃分 限定商用虛擬機器基本都採用分代收集演算法進行垃圾回收。根據物件的生命週期的不同將記憶體劃分為幾塊,然後根據各塊的特點採用最適當的收集演算法。大批物件死去、少量物件存活的,使用複製演算法,複製成本低;物件存活率高、沒有額外空間進行分配擔保的,採用標記-清除演算法

慕課網初始機器學習.md

實體 str 實時 語言 采樣 alt imageview 信貸 監督學習 初始機器學習 什麽是機器學習 什麽是機器學習? 機器學習指的是計算機對歷史數據進行統計分析,找出規律,建立模型,最關鍵的是可以對未來不確定性場景進行判斷和決策 具體可見:什麽是機器學習 那什麽是不確

VMware虛擬機三種網絡模式

童鞋 tro 修改 遠程連接 www 學習交流 退出 con 現在 由於Linux目前很熱門,越來越多的人在學習Linux,但是買一臺服務放家裏來學習,實在是很浪費。那麽如何解決這個問題?虛擬機軟件是很好的選擇,常用的虛擬機軟件有VMware Workstations和

匯編語言——用機器指令和匯編指令編程

tps 課本 bubuko 任務 什麽 aid 同時 data 次方 初入大二,剛剛接觸和學習匯編語言這門課程,肯定有很多的不足和漏洞;本篇文章是關於王爽編著的《匯編語言》(第三版)第二章的章後實驗的實驗報告和總結。 一 實驗環境的配置和測試 Debug是DOS,Wi

LeetCode 簡單題62-用實現佇列

宣告: 今天是第62道題。使用棧實現佇列的相關操作。以下所有程式碼經過樓主驗證都能在LeetCode上執行成功,程式碼也是借鑑別人的,在文末會附上參考的部落格連結,如果侵犯了博主的相關權益,請聯絡我刪除 (手動比心ღ( ´・ᴗ・` )) 正文 題目:使用棧實現佇列的下列操作:

linuxValgrind工具集(十):SGCheck(檢查和全域性陣列溢位)

一、概述 SGCheck是一種用於檢查棧中和全域性陣列溢位的工具。它的工作原理是使用一種啟發式方法,該方法源於對可能的堆疊形式和全域性陣列訪問的觀察。 棧中的資料:例如函式內宣告陣列int a[10],而不是malloc分配的,malloc分配的記憶體是在堆中。 SGCheck和Me

劍指offer兩個實現佇列

用兩個棧來實現一個佇列,完成佇列的Push和Pop操作。 佇列中的元素為int型別。 public class Solution {        Stack<Integer> stack1 = new Stack<In

劍指offer判斷出序列是否合法

輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不可能是該壓棧序列的彈出序列。(注意:這兩個序列的長度是相等的) &n

機器學習實戰FP-growth演算法

Here is code 背景 apriori演算法 需要多次掃描資料,I/O 大大降低了時間效率 1. fp-tree資料結構 1> 項頭表 記錄所有的1項頻繁集出現的次數,並降序排列 2> fp tree 根據項頭表,構建fp樹 3>