jvm-執行時資料區(程式計數器、Java虛擬機器棧、本地方法棧)

陳同學_發表於2020-12-01

程式計數器

作用

程式計數器(Program Counter Register),也叫PC暫存器,是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼指令的行號指示器。位元組碼直譯器的工作就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。分支,迴圈,跳轉,異常處理,執行緒回覆等都需要依賴這個計數器來完成。

由於Java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(針對多核處理器來說是一個核心)都只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體。

儲存的資料

如果一個執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;
如果正在執行的是一個Native方法,這個計數器的值則為空。

異常

此記憶體區域是唯一一個在Java的虛擬機器規範中沒有規定任何OutOfMemoryError異常情況的區域。

Java虛擬機器棧

虛擬機器棧也是執行緒私有,而且生命週期與執行緒相同,每個Java方法在執行的時候都會建立一個棧幀(Stack Frame)。
在這裡插入圖片描述
在這裡插入圖片描述
棧記憶體為執行緒私有的空間,每個執行緒都會建立私有的棧記憶體。棧空間記憶體設定過大,建立執行緒數量較多時會出現棧記憶體溢位StackOverflowError。同時,棧記憶體也決定方法呼叫的深度,棧記憶體過小則會導致 方法呼叫的深度較小,如遞迴呼叫的次數較少。

棧幀

棧幀(Stack Frame)是用於支援虛擬機器進行方法呼叫和方法執行的資料結構。棧幀儲存了方法的區域性變數表、運算元棧、動態連線和方法返回地址等資訊。每一個方法從呼叫至執行完成的過程,都對應著一個棧幀在虛擬機器棧裡從入棧到出棧的過程。

一個執行緒中方法的呼叫鏈可能會很長,很多方法都同時處於執行狀態。對於JVM執行引擎來說,在在活動執行緒中,只有位於JVM虛擬機器棧棧頂的元素才是有效的,即稱為當前棧幀,與這個棧幀相關連的方法稱為當前方法,定義這個方法的類叫做當前類。

執行引擎執行的所有位元組碼指令都只針對當前棧幀進行操作。如果當前方法呼叫了其他方法,或者當前方法執行結束,那這個方法的棧幀就不再是當前棧幀了。

呼叫新的方法時,新的棧幀也會隨之建立。並且隨著程式控制權轉移到新方法,新的棧幀成為了當前棧幀。方法返回之際,原棧幀會返回方法的執行結果給之前的棧幀(返回給方法呼叫者),隨後虛擬機器將會丟棄此棧幀。

關於「棧幀」,我們在看看《Java虛擬機器規範》中的描述:

  • 棧幀是用來儲存資料和部分過程結果的資料結構,同時也用來處理動態連線、方法返回值和異常分派。
  • 棧幀隨著方法呼叫而建立,隨著方法結束而銷燬——無論方法正常完成還是異常完成都算作方法
    結束。
  • 棧幀的儲存空間由建立它的執行緒分配在Java虛擬機器棧之中,每一個棧幀都有自己的本地變數表 (區域性變數表)、運算元棧和指向當前方法所屬的類的執行時常量池的引用。
區域性變數表
儲存內容

區域性變數表(Local Variable Table) 是一組變數值儲存空間,用於存放方法引數和方法內定義的區域性 變數。

一個區域性變數可以儲存一個型別為 boolean、byte、char、short、int、float、reference和 returnAddress型別 的資料。reference型別表示對一個物件例項的引用。returnAddress型別是為jsr、jsr_w和ret指令服務的,目前已經很少使用了。

儲存容量

區域性變數表的容量以變數槽(Variable Slot)為最小單位,Java虛擬機器規範並沒有定義一個槽所應該佔用記憶體空間的大小,但是規定了一個槽應該可以存放一個32位以內的資料型別。

  • 在Java程式編譯為Class檔案時,就在方法的Code屬性中的max_locals資料項中確定了該方法所
    需分配的區域性變數表的最大容量。(最大Slot數量)
其他

虛擬機器通過索引定位的方法查詢相應的區域性變數,索引的範圍是從 0~區域性變數表最大容量 。如果Slot是32位的,則遇到一個64位資料型別的變數(如long或double型)時,會連續使用兩個連續的Slot來儲存。

運算元棧
作用

運算元棧(Operand Stack)也常稱為操作棧,它是一個 後入先出棧(LIFO) 。

當一個方法剛剛開始執行時,其運算元棧是空的,隨著方法執行和位元組碼指令的執行,會從區域性變數表 或物件例項的欄位中複製常量或變數寫入到運算元棧,再隨著計算的進行將棧中元素出棧到區域性變數表或者返回給方法呼叫者,也就是出棧/入棧操作。一個完整的方法執行期間往往包含多個這樣出棧/入棧的過程。

儲存內容

運算元棧的每一個元素可以是任意Java資料型別,32位的資料型別佔一個棧容量,64位的資料型別佔2個棧容量。

儲存容量

同區域性變數表一樣,運算元棧的最大深度也在編譯的時候寫入到方法的Code屬性的 max_stacks 資料項中。且在方法執行的任意時刻,運算元棧的深度都不會超過 max_stacks 中設定的最大值。

動態連線

在一個class檔案中,一個方法要呼叫其他方法,需要將這些方法的符號引用轉化為其在記憶體地址中的直接引用,而符號引用存在於方法區中的執行時常量池。

Java虛擬機器棧中,每個棧幀都包含一個指向執行時常量池中該棧所屬方法的符號引用,持有這個引用的目的是為了支援方法呼叫過程中的動態連線(Dynamic Linking)。

這些符號引用一部分會在類載入階段或者第一次使用時就直接轉化為直接引用,這類轉化稱為靜態解 析。另一部分將在每次執行期間轉化為直接引用,這類轉化稱為動態連線。

方法返回

當一個方法開始執行時,可能有兩種方式退出該方法:

  • 正常完成出口
  • 異常完成出口
    正常完成出口是指方法正常完成並退出,沒有丟擲任何異常(包括Java虛擬機器異常以及執行時通過
    throw語句顯示丟擲的異常)。如果當前方法正常完成,則根據當前方法返回的位元組碼指令,這時有可能會有返回值傳遞給方法呼叫者(呼叫它的方法),或者無返回值。具體是否有返回值以及返回值的資料型別將根據該方法返回的位元組碼指令確定。

異常完成出口是指方法執行過程中遇到異常,並且這個異常在方法體內部沒有得到處理,導致方法退出。

  • 無論是Java虛擬機器丟擲的異常還是程式碼中使用athrow指令產生的異常,只要在本方法的異常表中
    沒有搜尋到相應的異常處理器,就會導致方法退出。

無論方法採用何種方式退出,在方法退出後都需要返回到方法被呼叫的位置,程式才能繼續執行,方法返回時可能需要在當前棧幀中儲存一些資訊,用來幫他恢復它的上層方法執行狀態。

  • 方法退出過程實際上就等同於把當前棧幀出棧,因此退出可以執行的操作有:恢復上層方法的局
    部變數表和運算元棧,把返回值(如果有的話)壓入呼叫者的運算元棧中,調整PC計數器的值以指
    向方法呼叫指令後的下一條指令。

一般來說,方法正常退出時,呼叫者的PC計數值可以作為返回地址,棧幀中可能儲存此計數值。而方法異常退出時,返回地址是通過異常處理器表確定的,棧幀中一般不會儲存此部分資訊。

附加資訊

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

棧異常

Java虛擬機器規範中,對該區域規定了這兩種異常情況:

  • 1: 如果執行緒請求的棧深度大於虛擬機器所允許的深度,將會丟擲 StackOverflowError 異常;
  • 2:虛擬機器棧可以動態擴充,當擴充套件時無法申請到足夠的記憶體,就會丟擲 OutOfMemoryError 異常。
public class StackErrorMock {
	private static int index = 1;
	public void call(){ 
		index++; call(); 
	}
	public static void main(String[] args) { 
		StackErrorMock mock = new StackErrorMock();
		try {mock.call(); }catch (Throwable e){ 
			System.out.println("Stack deep : "+index); 
			e.printStackTrace(); 
		} 
	} 
}

本地方法棧

本地方法棧和虛擬機器棧相似,區別就是虛擬機器棧為虛擬機器執行Java服務(位元組碼服務),而本地方法棧為虛擬機器使用到的Native方法(比如C++方法)服務。

本地方法介紹

什麼是本地方法

簡單地講,一個Native Method就是一個java呼叫非java程式碼的介面。

一個Native Method是這樣一個java的方法:該方法的實現由非java語言實現,比如C。

在定義一個native method時,並不提供實現體(有些像定義一個java interface),因為其實現體是由非java語言在外面實現的。下面給了一個示例:

public class IHaveNatives {
	native public void Native1( int x ) ;
	native static public long Native2() ;
	native synchronized private float Native3( Object o ) ; 
	native void Native4( int[] ary ) throws Exception ; 
}

這些方法的宣告描述了一些非java程式碼在這些java程式碼裡看起來像什麼樣子。

識別符號native可以與所有其它的java識別符號連用,但是abstract除外。這是合理的,因為native暗示這些方法是有實現體的,只不過這些實現體是非java的,但是abstract卻顯然的指明這些方法無實現體。

native與其它java識別符號連用時,其意義同非Native Method並無差別,比如native static表明這個方法可以在不產生類的例項時直接呼叫,這非常方便,比如當你想用一個native method去呼叫一個C的類庫時。上面的第三個方法用到了native synchronized,JVM在進入這個方法的實現體之前會執行同步鎖機制(就像java的多執行緒。)

一個native method方法可以返回任何java型別,包括非基本型別,而且同樣可以進行異常控制。這些方法的實現體可以制一個異常並且將其丟擲,這一點與java的方法非常相似。

當一個native method接收到一些非基本型別時如Object或一個整型陣列時,這個方法可以訪問這 些非基本型的內部,但是這將使這個native方法依賴於你所訪問的java類的實現。有一點要牢牢記住:我們可以在一個native method的本地實現中訪問所有的java特性,但是這要依賴於你所訪問的java特性的實現,而且這樣做遠遠不如在java語言中使用那些特性方便和容易。

native method的存在並不會對其他類呼叫這些本地方法產生任何影響,實際上呼叫這些方法的其他 類甚至不知道它所呼叫的是一個本地方法。JVM將控制呼叫本地方法的所有細節。需要注意當我們將一個本地方法宣告為final的情況。用java實現的方法體在被編譯時可能會因為內聯而產生效率上的提升。但是一個native final方法是否也能獲得這樣的好處卻是值得懷疑的,但是這只是一個程式碼優化方面的問題,對功能實現沒有影響。

如果一個含有本地方法的類被繼承,子類會繼承這個本地方法並且可以用java語言重寫這個方法(這 個似乎看起來有些奇怪),同樣的如果一個本地方法被fianl標識,它被繼承後不能被重寫。

本地方法非常有用,因為它有效地擴充了jvm。事實上,我們所寫的java程式碼已經用到了本地方法,在sun的java的併發(多執行緒)的機制實現中,許多與作業系統的接觸點都用到了本地方法,這使得java程式能夠超越java執行時的界限。有了本地方法,java程式可以做任何應用層次的任務。

為什麼要使用本地方法

java使用起來非常方便,然而有些層次的任務用java實現起來不容易,或者我們對程式的效率很在意時,問題就來了。

有時java應用需要與java外面的環境互動。這是本地方法存在的主要原因,你可以想想java需要與 一些底層系統如作業系統或某些硬體交換資訊時的情況。本地方法正是這樣一種交流機制:它為我們提供了一個非常簡潔的介面,而且我們無需去了解java應用之外的繁瑣的細節。

JVM支援著java語言本身和執行時庫,它是java程式賴以生存的平臺,它由一個直譯器(解釋位元組碼)和一些連線到原生程式碼的庫組成。然而不管怎 樣,它畢竟不是一個完整的系統,它經常依賴於一些底層(underneath在下面的)系統的支援。這些底層系統常常是強大的作業系統。通過使用本地方法,我們得以用java實現了jre的與底層系統的互動,甚至JVM的一些部分就是用C寫的,還有,如果我們要使用一些java語言本身沒有提供封裝的作業系統的特性時,我們也需要使用本地方法。

JVM怎樣使本地方法跑起來

我們知道,當一個類第一次被使用到時,這個類的位元組碼會被載入到記憶體,並且只會回載一次。在這個被載入的位元組碼的入口維持著一個該類所有方法描述符的list,這些方法描述符包含這樣一些資訊:方法程式碼存於何處,它有哪些引數,方法的描述符(public之類)等等。

如果一個方法描述符內有native,這個描述符塊將有一個指向該方法的實現的指標。這些實現在一些DLL檔案內,但是它們會被作業系統載入到java程式的地址空間。當一個帶有本地方法的類被載入時,其相關的DLL並未被載入,因此指向方法實現的指標並不會被設定。當本地方法被呼叫之前,這些DLL才會被載入,這是通過呼叫java.system.loadLibrary()實現的。

最後需要提示的是,使用本地方法是有開銷的,它喪失了java的很多好處。如果別無選擇,我們可以選擇使用本地方法。

本地方法棧的使用流程

當某個執行緒呼叫一個本地方法時,它就進入了一個全新的並且不再受虛擬機器限制的世界。本地方法可以通過本地方法介面來訪問虛擬機器的執行時資料區,但不止如此,它還可以做任何它想做的事情。

本地方法本質上時依賴於實現的,虛擬機器實現的設計者們可以自由地決定使用怎樣的機制來讓Java程式呼叫本地方法。

任何本地方法介面都會使用某種本地方法棧。當執行緒呼叫Java方法時,虛擬機器會建立一個新的棧幀並壓入Java棧。然而當它呼叫的是本地方法時,虛擬機器會保持Java棧不變,不再線上程的Java棧中壓入新的幀,虛擬機器只是簡單地動態連線並直接呼叫指定的本地方法。

如果某個虛擬機器實現的本地方法介面是使用C連線模型的話,那麼它的本地方法棧就是C棧。當C程式呼叫一個C函式時,其棧操作都是確定的。傳遞給該函式的引數以某個確定的順序壓入棧,它的返回值也以確定的方式傳回撥用者。同樣,這就是虛擬機器實現中本地方法棧的行為。

很可能本地方法介面需要回撥Java虛擬機器中的Java方法,在這種情況下,該執行緒會儲存本地方法棧的狀態並進入到另一個Java棧。

下圖描繪了這樣一個情景,就是當一個執行緒呼叫一個本地方法時,本地方法又回撥虛擬機器中的另一個 Java方法。

這幅圖展示了JAVA虛擬機器內部執行緒執行的全景圖。一個執行緒可能在整個生命週期中都執行Java方法,操作它的Java棧;或者它可能毫無障礙地在Java棧和本地方法棧之間跳轉。
在這裡插入圖片描述
該執行緒首先呼叫了兩個Java方法,而第二個Java方法又呼叫了一個本地方法,這樣導致虛擬機器使用了一個本地方法棧。假設這是一個C語言棧,其間有兩個C函式,第一個C函式被第二個Java方法當做本地方法呼叫,而這個C函式又呼叫了第二個C函式。之後第二個C函式又通過本地方法介面回撥了一個Java方法(第三個Java方法),最終這個Java方法又呼叫了一個Java方法(它成為圖中的當前方法)。

相關文章