java Stack(虛擬機棧)

在這裏插入圖片描述

1、Java 虛擬機棧的定義

Java 虛擬機棧(java Virtual Machine Stack),每個線程在創建時都會創建一個虛擬機棧,其內部保存一個個 棧幀(Stack Frame)

  • 虛擬機棧,描述的是Java方法執行的內存模型:每個方法執行的同時會創建一個棧幀。
  • 是線程私有的,它的生命週期與線程相同(隨線程而生,隨線程而滅)
  • 對於我們來說,主要關注的stack棧內存,就是虛擬機棧中局部變量表。

1.1、異常

虛擬機棧 不存在GC(垃圾回收)。

  • StackOverflowError
    如果線程請求的棧深度大於虛擬機所允許的深度,將拋出異常;

  • OutOfMemoryError
    如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,就會拋出異常;
     (當前大部分JVM都可以動態擴展,只不過JVM規範也允許固定長度的虛擬機棧)

1.2、棧幀(Stack Frame)

棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構。它是虛擬機棧的棧元素。
棧幀存儲了方法的局部變量表、操作數棧、動態連接和方法返回地址等信息。
每一個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機裏面從入棧到出棧的過程。

注意:

在編譯程序代碼的時候,棧幀中需要多大的局部變量表內存,多深的操作數棧都已經完全確定了。
因此一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。

在這裏插入圖片描述

棧幀的操作只有兩個:壓棧(push)和 出棧( pop)。
在一個線程中,只有位於棧頂的棧幀纔是有效的,稱爲 當前棧幀 ,與這個棧幀相關聯的方法稱爲 當前方法
執行引擎運行的所有字節碼指令都只針對當前棧幀進行操作;
如果 當前方法調 用其他方法,對應的新的棧幀纔會被創建出來,放到棧頂,成爲新的 當前棧幀

棧幀的內部結構:

  • 局部變量表(Local Variable Table)
  • 操作數棧 (Operand Stack)(或者叫表達式棧)
  • 動態鏈接 (Dynamic Linking) (或指向運行時常量池的方法引用)
  • 方法返回地址(Return Address)
  • 一些附加信息

下面介紹棧幀的各個成員。

2、局部變量表(Local Variable Table)

局部變量表(Local Variable Table),也叫 局部變量數組,用於存放 方法參數 和 方法體內部的局部變量。

局部變量表存放了編譯期可知的8種基本數據類型、對象引用(reference類型,包括String )和 returnAddress 類型。

局部變量表的 容量大小,在Java編譯爲Class文件時就確定下來的,並保存在方法的Code 屬性的maximnum_local_variables 數據項中。 方法運行期間是不會改變局部變量表的大小。

2.1、面試題:基本數據和對象引用存儲在棧中 ?

當然這種說法雖然是正確的,但是很不嚴謹,只能說這種說法針對的是 局部變量

局部變量 存儲在局部變量表 中,隨着線程而生,線程而滅。並且線程間數據不共享。

但是,如果是成員變量,或者定義在方法外對象的引用,它們存儲在堆中。

因爲在堆中,是線程共享數據的,並且棧幀裏的命名就已經清楚的劃分了界限 : 局部變量表。

2.2、靜態變量與局部變量的對比

參數表分配完畢之後,再根據方法體內定義的變量的順序和作用域分配。

我們知道類變量表有兩次初始化的機會,第一次 是在準備階段,執行系統初如化,對類變量設置默認值**,另一次** 則是在初始化階段,賦予變量在代碼中定義的初始值。

和類變量不同,局部變量表不存在系統初始化的過程,這就需要在定義局部變量時必須人爲的初始化,否則無法使用。

public void test(){
	int i;
	System.out.println(i);
}

上面的代碼是錯誤的,會編譯報錯的,因爲沒有i沒有初始值。

總結:

變量的分類
    按照數據類型: 1)基本數據類型 、2)引用數據類型

    按照在類中聲明的位置:
    1)成員變量: 在使用前,都經歷默認初始化賦值。
        a)類變量(靜態變量): linking的prepare階段,給類變量賦默認值,initial 階段,給類變量賦予代碼中定義的值。
        b)實例變量:隨着對象的創建,會在堆中分配實例變量空間,並進行默認賦值。
    2)局部變量: 在使用前,必須進行顯式賦值! 否則編譯不通過。

2.3、reference(對象實例的引用)

我的理解是:一個超鏈接

一般來說,虛擬機都能從引用中直接或者間接的查找到對象的以下兩點 :

a. 在Java堆中的數據存放的起始地址索引。

b. 所屬數據類型在方法區中的存儲類型。

例如:我們在創建一個Student對象時的數據存儲結構 :

在這裏插入圖片描述

2.4、Slot( 變量槽 )

局部變量表,最基本的存儲單元是Slot (變量槽)。

  • 32位以內的類型佔用一個slot :6種基本數據類型、reference、returnAddress 。

  • 64位的類型的佔用兩個slot : long 、double 。

     虛擬機會以高位對齊方式爲其分配兩個連續的Slot空間,也就是相當於把一次long和double數據類型讀寫分割成爲兩次32位讀寫。
    

2.4.1、Slot 的理解

JVM 會爲局部變量表中的每一個Slot分配一個訪問索引,通過這個索引即可成功訪問局部變量表中指定的局部變量值。

當一個實例方法被調用時,它的方法參數和方法體內部定義的局部變量將會按照順序被複制到局部變量表中的每一個Slot上。

如果需要訪問局部變量表中的一個64bit的局部變量時,只需要使用前一個索引即可。(訪問long 或 double 類型的變量)

如果當前幀是由構造方法或實例方法創建的,那麼該對象引用this將會存放到index爲0的slot處,其餘的參數按照參數表順序繼續排列。

2.4.2、Slot的複用

爲了儘可能節省棧幀空間,局部變量表中的Slot是可以重用的,
也就是說當PC計數器的指令指已經超出了某個變量的作用域(執行完畢),
那這個變量對應的Slot就可以交給其他變量使用。

優點 : 節省棧幀空間。
缺點 : 影響到系統的垃圾收集行爲。

(如大方法佔用較多的Slot,執行完該方法的作用域後沒有對Slot賦值或者清空設置null值,垃圾回收器便不能及時的回收該內存。)

2.5、補充說明

在棧幀中,與性能調優關係最密切的是局部變量表。

在方法執行時,虛擬機使用局部變量表完成方法的傳遞。

局部變量表中的變量是重要的垃圾加收根節點,只要被局部變量表直接或間接引用的對象都不會被回收。

3、操作數棧(Operand Stack)

每個獨立的棧幀中除了包含局部變量表以外,還包含 一個後進先出(Last-In-First-Out)的操作數棧,也叫 表達式棧。

操作數棧,在方法執行過程中,根據字節碼指令,往棧中寫入數據或提取數據,即入棧(push)/出棧(pop)。

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

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

操作數棧,主要用於保存計算過程的中間結果,同時作爲計算過程中變量臨時的存儲空間。

操作數棧就是JVM 執行引擎的一個工作區,當一個方法剛開始執行的時候,一人新的棧幀也支隨着被創建出來, 這個方法的操作數棧是空的。

每一個操作數棧都會擁用一個明確的棧深度,用於存儲數值,其所需的最大深度在編譯期就定義好了,保存在方法的Code屬性中,爲max_stack的值。

棧中的任何元素都是可以任意的java數據類型:

  • 32bit的類型,佔用一個棧單位深度
  • 64bit的類型,佔用兩個棧單位深度

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

如果被調用的方法帶有返回值,其返回值將會被壓入當前棧幀的操作數棧中,並更新PC寄存器中下一條需要執行的字節碼指令。

操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,這由編譯器在編譯期間進行驗證,同時在類加載過程中的類檢驗階段的數據流分析階段要再次驗證。

java虛擬機的解析引擎是基於棧的執行引擎,其中的棧就是操作數棧。

3.1、代碼追蹤

public class Test {
	public void testAddOperation(){
		byte i = 15;
		int j = 8;
		int k = i + j;
	}
}

使用javap命令反編譯class文件: javap -v 類名.class

3.2、棧頂緩存技術 (Top-of-StackCashing)

前面提過,基本棧架構的虛擬機使用的零地址指令更緊湊,但完成一項操作時必須要使用更多的入棧和出棧指令,這就意味着需要更多的指令分派次數和內存讀寫次數。

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

4、動態鏈接(Dynamic Linking)

動態鏈接(Dynamic Linking) ,也叫指向運行時常量池的方法引用。

每一個棧幀內部都包含 一個指定運行時常量池中該棧幀所屬方法的引用。

包含這們引用的目的就是爲了支持當前方法的代碼能夠實現動態鏈接(Dynamic Linking)。比如:invokeddynamic 指令。

在java源文件被編譯爲字節碼文件時,所有的變量和方法引用都作爲符號引用(Symbolic Reference)保存在class文件的常量池裏。

比如:描述一個方法調用其他方法時,就是通過常量池中指向方法的符號引用來表示的,那麼動態鏈接的作用就是爲了將這些符號引用轉換爲調用方法的直接引用。

爲什麼需要常量池?

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

5、方法的調用

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

5.1、靜態鏈接、動態鏈接

靜態鏈接:

在類加載階段中的解析階段,如果被調用的目標方法在編譯可知,且運行期保持不變時。 這種情況下將調用方法的符號引用轉換爲直接引用的過程稱爲靜態鏈接。

動態鏈接:

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

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

5.2、早期綁定、晚期綁定 與 多態

早期綁定:

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

晚期綁定:

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

類似於java的面向對象的編譯語言,儘管語法風格不同,但保持一個共性,就是都技術封裝、繼承 和 多態 等面向對象的特性。

這類編程語言具備多態特性,那麼自我就具備早期綁定和晚期綁定。

5.3、虛方法 、非虛方法

虛方法

java中任何一個普通的方法都具備虛函數的特徵,相當於C++中的虛函數(C++使用 virtual 關鍵字顯示定義)。如果在java中不希望某個方法擁有虛函數的特徵時,使用 final 關鍵字來標記。

非虛方法:

如果在方法編譯期就確定了具本的調用版本,這個版本在運行時是不可變的。 這樣的方法稱爲 非虛方法。

靜態方法、私用方法、final方法、實例構造器、父類方法都是非虛方法。

其他方法稱爲虛方法。

對類對象的多態性的使用前提: 類的繼承、方法的重寫

虛方法表什麼時候被創建?

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

5.4、方法調用指令

普通調用指令:

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

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

  3. invokevirtual : 調用所有虛方法

  4. invokeinterface : 調用接口方法

序號 名稱 說明 是否虛方法
1 invokestatic 調用靜態方法,解析階段確定唯一方法的版本。 非虛方法
2 invokespecial 調用<init> 方法、私用及父類方法,解析階段確定唯一方法版本。 非虛方法
3 invokevirtual 調用所有虛方法 虛方法
4 invokeinterface 調用接口方法 虛方法

動態調用指令:

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

普通調用指令 固化在虛擬機內部, 方法的調用執行不可人爲干預。

invokedynamic 指令則支持用戶確定方法版本。

invokestaticinvokespecial指令調用的方法稱爲非虛方法,其餘的(final修改的除外)稱爲虛方法。

5.5、 invokedynamic 指令

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

但是 java7 中並沒有提供直接生成 invokedynamic 指令的方法,需要 藉助ASM 這種底層字節碼指令工具來產生 invokedynamic 指令。

JDK8的 Lambda 表達式的出現,invokedynamic 指令的生成,在java中才有了直接的生成方式。

動態類型的語言 和 靜態類型語言的區別?

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

5.6、方法重寫的本質

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

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

  2. 如果在類型C 中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,

    如果通過,則返回這個方法的直接引用,查找過程結束;

    如果不通過,則返回 java.lang.IllegalAccessError 異常。

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

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

IllegalAccessError 介紹:

程序試圖訪問或修改一個屬性或調用一個方法,這個屬性或方法,你沒有權限訪問。

一般的,這個會引起編譯器異常。

這個錯誤如果發生在運行時,就說明一個類發生了不兼容的改變。

package demo3;

interface Friendly {

	void sayHello();
	void sayGoodbye();
}

class Dog {
	public void sayHello() {
	}

	public String toString() {
		return "Dog";
	}
}

class Cat implements Friendly {

	@Override
	public void sayHello() {
	}

	@Override
	public void sayGoodbye() {
	}

	public void eat() {

	}

	protected void finalize() {

	}
}

class CockerSpaniel extends Dog implements Friendly {

	@Override
	public void sayGoodbye() {
	}

	public void sayHello() {
		super.sayHello();
	}
}

public class Main {
	public static void main(String[] args) {
		CockerSpaniel cockerSpaniel=new CockerSpaniel();
		System.out.println(cockerSpaniel.toString());
	}
}

6、方法返回地址(Return Address)

存放調用方法的PC計數器的值。

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

  • 正常執行完成
  • 出現未處理的異常,非正常退出

無論通過哪種方式退出,在方法退出後都返回到該方法被調用的位置。

方法正常退出時,調用者的PC計數器的值作爲返回地址,即調用方法的指令的下一條指令的地址。

異常退出時,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。

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

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

6.1、正常完成出口和異常完成出口的區別:

異常完成出口退出的不會給他的上層調用者產生任何的返回值。

7、一些附加信息

對程序調試提供支持的信息。

LineNumberTable : 字節碼 與 java 源碼行號 的對應關係
在這裏插入圖片描述

每個LineNumberTable中的 line_number_table部分, 可以看做是一個數組, 數組的每項是一個line_number_info ,

每個 line_number_info 結構描述了一條字節碼和源碼行號的對應關係。

  • start_pc 是這個 line_number_info 描述的字節碼指令的偏移量,
  • line_number 是這個 line_number_info 描述的字節碼指令對應的源碼中的行號。

方法中的每條字節碼都對應一個 line_number_info , 這些 line_number_info 中的 line_number 可以指向相同的行號, 因爲一行源碼可以編譯出多條字節碼。

LocalVariableTable : 方法的棧幀中局部變量中的變量名和描述符、源碼的對應關係。

在這裏插入圖片描述

每個 LocalVariableTable 的 local_variable_table 部分可以看做是一個數組,

每個數組項是一個叫做 local_variable_info 的結構, 該結構描述了某個局部變量的變量名和描述符, 還有和源代碼的對應關係。

下面講解 local_variable_info 的各個部分:

  1. start_pc ,是當前的局部變量的作用域的起始字節碼偏移量;

  2. length ,是當前的局部變量的作用域的大小。 也就是從字節碼偏移量 start_pc 到 start_pc+length 就是當前局部變量的作用域範圍;

  3. index ,這個局部變量在棧幀局部變量表中Slot的位置。當這個變量是 64 位類型時,它佔用的Slot 爲 index and index + 1;

  4. name_index ,指向常量池中的一個 CONSTANT_Utf8_info 型常量的索引, 該 CONSTANT_Utf8_info 描述了當前局部變量的變量名;

  5. descriptor_index ,指向常量池中的一個 CONSTANT_Utf8_info 型常量的索引, 該 CONSTANT_Utf8_info 描述了當前局部變量的描述符;

由此可知, 方法中的每個局部變量都會對應一個local_variable_info 。

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