Java虛擬機器 —— 執行時資料區

xiaoyanger發表於2017-09-22

Java虛擬機器記憶體,是指JVM的執行時資料區域,主要分為:方法區、堆、虛擬機器棧、本地方法棧、程式計數器。其中方法區和堆為索引執行緒的共享資料區,而虛擬機器棧、本地方法棧、程式計數器為執行緒隔離的資料區。

程式計數器

每個執行緒都有一個獨立的計數器用來記錄程式當前執行的指令,可以看成是當前執行緒所執行的位元組碼的行號指示器。如果執行緒正在執行Java方法,計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果執行的是Native方法,計數器記錄值為空(Undefined)。程式計數器佔用的記憶體空間非常小,是執行緒的私有區域,此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

虛擬機器棧

虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同。虛擬機器棧是一個後進先出的資料結構,裡面存放的是棧幀,每個Java方法的呼叫對應一個棧幀在虛擬機器棧中的入棧和出棧。當執行緒執行一個Java方法執行時,就會建立一個新的棧幀並壓入到該執行緒的虛擬機器棧的棧頂,Java方法執行結束後棧頂的該棧幀就會彈出棧並銷燬。

棧幀裡面存放的是Java方法執行的一些資料,包括區域性變數表、運算元棧、動態連線、方法出口等。

區域性變數表

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

區域性變數表的容量以變數槽(Slot)為最小單位,32位虛擬機器中一個Slot可以存放一個32位以內的資料型別(boolean、byte、char、short、int、float、reference和returnAddress八種)。reference型別虛擬機器規範沒有明確說明它的長度,但一般來說,虛擬機器實現至少都應當能從此引用中直接或者間接地查詢到物件在Java堆中的起始地址索引和方法區中的物件型別資料。returnAddress型別是為位元組碼指令jsr、jsr_w和ret服務的,它指向了一條位元組碼指令的地址。

Java虛擬機器是使用區域性變數表完成引數值到Java方法引數變數列表的傳遞過程的,如果是例項方法(非static),那麼區域性變數表的第0位索引的Slot預設是用於傳遞方法所屬物件例項的引用,在方法中通過this訪問。

Slot是可以重用的,下一次分配Slot的時候,將會覆蓋原來的資料。Slot對物件的引用會影響GC(要是被引用,將不會被回收)。

運算元棧

運算元棧也常被稱為操作棧,同樣是一個後進先出的資料結構。當一個方法剛剛開始執行的時候,這個方法的運算元棧是空的,在方法的執行過程中,會有各種位元組碼指令向運算元棧中寫入和提取內容,也就是入棧出棧操作。在做算術運算的時候是通過運算元棧來進行的,在呼叫其他方法的時候是通過運算元棧來進行引數傳遞的。JVM將運算元棧作為工作區。JVM沒有暫存器,所有的引數傳遞和返回值都是基於運算元棧來完成的。

比如,執行引擎執行c = a + b時,會先被操作的引數ab壓入運算元棧,然後操作指令將他們彈出棧,並執行操作,將結果再壓入棧。

Java虛擬機器的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的“棧”就是運算元棧。

動態連線

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

方法出口(返回地址)

當一個方法被執行後,有兩種方式退出這個方法。第一種方式是執行引擎遇到任意一個方法返回的位元組碼指令,這時候可能會有返回值傳遞給上層的方法呼叫者(呼叫當前方法的方法稱為呼叫者),是否有返回值和返回值的型別將根據遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。

另外一種退出方式是,在方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理,無論是Java虛擬機器內部產生的異常,還是程式碼中使用athrow位元組碼指令產生的異常,只要在本方法的異常表中沒有搜尋到匹配的異常處理器,就會導致方法退出,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個方法使用異常完成出口的方式退出,是不會給它的上層呼叫者產生任何返回值的。

無論採用何種退出方式,在方法退出之後,都需要返回到方法被呼叫的位置,程式才能繼續執行,方法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,呼叫者的PC計數器的值就可以作為返回地址,棧幀中很可能會儲存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會儲存這部分資訊。

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

虛擬機器棧Error

Java虛擬機器棧有可能出現的error就是StackOverflowErrorOutOfMemoryError。當執行緒請求的棧深度大於Java虛擬機器棧允許的深度時,就會丟擲StackOverflowError錯誤。比如將一個方法反覆遞迴,最終就會出現StackOverflowError。當Java虛擬機器棧可以動態擴充套件時(大部分的 Java 虛擬機器都可動態擴充套件,不過 Java 虛擬機器規範中也允許固定長度的虛擬機器棧),如果無法申請到足夠的記憶體來擴充套件棧,就會丟擲OutOfMemoryError錯誤。

本地方法棧

本地方法棧與虛擬機器棧的功能類似,他們的區別在於虛擬機器棧為執行Java程式碼方法服務,而本地方法棧是為Native方法服務。本地方法棧就是一個C的方法棧,本地方法棧的引數順序、返回值和典型的C程式相同,本地方法一般來說可以(依賴 JVM 的實現)反過來呼叫 JVM 中的 Java 方法。這種native方法呼叫Java會發生在棧(一般是Java棧)上,執行緒將離開本地方法棧,並在 Java 棧上開闢一個新的棧幀。

與虛擬機器棧一樣,本地方法棧也會丟擲StackOverflowErrorOutOfMemoryError

堆是Java虛擬機器中最大的一塊記憶體區域,它是有所有的執行緒共享。幾乎所有的例項物件和陣列都是在堆中存放。只要是通過new關鍵字建立物件或者直接宣告陣列,都會在堆中開闢記憶體空間來存放。因為在棧幀被建立後無法調整大小,棧幀中只能存放物件和陣列在堆中的引用。方法或執行緒結束時物件和陣列不會立即被移除銷燬,它只能由垃圾回收器回收。

同樣地,如果在堆中沒有記憶體來完成例項分配,並且堆也無法擴充套件時,將會丟擲OutOfMemoryError

方法區

方法區與堆一樣,是各個執行緒共享的記憶體區域,它儲存已經被虛擬機器載入的類資訊(包括欄位資訊、方法資訊、方法程式碼等)、常量、靜態變數、即時編譯器編譯後的程式碼等資料。

方法區中的記憶體一般不會被GC回收,GC也很難回收。方法區的記憶體回收主要是針對針對常量池的回收和對類的解除安裝。根據 Java 虛擬機器規範的規定,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常。

執行時常量池

執行時常量池是方法區的一部分,Class檔案除了有關類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。執行時常量池可以理解為是類或介面的常量池的執行時表現形式。


參考:
Java虛擬機器記憶體區域劃分詳解
JAVA記憶體結構之執行時棧幀結構

相關文章