前言
在 Java 程式執行過程中,作業系統為其分配了實體記憶體和虛擬記憶體。理解這兩者的概念有助於明晰記憶體管理和效能最佳化。
一、實體記憶體
實體記憶體是指計算機的實際 RAM(隨機存取儲存器)。Java 程序在執行時需要向作業系統請求記憶體資源,作業系統透過分配實體記憶體來滿足 Java 程序的記憶體需求。
簡而言之,實體記憶體是計算機系統的一種儲存介質和磁碟不同、容量相對較小,通常在幾GB到幾十GB之間,常常用於臨時資料儲存。JVM 在堆記憶體、棧記憶體等區域中分配的記憶體實際上會對映到實體記憶體。
二、虛擬記憶體
虛擬記憶體是作業系統的一種記憶體管理機制,它允許應用程式看到的記憶體地址空間遠大於實體記憶體。虛擬記憶體結合了 RAM 和磁碟(通常是交換空間 swap)的儲存資源,將其作為一個大的統一記憶體空間對外暴露。
JVM 和應用程式看到的是虛擬地址空間,而非直接訪問實體記憶體。
虛擬記憶體透過分頁(paging)機制管理,將實體記憶體和磁碟上的頁面檔案結合使用。當實體記憶體不足時,作業系統會將不常用的記憶體頁面換出到磁碟,並在需要時從磁碟換入。
簡而言之,虛擬記憶體會指向實體記憶體或者磁碟,當JVM處理的資料處於磁碟上時,JVM會將磁碟中的資料load進入實體記憶體。實體記憶體不足時,將記憶體中不需要處理的資料放入磁碟中。
三、Java記憶體模型
- 堆(Heap):這是 Java 應用程式使用的主要記憶體區域,用於存放物件。堆的大小可以透過 JVM 引數 -Xms 和 -Xmx 來設定。堆記憶體被作業系統對映到虛擬地址空間,具體的記憶體頁會在執行時根據需要分配到實體記憶體。
- 非堆記憶體(Non-Heap Memory):包括元空間(Metaspace)、程式碼快取、直接記憶體等區域。它們用於儲存類資訊、常量、JIT 編譯的程式碼等。這部分記憶體也消耗虛擬記憶體。
- 棧(Stack):每個執行緒都有自己的棧記憶體,用於儲存區域性變數和方法呼叫棧幀。棧的大小可以透過 JVM 引數 -Xss 來設定。棧也是分配在虛擬記憶體中的。
- 直接記憶體(Direct Memory):這是由 NIO(New I/O)庫使用的記憶體區域,允許 Java 直接從堆外分配記憶體,用於更高效地進行 I/O 操作。直接記憶體不是分配在堆上,但消耗的是虛擬記憶體,大小可以透過 -XX:MaxDirectMemorySize 來配置。
四、總結與反思
實體記憶體和虛擬記憶體的區別在於前者是實際的記憶體資源,後者是作業系統透過對映建立的邏輯空間。
五、附錄
1. 有虛擬記憶體為什麼Java程序還會OOM?
堆記憶體不足
Java 應用程式的堆記憶體是由 JVM 管理的,虛擬記憶體並不意味著可以無限制地分配堆記憶體。如果應用程式請求的堆記憶體超過了 JVM 設定的最大堆大小(透過 -Xmx 配置),即使系統有足夠的虛擬記憶體,JVM 也會丟擲 OOM。
元空間不足
從 Java 8 開始,元空間用於儲存類的後設資料,元空間大小不是固定的。如果動態載入了大量類而未配置足夠的元空間,可能會導致 OOM。這與虛擬記憶體的可用性無關。
直接記憶體不足
直接記憶體(透過 NIO 使用)是堆外分配的記憶體。如果應用程式需要大量直接記憶體並且未配置足夠的最大直接記憶體(-XX:MaxDirectMemorySize),也會引發 OOM。
總結
虛擬記憶體雖然提供了擴充套件的地址空間,但 Java 程序的記憶體管理依然受到堆、元空間和直接記憶體等限制的影響。
簡而言之,虛擬記憶體解決了計算過程將所有資料load到記憶體的困境,但是仍然受限於實體記憶體。
舉個例子:寫一個程式計算 a+b+c, 虛擬記憶體可以load a+b,然後再將計算結果x + c,保證記憶體中不會將不需要此刻處理的資料load到記憶體。
但是,對於JVM執行過程中正在使用的資料,仍然需要load到實體記憶體中。