Java 虛擬機運行時數據區詳解

本文摘自深入理解 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 等參數資訊

Tags: