五、虛擬機棧

1、虛擬機棧概述

虛擬機棧出現的背景

由於跨平臺性的設計, Java的指令都是根據棧來設計的。不同平臺CPU架構不同,所以不能設計爲基於寄存器的。

優點是跨平臺,指令集小,編譯器容易實現,缺點是性能下降,實現同樣的功能需要更多的指令。

 

初步印象

有不少Java開發人員一提到Java內存結構,就會非常粗粒度地將JVM中的內存區理解爲僅有Java堆(heap)和Java棧(stack) ?爲什麼?

 

內存中的棧與堆

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

即:棧解決程序的運行問題,即程序如何執行,或者說如何處理數據。堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。

 

虛擬機棧基本內容

 

  1. 虛擬機棧是什麼?

              Java虛擬機棧(Java Virtual Machine Stack) ,早期也叫Java棧,每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個的棧幀(stack Frame) ,對應着一次次的Java方法調用。

             >是線程私有的

  •  

              生命週期和線程一致。

  •  

              主管Java程序的運行,它保存方法的局部變量(8種基本數據類型、對象的引用地址)、部分結果,並參與方法的調用和返回。

              >局部變量 vs 成員變量(或屬性)

              >基本數據變量 vs 引用類型變量(類、數組、接口)

 

虛擬機棧基本內容

 

棧的特點(優點)

  • ,訪問速度儀次於程序計數器
  1. 直接對Java棧的操作只有兩個:

              >每個方法執行,伴隨着進棧(入棧、壓棧)

              >執行結束後的出棧工作

  •  

             >GC; OOM

 

 

虛擬機棧基本內容

面試題:開發中遇到的異常有哪些?

 

棧中可能出現的異常

 

  1. 虛擬機規範允許棧的大小是動態的或者是固定不變的

              > 如果採用固定大小的Java虛擬機棧,那每一個線程的Java虛擬機棧容量可以在線程創建的時候獨立選定。如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量, Java虛擬機將會拋出一個StackoverflowError異常。

             >如果Java虛擬機棧可以動態擴展,並且在嘗試擴展的時候無法申請到足夠的內存,或者在創建新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出一個OutOfMemoryError異常。

 

  •  

我們可以使用參數 -Xss 選項來設置線程的最大棧空間,棧的大小直接決定了函數調用的最大可達深度。

 

 

2、棧的存儲單位

棧中存儲什麼?

  • ,棧中的數據都是以棧幀(Stack Frame)的格式存在。
  • (Stack Frame)。
  • ,是一個數據集,維繫着方法執行過程中的各種數據信息。

 

棧運行原理

  1. 直接對Java棧的操作只有兩個,就是對棧幀的壓棧出棧,遵循“先進後出” / “後進先出”原則。  

 

  • ,一個時間點上,只會有一個活動的棧幀。即只有當前正在執行的方法的棧幀(棧頂棧幀)是有效的,這個棧幀被稱爲當前棧幀(current Frame) ,與當前棧幀相對應的方法就是當前方法(Current Method) ,定義這個方法的類就是當前類(Current Class)
  •  
  • ,對應的新的棧幀會被創建出來,放在棧的頂端,成爲新的當前幀。

 

  • ,即不可能在一個棧幀之中引用另外一個線程的棧幀。
  • ,方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,接着,虛擬機會丟棄當前棧幀,使得前一個棧幀重新成爲當前棧幀。
  1. 方法有兩種返回函數的方式,一種是正常的函數返回,使用return指令;另外一種是拋出異常。不管使用哪種方式,都會導致棧幀被彈出

 

棧楨的內部結構

每個棧幀中存儲着:

  • (Local Variables)
  • (Operand Stack) (或表達式棧)
  • (Dynamic Linking) (或指向運行時常量池的方法引用)
  • (Return Address) (或方法正常退出或者異常退出的定義
  •  

 

3、局部變量表

  •  
  • ,主要用於存儲方法參數和定義在方法體內的局部變量,這些數據類型包括各類基本數據類型、對象引用(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.

 

  1. 會爲局部變量表中的每一個Slot都分配一個訪問索引,通過這個素引即可成功訪問到局部變量表中指定的局部變量值
  • ,它的方法參數和方法體內部定義的局部變量將會按照順序被複制到局部變量表中的每一個slot上
  • 64bit的局部變量值時,只需要使用前一個素引即可。(比如:訪問long或double類型變量)
  • ,那麼該對象引用this將會存放在index爲0的slot處,其餘的參數按照參數表順序繼續排列。

 

Slot的重複利用

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

 

舉例:靜態變量與局部度量的對比

  • ,再根據方法體內定義的變量的順序和作用域分配。
  • ,第一次是在“準備階段",執行系統初始化,對類變量設置零值,另一次則是在“初始化”階段,賦予程序員在代碼中定義的初始值。
  • ,局部變量表不存在系統初始化的過程,這意味着一旦定義了局部變量則必須人爲的初始化,否則無法使用。

 

這樣的代碼是錯誤的,沒有賦值不能夠使用

 

補充說明

  • ,與性能調優關係最爲密切的部分就是前面提到的局部變量表。在方法執行時,虛擬機使用局部變量表完成方法的傳遞。
  • ,只要被局部變量表中直接或間接引用的對象都不會被回收

 

 

4、操作數棧

  • ,還包含一個後進先出(Last-In-First-Out)的操作數棧,也可以稱之爲表達式棧(Expression Stack) 。
  • ,在方法執行過程中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧(push) /出棧(pop)

            >某些字節碼指令將值壓入操作數棧,其餘的字節碼指令將操作數取出棧。使用它們後再把結果壓入棧。

           >比如:執行復制、交換、求和等操作

 

 

  • ,主要用於保存計算過程的中間結果,同時作爲計算過程中變量臨時的存儲空間
  • JVM執行引擎的一個工作區,當一個方法剛開始執行的時候,一個新的棧幀也會隨之被創建出來,這個方法的操作數棧是空的
  • ,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,爲max_stack的值。
  • Java數據類型。

              > 32bit的類型佔用一個棧單位深度

              > 64bit的類型佔用兩個棧單位深度

  • 並非採用訪問索引的方式來進行數據訪問的,而是隻能通過標準的入棧(push)和出棧(pop)操作來完成一次數據訪問。

 

  • ,其返回值將會被壓入當前棧幀的操作數棧中,並更新PC寄存器中下一條需要執行的字節碼指令。
  • ,這由編譯器在編譯器期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證。
  • ,我們說Java虛擬機的解釋引擎是基於棧的執行引擎,其中的棧指的就是操作數棧

 

5、代碼追蹤

 

 

 

 

 

程序員面試過程中,常見的i++和++i的區別,放到字節碼篇章時再介紹。

public void add(){    //第1類問題:    int i1 = 10;    i1++;    int i2 = 10;    ++i2;    //第2類問題:    int i3 = 10;    int i4 = i3++;    int i5 = 10;    int i6 = ++i5;    //第3類問題:    int i7 = 10;    i7 = i7++;    int i8 = 10;    i8 = ++i8;    //第4類問題:    int i9 = 10;    int i10 = i9++ + ++i9; }

 

 

6、棧頂緩存技術

前面提過,基於棧式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操作的時候必然需要使用更多的入棧和出棧指令,這同時也就意味着將需要更多的指令分派(instruction dispatch)次數和內存讀/寫次數。

由於操作數是存儲在內存中的,因此頻繁地執行內存讀/寫操作必然會影響執行速度。爲了解決這個問題, HotSpot JvM的設計者們提出了棧頂緩存(Tos, Top-of-stack Cashing)技術,將棧頂元素全部緩存在物理CPU的寄存器中,以此降低對內存的讀/寫次數,提升執行引擎的執行效率

 

 

 

 

7、動態鏈接

動態鏈接(或指向運行時常量池的方法引用)

  • 運行時常量池該棧幀所屬方法的引用。包含這個引用的目的就是爲了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking) 。比如: invokedynamic指令
  • Java源文件被編譯到字節碼文件中時,所有的變量和方法引用都作爲符號引用(Symbolic Reference)保存在class文件的常量池裏。比如:描述一個方法調用了另外的其他方法時,就是通過常量池中指向方法的符號引用來表示的,那麼動態鏈接的作用就是爲了將這些符號引用轉換爲調用方法的直接引用

 

爲什麼需要常量池呢?

常量池的作用,就是爲了提供一些符號和常量,便於指令的識別。

 

8、方法的調用:解析與分派

 

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

  • :

當一個字節碼文件被裝載進JVM內部時,如果被調用的目標方法在編譯期可知,且運行期保持不變時。這種情況下將調用方法的符號引用轉換爲直接引用的過程稱之爲靜態鏈接。

  • :

如果被調用的方法在編譯期無法被確定下來,也就是說,只能夠在程序運行期將調用方法的符號引用轉換爲直接引用,由於這種引用轉換過程具備動態性,因此也就被稱之爲動態鏈接。

 

 

對應的方法的綁定機制爲:早期綁定(Early Binding)和晚期綁定(Late Binding) 。綁定是一個字段、方法或者類在符號引用被替換爲直接引用的過程,這僅僅發生一次

 

  • :

早期綁定就是指被調用的目標方法如果在編譯期可知,且運行期保持不變時即可將這個方法與所屬的類型進行綁定,這樣一來,由於明確了被調用的目標方法究竟是哪一個,因此也就可以使用靜態鏈接的方式將符號引用轉換爲直接引用。

  • :

如果被調用的方法在編譯期無法被確定下來,只能夠在程序運行期根據實際的類型綁定相關的方法,這種綁定方式也就被稱之爲晚期綁定。

 

隨着高級語言的橫空出世,類似於Java一樣的基於而向對象的編程語言如今越來越多,儘管這類編程語言在語法風格上存在一定的差別,但是它們彼此之間始終保持着一個共性,那就是都支持封裝、繼承和多態等面向對象特性,既然這一類的編程語言具備多態特性,那麼自然也就具備早期綁定和晚期綁定兩種綁定方式。

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

 

 

方法的調用:虛方法與非虛方法

 

非虛方法:

  • ,這個版本在運行時是不可變的這樣的方法稱爲非虛方法
  • final方法、實例構造器、父類方法都是非虛方法。
  •  

 

子類對象的多態性的使用前提: ①類的繼承關係②方法的重寫

 

在類加載的解析階段就可以進行解析,如下是非虛方法舉例:

 

 

虛擬機中提供了以下幾條方法調用指令:

  • :

1. invokestatic : 調用靜態方法,解析階段確定唯一方法版本

2. invokespecial : 調用<init>方法、私有及父類方法,解析階段確定唯一方法版本

3. invokevirtual : 調用所有虛方法

4. invokeinterface : 調用接口方法

  • :

5. invokedynamic : 動態解析出需要調用的方法,然後執行

 

前四條指令固化在虛擬機內部,方法的調用執行不可人爲干預,而invokedynamic指令則支持由用戶確定方法版本。其中invokestatic指令和invokespecial指令調用的方法稱爲非虛方法,其餘的(final修飾的除外)稱爲虛方法。

 

 

方法的調用:關於invokedynamic指令

 

  1. 字節碼指令集一直比較穩定,一直到Java7中才增加了一個 invokedynamic 指令,這是Java爲了實現「動態類型語言」支持而做的一種改進。

 

  • Java7中並沒有提供直接生成 invokedynamic 指令的方法,需要藉助ASM這種底層字節碼工具來產生 invokedynamic 指令。直到Java8的Lambda表達式的出現, invokedynamic指令的生成,在Java中才有了直接的生成方式。

 

  1. 中增加的動態語言類型支持的本質是對Java虛擬機規範的修改,而不是對Java語言規則的修改,這一塊相對來講比較複雜,增加了虛擬機中的方法調用,最直接的受益者就是運行在Java平臺的動態語言的編譯器。

 

 

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

動態類型語言和靜態類型語言兩者的區別就在於對類型的檢查是在編譯期還是在運行期,滿足前者就是靜態類型語言,反之是動態類型語言。

 

說的再直白一點就是,靜態類型語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值纔有類型信息,這是動態語言的一個重要特徵。

Java: String info = "atguigu"; //info =atguigu; JS: var name = "shkstart"; var name = 10; Python: info = 130.5;

 

方法的調用:方法重寫的本質

Java 語言中方法重寫的本質:

① 找到操作數棧頂的第一個元素所執行的對象的實際類型,記作 C.

② 如果在類型 C 中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回

java.lang.IllegalAccessrror異常。

③ 否則,按照繼承關係從下往上依次對 C 的各個父類進行第2步的搜索和驗證過程。

④ 如果始終沒有找到合適的方法,則拋出 java.lang.AbstractMethodError 異常。

 

IllegalAccessError介紹:

程序試圖訪問或修改一個屬性或調用一個方法,這個屬性或方法,你沒有權限訪問。一般的,這個會引起編譯器異常。這個錯誤如果發生在運行時,就說明一個類發生了不兼容的改變。

 

方法的調用:虛方法表

 

  • ,會很頻繁的使用到動態分派,如果在每次動態分派的過程中都要重新在類的方法元數據中搜索合適的目標的話就可能影響到執行效率。因此,爲了提高性能, JVM採用在類的方法區建立一個虛方法表(virtual method table) (非虛方法不會出現在表中)來實現。使用索引表來代替查找。
  • ,表中存放着各個方法的實際入口。
  • ?

虛方法表會在類加載的鏈接階段被創建並開始初始化,類的變量初始值準備完成之後, JVM會把該類的方法表也初始化完畢。

 

 

 

 

 

 

 

 

9、方法返回地址

  • pc寄存器的值。
  • ,有兩種方式:

                 > 正常執行完成

        > 出現未處理的異常,非正常退出

  • ,在方法退出後都返回到該方法被調用的位置。方法正常退出時,調用者的pc計數器的值作爲返回地址,即調用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。

 

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

1、執行引擎遇到任意一個方法返回的字節碼指令(return) ,會有返回值傳遞給上層的方法調用者,簡稱正常完成出口;

> 一個方法在正常調用完成之後究竟需要使用哪一個返回指令還需要根據方法返回值的實際數據類型而定。

> 在字節碼指令中,返回指令包含ireturn (當返回值是boolean, byte, char,short和int類型時使用)、lreturn. freturn, dreturn以及areturn,另外還有一個return指令供聲明爲void的方法、實例初始化方法、類和接口的初始化方法使用。

 

2、在方法執行的過程中遇到了異常(Exception) ,並且這個異常沒有在方法內進行處理,也就是隻要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出。簡稱異常完成出口

方法執行過程中拋出異常時的異常處理,存儲在一個異常處理表,方便在發生異常的時候找到處理異常的代碼。

 

 

本質上,方法的退出就是當前棧幀出棧的過程。此時,需要恢復上層方法的局部變量表、操作數棧、將返回值壓入調用者棧幀的操作數棧、設置PC寄存器值等,讓調用者方法繼續執行下去。

 

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

 

 

 

 

 

10、一些附加信息

棧幀中還允許攜帶與Java虛擬機實現相關的一些附加信息。例如,對程序調試提供支持的信息。

 

 

 

11、棧的相關面試題

  • 舉例棧溢出的情況? (StackOverflowError)

通過-Xss設置棧的大小; OOM

  • 調整棧大小,就能保證不出現溢出嗎?不能
  • 分配的棧內存越大越好嗎?不是!
  • 垃圾回收是否會涉及到虛擬機棧?不會的!
  • 方法中定義的局部變量是否線程安全?具休問題具體分析

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章