Java 虛擬機器執行時資料區詳解

低吟不作語發表於2020-11-29

本文摘自深入理解 Java 虛擬機器第三版


概述

Java 虛擬機器在執行 Java 程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域,這些區域有各自的用途,以及建立和銷燬的時間,有的區域隨著虛擬機器程式的啟動而一直存在,有的區域則是依賴使用者執行緒的啟動和結束而建立和銷燬。因此,我們可以根據這個特點將區域劃為為執行緒公有區域和執行緒私有區域兩部分


程式計數器

程式計數器(Program Counter Register)是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器通過改變計數器的值來選取下一條需要執行的位元組碼指令,程式的執行流程都依賴該計數器來完成

由於 Java 虛擬機器的多執行緒是通過執行緒切換、分配處理器執行時間的方式來執行的,在任一時刻,一個處理器只能執行一條執行緒中的指令。為了保證執行緒切換後能恢復到原來正在執行的位置,每條執行緒都需要有一個獨立的程式計數器。因此程式計數器是執行緒私有的,各個執行緒之間的計數器互不影響,獨立儲存

如果執行緒正在執行的是一個 Java 方法,計數器記錄的就是正在執行的虛擬機器位元組碼指令的地址;如果執行的是本地方法,則計數器對應的值為空。程式計數器是唯一一個在 Java 虛擬機器規範中沒有規定 OutOfMemoryError 情況的區域,至於為什麼,可以看看下面的官方解釋:

The Java Virtual Machine's pc register is wide enough to hold a returnAddress or a native pointer on the specific platform

翻譯過來就是:Java 虛擬機器的 pc 暫存器足夠寬,可以在特定平臺上儲存 returnAddress 或本機指標。也就是說,程式計數器儲存的只是一個值,這個值指向下一條要執行的指令的位置,而這個值不可能超過 pc 暫存器的範圍,自然也就不存在記憶體溢位的問題了


Java 虛擬機器棧

Java 虛擬機器棧(Java Virtual Machine Stack)也是執行緒私有的,其描述的是 Java 方法執行的執行緒記憶體模型:每個方法被執行時,Java 虛擬機器都會同步建立一個棧幀(Stack Frame)用於儲存區域性變數表、運算元棧、動態連線、方法出口等資訊。每個方法被呼叫直至執行完畢的過程,就對應著一個棧幀從入棧到出棧的過程

區域性變數表存放了編譯器可知的各種 Java 虛擬機器基本資料型別、物件引用以及 returnAddress(指向了一條位元組碼指令的地址)。這些資料型別在區域性變數表中的儲存空間以區域性變數槽(Slot)來表示,其中 64 位的 long 和 double 型別會佔用兩個 Slot,其餘只佔一個。區域性變數表所需的記憶體空間是在編譯期就完成分配的,執行期間不會改變區域性變數表的大小,這裡的大小指的是 Slot 的數量,至於一個 Slot 佔用多少空間,則是由虛擬機器自行決定的事情

在 Java 虛擬機器規範中,對這個記憶體區域規定了兩類異常狀況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲 StackOverflowError 異常;如果 Java 虛擬機器棧容量可以動態擴充套件,則當棧擴充套件無法申請足夠的記憶體時會丟擲 OutOfMemoryError 異常(HotSpot 虛擬機器是無法動態擴充套件的,因此只有當執行緒請求棧空間失敗時才會丟擲 OOM 異常)


本地方法棧

本地方法棧(Native Method Stacks)與虛擬機器棧的作用是類似的,其區別只是虛擬機器棧為虛擬機器執行 Java 方法服務,而本地方法棧則是為虛擬機器執行本地方法服務,本地方法棧也是執行緒私有的

Java 虛擬機器規範對本地方法棧所使用的語言、使用方式、資料結構並沒有強制規定,具體的虛擬機器可以根據需要自由實現。HotSpot 虛擬機器直接就把本地方法棧和虛擬機器棧合二為一。與虛擬機器棧一樣,本地方法棧也會丟擲 StackOverflowError 和 OutOfMemoryError 異常


Java 堆

Java 堆(Heap)是虛擬機器所管理的記憶體中最大的一塊記憶體區域,被所有執行緒共享,此區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體

Java 堆是垃圾收集器管理的記憶體區域,所以從記憶體回收的角度來看,由於現代垃圾收集器大部分都基於分代收集理論設計,所以 Java 堆中經常出現“新生代”、“老年代”、“永久代”、“Eden 空間”、“From Survivor 空間”、“To Survivor 空間”等名詞。在這裡要指明的是,這些區域僅僅是一部分垃圾收集器的共同特性或者說設計風格而已,並非某個 Java 虛擬機器具體實現的記憶體佈局

根據 Java 虛擬機器規範規定,Java 堆可以處於物理上不連續的記憶體空間,但在邏輯上必須視為連續。但對於大物件(如陣列物件),多數虛擬機器出於實現簡單、儲存高效的考慮,很有可能會要求連續的陣列

Java 堆既可以被實現成固定大小,也可以是可擴充套件的(通過引數 -Xmx 和 -Xms 設定),如果沒有記憶體可分配給例項,並且堆也無法再擴充套件時,會丟擲 OutOfMemoryError 異常


方法區

方法區(Method Area)與 Java 堆一樣,也是執行緒共享區域,用於儲存已被虛擬機器載入的型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等資料。雖然在 Java 虛擬機器規範中把方法區描述為堆的一個邏輯部分,但是它還有一個別名叫作“非堆”,目的就是與 Java 堆區分開來

如何實現方法區取決於虛擬機器的具體實現,並不受 Java 虛擬機器規範管束。對於 HotSpot 虛擬機器,JDK8 以前方法區是使用永久代實現的,永久代屬於 JVM 執行時記憶體區域的一部分,這樣使得 HotSpot 的垃圾收集器能像管理 Java 堆一樣管理這部分記憶體

但現在回頭看,使用永久代實現方法區並不是一個好主意,這會導致 Java 應用更容易遇到記憶體溢位的問題,為此我們需要設定 -XX:MaxPermSize 的上限。在 JDK6 的時候 HotSpot 的開發團隊就有放棄永久代,採用本地記憶體(Native Memory)來實現方法區的計劃(這樣只要不觸碰到機器記憶體上限就不會溢位)。從 JDK7 開始,已經把原來放在永久代的字串常量池、靜態變數等移出,到了 JDK8,終於完全廢棄永久代的概念,採用本地記憶體實現的元空間(Meta-Space)來替代,把 JDK7 中永久代還剩餘的內容(主要是型別資訊)全部移到元空間

和 Java 堆一樣,方法區除了可以不需要連續的記憶體和可以選擇固定大小或可擴充套件外,甚至可以選擇不實現垃圾收集。相對而言,垃圾收集行為在這個區域比較少出現,但並不意味著不需要垃圾收集,這個區域的記憶體回收目標主要是針對常量池的回收和型別的解除安裝。根據 Java 虛擬機器規定,如果方法區無法滿足新的記憶體分配需求,將丟擲 OutOfMemoryError 異常


執行時常量池

執行時常量池(Runtime Constant Pool)是方法區的一部分。Class 檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池表(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中

對於執行時常量池,Java 虛擬機器規範並沒有做任何細節的要求,不同的虛擬機器可以根據需要來自主實現。一般來說,除了儲存 Class 檔案中描述的符號引用外,還會把由符號引用翻譯出來的直接引用也儲存在執行時常量池

執行時常量池相對於 Class 檔案常量池的一個重要特徵就是具備動態性,Java 語言並不要求常量一定只能在編譯期才能產生,也就是說,並非預置入 Class 檔案常量池的內容才能進入方法區執行時常量池,執行期間也可以將新的常量放入池中,用得最多的便是 String 類的 intern() 方法


直接記憶體

直接記憶體(Direct Memory)並不是虛擬機器執行時資料區的一部分,但這部分也被頻繁使用,也可能導致 OutOfMemoryError 異常

在 JDK1.4 新加入了 NIO 類,這是一種基於通道(Cancel)與緩衝區(Buffer)的 I/O 方式,可以使用 Native 函式直接分配堆外記憶體,然後通過一個儲存在 Java 堆中 DirectByteBuffer 物件作為這塊記憶體的引用進行操作,既能提高效能,還避免了 Java 堆和直接記憶體的頻繁交換資料

本機直接記憶體的分配雖然不受 JVM 限制,但還是會受本機記憶體大小以及處理器定址空間的限制。伺服器管理員配置虛擬機器引數時,同時也要考慮直接記憶體的因素,來設定合理的 -Xmx 等引數資訊


相關文章