JVM學習筆記——虛擬機棧

虛擬機棧

背景

因爲跨平臺性的設計,JAVA的指令都是根據棧來設計的。
優勢:跨平臺,指令集小,編譯器容易實現。
缺點:性能降低,實現一樣的功能須要更多指令。java

棧和堆

棧是運行時的單位,堆是儲存的單位。數組

簡介

每一個線程在建立的時候都會建立一個虛擬機棧,其內部保存一個個的棧幀,對應着一次次的Java方法調用。
虛擬機棧主管Java程序的運行,它保存方法的局部變量,部分結果並參與方法的調用和返回。緩存

特色
  • 棧是一種快速有效的分配儲存方式,訪問速度僅次於程序計數器。
  • JVM直接對虛擬機棧只有兩個操做。每一個方法執行,伴隨着進棧,方法執行結束伴隨着出棧。
  • 對於棧來講不存在垃圾回收問題,由於只有出棧入棧兩個操做。
  • JVM規範容許虛擬機棧的大小是動態的或是固定不變的。
    1.若是採用固定不變的,若是線程請求分配的棧容量超過了JVM容許的最大容量,JVM則會拋出一個StackOverflowError異常。
    2.若是採用動態擴展,在線程請求分配的站容量超過容許的最大容量,虛擬機棧會嘗試向JVM申請內存,但在申請時沒法得到足夠的內存,或者在建立新線程時沒有足夠的內存去建立對應的虛擬機棧,JVM則會拋出一個OutOfMemoryError異常。
設置棧內存大小

咱們可使用參數-Xss來設置虛擬機棧的大小。
IDEA能夠在run -> Edit Configurations進行設置。
Xss.png安全

虛擬機棧的存儲單位

棧幀:
  • 每一個線程都有本身的棧,棧中的數據都是以棧幀的格式存在的。
  • 在這個線程上正在執行的每一個方法都各自對應一個棧幀。
  • 棧幀是一個內存區塊,是一個數據集。
棧幀運行原理:
  • JVM對虛擬機棧的操做只有入棧和出棧兩個操做。
  • 在一條活動的線程中,一個時間點只有一個活動的棧幀,只有當前正在執行方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱爲當前棧幀,與其對應的方法被稱爲當前方法,定義這個方法的類被稱爲當前類。
  • 執行引擎運行的全部字節碼指令只針對當前棧幀進行操做。
  • 若是在當前方法中調用了新方法,對應的新棧幀會建立出來併入棧,稱爲新的當前棧幀。
  • 不一樣線程所包含的棧是不容許互相引用的。
  • 當前方法調用了其餘方法,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,而後當前棧幀出棧,前一個棧幀稱爲當前棧幀。
  • JAVA有兩種返回函數的方式,一種是正常的return指令返回。另外一種是拋出未處理的異常。不論是哪一種返回方式都會致使棧幀出棧。
入棧出棧測試代碼

public class StackFrameTest {架構

public static void main(String[] args) {
    StackFrameTest test = new StackFrameTest();
    test.methon1();
}

private void methon1() {
    System.out.println("方法一開始執行");
    methon2();
    System.out.println("方法一執行結束");
}
private void methon2() {
    System.out.println("方法二開始執行");
    methon3();
    System.out.println("方法二執行結束");
}
private void methon3() {
    System.out.println("方法三開始執行");
    System.out.println("方法三執行結束");
    }
}
  • 測試代碼中,每出現方法調用,都會有新棧幀入棧,直到當前方法執行結束以後,原方法從新稱爲當前方法後繼續執行。在IDEA中的DEBUG的Frames窗口中,也形象的顯示了方法入棧的操做。後附圖。
  • 代碼中方法若是出現異常,且當前方法中沒有處理,則會拋給前一個棧幀,若是前一個棧幀也沒有處理則繼續拋出,直到main函數,若仍未處理,則程序異常中止,由控制檯打印異常信息。

棧debug.png

棧幀的內部結構

每一個棧幀中存儲着如下內容:函數

  • 局部變量表
  • 操做數棧
  • 動態連接
  • 方法返回地址
  • 一些其餘的附加信息

局部變量表和操做數棧主要影響着棧幀的大小,棧幀的大小影響着虛擬機棧能存放多少棧幀。性能

局部變量表

局部變量表也被稱爲局部變量數組或本地變量表。測試

  • 定義爲一個數字數組,主要用於存儲方法參數和定義在方法體內的局部變量。這些數據類型包括基本數據類型,對象引用和returnAdress類型。
  • 局部變量表線程私有,不存在數據安全問題。
  • 局部變量表所需的容量大小是在編譯期就肯定下來的。
  • 在棧幀中,與性能調優關係最密切的就是局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞。
  • 局部變量表中的變量也是重要的垃圾回收根節點,只要被局部變量表中直接或間接引用的對象都不會被回收。
局部變量表測試代碼

public class LocalVariablesTest {this

public static void main(String[] args) {
    LocalVariablesTest test = new LocalVariablesTest();
    int i = 10;
    }
}

javap反編譯後查看局部變量表,結果如圖:
局部變量表.png
其中,spa

  • Start表示在執行指令該變量開始生效的指令行號。
  • Length表示該變量生效的行數。
  • Slot表示該變量的索引。
  • Name表示該變量的名稱。
  • Signature表示該變量的類型。
  • [:表示是個數組。L表示是引用變量。I表示是int類型。

除了javap指令,咱們也能夠經過JClasslib插件查看局部變量表。
jclasslib.png
局部變量表內容同上,start和length兩個屬性決定變量的做用域,變量在start標誌的下一行開始生效。

slot
  • 局部變量表,最基本的單位是Slot(變量槽)
  • 在局部變量表中,32位之內的類型只佔一個slot,64位的類型佔兩個slot。
  • JVM會爲局部變量表中的每個slot分配一個訪問索引,經過這個索引訪問指定的額局部變量值。
  • 當一個實例方法被調用的時候,它的方法參數和方法體內部定義的局部變量會按照順序複製到局部變量表中的每個slot上。
  • 若是須要訪問一個64位的局部變量值,只須要使用前一個索引便可。例:一個long類型的變量佔據了4,5兩個slot,咱們使用4這個索引即可以找到這個變量。一樣的,咱們須要6這個索引來找到這個long類型以後的那個變量,而不是使用5。
  • 若是當前幀是由構造方法或者實例方法建立的,那麼該對象的引用變量this會存放在索引爲0的slot處。

類方法爲何不能使用this關鍵字?
由於在類方法的局部變量表中不存在this變量。

slot的重複利用

棧幀中的局部變量表中的slot是能夠重用的,若是一個局部變量過了其做用域,那麼在其做用域後申請的新局部變量就頗有可能複用這個過時變量的槽位。測試代碼:

public void test() {
        int a = 1;
        {
            int b = 0;
            b = a + 1;
        }
        int c = a + 1;
    }

測試代碼的局部變量圖以下:
slot複用.png
經過索引,咱們能夠看出c複用了b的slot。

類變量和局部變量的對比

變量的分類:
根據數據類型分類:

  • 基本數據類型
  • 引用數據類型

根據在類中聲明的位置分類:

  • 成員變量

    • 類變量:在類加載的連接準備階段已經給類變量賦初值,並在初始化階段顯式賦值。
    • 實例變量:隨着對象的建立,在堆中分配實例變量空間,並進行默認賦值。
  • 局部變量:在使用前必須顯式賦值!不然編譯不經過。

操做數棧

  • 使用數組實現,在方法執行過程當中,根據字節碼指令執行,往棧中寫入數據或提取數據。
  • 主要用於保存計算過程的中間結果,同時做爲計算過程當中變量臨時的存儲空間。
  • 操做數棧在一個方法剛開始執行的時候便建立出來了,這個方法的操做數棧是空的。
  • 操做數棧有一個明確的棧深度用於儲存數值,最大深度在編譯器就肯定好了。
  • 操做數棧並不是採用索引的方式來進行數據訪問,而只能經過入棧和出棧操做完成一次數據訪問。
測試代碼
public void testadd() {
       byte i = 15;
       int j = 8;
       int k = i + j;
    }

反編譯後的字節碼指令以下:

0 bipush 15
 2 istore_1
 3 bipush 8
 5 istore_2
 6 iload_1
 7 iload_2
 8 iadd
 9 istore_3
10 return

字節碼指令解析:
0:將數值15壓入操做數棧。
2: 將操做數棧的當前棧幀壓入局部變量表,索引爲1。
3:將數值8壓入操做數棧。
5:將操做數棧的當前棧幀壓入局部變量表,索引爲2。
6:讀取局部變量表中索引爲1的變量進操做數棧。
7:讀取局部變量表中索引爲2的變量進操做數棧。
8:執行加操做,並將結果壓入操做數棧。
9:將操做數棧的當前棧幀壓入局部變量表,索引爲3。
10:方法正常返回結束。

棧頂緩存技術
  • 基於棧式架構的虛擬機完成一項操做的時候須要使用更多的入棧和出棧操做,會出現更多的讀寫操做。
  • 將棧頂元素所有緩存到物理的寄存器中,下降對內存的讀寫次數,提高執行引擎的執行效率。

動態連接

有些地方可能會將方法返回地址,動態連接和附加信息稱爲幀數據區。

  • 每個棧幀內部都包含一個指向運行時常量池中該站真所述方法的引用,包含這個引用的目的就是爲了支持當前方法的代碼可以實現動態連接。
  • 在java源文件被編譯成字節碼文件時,全部變量和方法引用都做爲符號引用保存在class文件的常量池裏。動態連接的做用就是爲了將這些符號引用轉換爲直接引用。
測試代碼
public class DynamicLinkingTest {

    int num = 1;
    
    public void methonA() {
        System.out.println("methonA");
    }
    
    public void methonB() {
        System.out.println("methonB");
        methonA();
        num++;
    }
}

反編譯以後的執行指令以下:

Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String methonB
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: aload_0
         9: invokevirtual #7                  // Method methonA:()V
        12: aload_0
        13: dup
        14: getfield      #2                  // Field num:I
        17: iconst_1
        18: iadd
        19: putfield      #2                  // Field num:I
        22: return

咱們能夠看到在指令後出現大量的#開頭的符號引用,這些引用能夠在編譯後的常量池中找到對應的實際方法,測試代碼編譯後的常量池以下圖:
常量池.png

爲何要有運行時常量池?

爲了提供一些符號和常量,便於指令的識別,減小內存,提升複用。

方法的調用

在JVM中,將符號引用轉換爲調用方法的直接引用與方法的綁定機制相關。

  • 靜態連接:當一個字節碼文件被裝載進JVM時,若是被調用的目標方法在編譯器可知,且運行期保持不變,這種稱爲靜態連接。
  • 動態連接:若是被調用的方法在編譯器沒法肯定,只有在程序運行期才能將其轉換爲直接引用,稱爲動態連接。

根據兩種連接對應的綁定機制,綁定是指一個字段,方法或者類在符號引用被替換成直接引用的過程,這僅僅發生一次。

  • 早期綁定:早期綁定指被調用的目標若是在編譯器可知,且運行期保持不變。
  • 晚期綁定:被調用的目標只能在運行時才能從符號綁定轉爲直接綁定,這稱爲晚期綁定。
例子
  • 實例方法就是典型的晚期綁定,在程序運行以前,沒法肯定將要調用哪一個實例的方法。
  • 在子類構造器中顯式調用super()則是早期綁定的例子,在編譯期間,就能夠肯定該方法的直接引用。
虛方法和非虛方法

非虛方法:若是方法在編譯期就肯定了具體的調用版本,這個版本在運行時是不可變的,這樣的方法稱爲非虛方法。如靜態方法,私有方法,final方法,實例構造器,父類方法都是非虛方法。其餘方法都稱爲虛方法。
虛擬機中提供了一下幾條方法調用指令:

  • 普通調用指令

    • invokestatic 調用靜態方法,解析階段肯定惟一方法版本。
    • invokespecial 調用<init>方法,私有及父類方法,解析階段肯定惟一方法版本。
    • invokevirtual 調用虛方法。
    • invokeinterface 調用接口方法。
  • 動態調用指令

    • invokedynamic 動態解析出須要調用的方法,而後執行。

前四條指令方法的調用執行不可人爲干預,而動態調用支持由用戶肯定方法版本。其中invokestatic和invokespecial指令調用的方法稱爲非虛方法,其他的(final修飾的除外)稱爲虛方法。
沒有顯式使用super的方法調用也會被認爲是虛方法,使用invokevirtual調用。

invokedynamic指令

JDK7中爲了實現動態類型語言支持而作出的改進,出現了invokedynamic指令。但直到JDK8的Lambda表達式的出現,invokedynamic指令的生成,在JAVA種纔有了直接的生成方式。

靜態類型語言和動態類型語言

這兩種語言最重要的區別在於對類型的檢查是在編譯期間仍是在運行期間,前者爲靜態類型語言,後者爲動態類型語言。直白的說,靜態語言判斷變量的類型,動態語言判斷變量值的類型。

方法重寫

方法重寫的本質

  • 找到操做數棧頂的第一個元素所執行的對象的實際類型,記做C。
  • 若是在類型C中找到與常量池中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,若是經過則返回這個方法的直接引用,查找過程結束,若是不經過則返回IllegalAccessError異常。
  • 若是沒有找到,按照繼承關係從下往上依次對C的各個父類進行第二步的搜索和驗證操做。
  • 若是始終沒有找到,拋出AbstractMethodError異常。

IllegalAccessError異常介紹:
程序試圖訪問或修改一個屬性或調用一個方法,這個屬性或方法你沒有權限訪問。通常來講這個會引發編譯器異常,若是發生在運行期間,就說明一個類發生了不兼容的改變。

虛方法表

爲了提升性能,JVM在方法區創建一個虛方法表來實現,使用索引表來代替查找。
虛方法表主要在類加載的連接解析階段建立。

方法返回地址

存放調用該方法的程序計數器的值。在方法結束以後都須要返回到該方法被調用的位置,方法正常退出時,調用者的程序計數器的值做爲返回地址,即調用該方法指令的下一條指令的地址。而經過異常退出須要經過異常表肯定,棧幀中不保存這部分信息。
因此,經過異常完成的方法退出不會給它的上層調用者任何的返回值。
補充:方法返回指令!

  • ireturn負責返回值是booleanbytecharshortint
  • lreturn負責返回值是long
  • freturn負責返回值是float
  • dreturn負責返回值是double
  • areturn負責返回值是引用類型
  • return負責生命爲void的方法,實例初始化方法,類和接口的初始化方法。

棧幀中的附加信息

這個部分可能會有,也可能沒有,主要看虛擬機的具體實現。

相關文章
相關標籤/搜索