深入理解 JVM 之 JVM 記憶體結構

TimberLiu發表於2019-02-25

Java 虛擬機器在執行 Java 程式 時,把它所管理的記憶體劃分為若干個不同的資料區域,主要包括以下五個部分:程式計數器、Java 堆、Java 虛擬機器棧、方法區和本地方法棧。

深入理解 JVM 之 JVM 記憶體結構

JVM 記憶體結構

程式計數器

程式計數器是當前執行緒所執行的位元組碼的行號指示器,它會指出下一條將要執行的指令的地址,位元組碼直譯器就是通過改變計數器的值來選取程式接下來執行的操作。

程式計數器是執行緒私有的一小塊記憶體,每條執行緒都要有一個獨立的程式計數器,以使執行緒切換後恢復到正確的執行位置。

  • 如果執行緒正在執行 Java 方法,則計數器記錄的是正在執行的虛擬機器位元組碼指令的地址
  • 如果執行 native 方法,則計數器為空

它也是唯一一個不會出現 OutOfMemoryError 的記憶體區域。

Java 虛擬機器棧

與程式計數器一樣,Java 虛擬機器棧也是執行緒私有的,線上程建立時 Java 棧會被建立,每個方法在在執行的同時都會建立一個棧幀,用於存放區域性變數表,運算元棧,動態連結,方法出口等資訊。每一個方法從呼叫直至執行完成,都對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

一般所謂的“棧”,指的是虛擬機器棧中區域性變數表部分,其中存放了各種基本資料型別( 8 種),物件引用(reference 型別) 和 returnAddress 型別。區域性變數表所需的空間在編譯期就已經確定並完成分配,在方法執行期間不會被改變。

Java 虛擬棧中可能出現兩種異常:

  • StackOverflowError:執行緒請求的棧深度大於虛擬機器所允許的深度
  • OutOfMemoryError:虛擬機器棧擴充套件時無法申請到足夠的記憶體

本地方法棧

本地方法棧與 Java 虛擬機器棧的作用類似,區別是 Java 虛擬機器棧為虛擬機器執行 Java 方法服務,而本地方法棧為虛擬機器執行 Native 方法服務。有的虛擬機器(例如 HotSpot 虛擬機器)直接把本地方法棧和 Java 虛擬機器棧合併在一起。

本地方法棧也可能會丟擲 StackOverflowErrorOutOfMemoryError 異常。

Java 堆

Java 堆是是虛擬機器中最主要的記憶體區域。它為執行緒共享,在虛擬機器啟動時建立,幾乎所有的物件例項都儲存在 Java 堆中。

Java 堆也被稱作 "GC" 堆。從記憶體回收角度看,可分為新生代和老年代。而新生代又可分為 Eden 區、From Survivor 區、To Survivor 區等。

Java 堆的實現,既可以實現為固定的,也可以是擴充套件的。當前虛擬機器都按照可擴充套件來實現,通過 -Xmx-Xms 控制堆大小。

如果堆中沒有記憶體並且也無法再擴充套件時,會丟擲 OutOfMemeoryError 異常。

方法區

方法區與 Java 堆一樣,為執行緒共享。用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等資料。也叫作 Non-Heap(非堆)。

如果方法區無法滿足記憶體分配需求,會丟擲 OutOfMemoryError 異常。

執行時常量池

執行時常量池是方法區的一部分。Class 檔案中的常量池用於編譯期生成的各種字面量和符號引用,這部分內容在類載入後被存入執行時常量池。

動態性是執行時常量池相對於 Class 檔案常量池的一個重要特徵,即不要求常量一定只有編譯期才能產生,執行期間也可能將新的常量放入池中。

執行時常量池受到方法區記憶體的限制,如果常量池無法再申請記憶體,就會丟擲 OutOfMemoryError 異常。

直接記憶體

直接記憶體並不由 JVM 管理,它是利用 Native 函式庫在 Java 堆外申請分配的記憶體區域,可以避免在 Java 堆和 Native 堆中複製資料以提高效能。

例如 NIO 中的 DirectByteBuffer 就可以作為這塊記憶體的引用進行操作直接記憶體。

永久代與元空間

有時會看到方法區被稱為永久代,其實兩者有著本質的區別。方法區是 JVM 規範中的定義,而永久代是 JVM 規範的一種實現,並且只有在 HotSpot 虛擬機器中如此,其他虛擬機器中沒有永久代的說法。

JDK1.6 之前,HotSpot 虛擬機器把 GC 分代收集擴充套件至方法區,或者說使用永久代實現方法區。不過永久代有 -XX:MaxPermSize 的上限,很容易遇到記憶體溢位問題。

所以在 JDK1.7 中,將部分資料已經轉移 Java HeapNative Heap 中,例如:將原本放在永久代中的字串池和類的靜態變數移出到 Java Heap 中,將符號引用轉移到 Native Heap 中。但永久代仍然存在,並沒有移除。

JDK1.8 中,取消了永久代,代替為元空間實現,它也是 JVM 規範中方法區的一種實現。不過它與永久代最大的不同是:元空間並不在虛擬機器中,而是將元空間放到本地記憶體中。所以預設情況下,它只受本地記憶體的限制,可以通過 -XX:MetaspaceSize 引數設定初始空間大小,預設沒有最大空間限制。

常見的 OOM 及原因

Java 中的 OOM 指的就是 java.lang.OutOfMemoryError 異常。主要有以下幾種:

java.lang.OutOfMemoryError:Java heap space

Java 堆中主要用於存放各種物件例項。當堆中沒有足夠的空間分配給新物件時,或者說達到了堆空間設定的最大空間限制,則會丟擲此異常。

引起記憶體溢位的原因主要有:

  • 流量訪問量大,超過設定的堆空間大小;
  • 記憶體洩露,不能被回收的物件消耗過多堆空間;

java.lang.OutOfMemoryError:Permgen space

JDK7 中,HotSpot 虛擬機器使用永久代實現方法區,永久代較小,而且回收效率較低,很容易出現記憶體溢位。

因此,JDK8 取消了永久代,使用元空間來實現方法區,存放在本地記憶體中。

java.lang.OutOfMemoryError:Metaspace

方法區主要儲存類的元資訊,HotSpot 後設資料區。當元空間沒有足夠的空間分配給載入的類時,會丟擲此異常。

引起後設資料區空間不足的原因主要有:

  • 載入的類太多,常見於 jsp 頁面過多時;
  • 元空間被實現在堆外,主要受到程式本身的記憶體限制,一般很難出現溢位。

參考資料

相關文章