天天看點

深入了解JVM——JVM記憶體結構

目錄

Java 堆(Heap)

方法區(Method Area)

Java 虛拟機棧(JVM Stacks)

程式計數器(Program Counter Register)

本地方法棧(Native Method Stacks)

JVM 記憶體結構是指:Java 虛拟機定義了若幹種程式運作期間會使用的運作時資料區,其中有一些會随着虛拟機啟動而建立,随着虛拟機退出而銷毀,另一些則與線程一一對應,随着線程的開始而建立,随着線程的結束而銷毀。具體的運作時資料區如下圖所示:

深入了解JVM——JVM記憶體結構

在 Java 虛拟機規範中,定義了五種運作時資料區,分别是 Java 堆、方法區、虛拟機棧、本地方法區、程式計數器,其中 Java 堆和方法區是線程共享的。接下來就具體看看這 五種運作時資料區。

Java 堆(Heap)

Java 堆是所有線程共享的一塊記憶體區域,它在虛拟機啟動時 就會被建立,并且單個 JVM 程序有且僅有一個 Java 堆。Java 堆是用來存放對象執行個體及數組,也就是說我們代碼中通過 new 關鍵字 new 出來的對象都存放在這裡。是以這裡也就成為了垃圾回收器的主要活動營地了,于是它就有了一個别名叫做 GC 堆,根據垃圾回收器的規則,我們可以對 Java 堆進行進一步的劃分,具體 Java 堆記憶體結構如下圖所示:

深入了解JVM——JVM記憶體結構

我們可以将 Java 堆劃分為新生代和老年代兩個大子產品,在新生代中,我們又可以進一步分為 Eden 空間、From Survivor 空間(s0)、To Survivor 空間(s1),Survivor 空間有一個為空,用于發生 GC 時存放存活對象,老年代存放的是經過多次 Minor GC 仍然存活的對象或者是一些大對象,FGC 就是發生在老年代。

上面就是 Java 堆的具體結構,我們也知道 Java 堆中的各空間大小,我們是可以動态控制的,這個在圖中我也進行了簡單的标注,下面我們一起來詳細的了解一下這三個參數:

  • -Xms:JVM啟動時申請的初始Heap值,預設為作業系統實體記憶體的1/64,例如-Xms20m
  • -Xmx:JVM可申請的最大Heap值,預設值為實體記憶體的1/4,例如-Xmx20m,我們最好将 -Xms 和 -Xmx 設為相同值,避免每次垃圾回收完成後JVM重新配置設定記憶體;
  • -Xmn:設定新生代的記憶體大小,-Xmn 是将NewSize與MaxNewSize設為一緻,我們也可以分别設定這兩個參數

在 Java 堆中會發生 OOM 異常,當我們的 Java 堆内有足夠的空間去完成執行個體配置設定時,并且堆也無法擴充,将會抛出我們常見的OutOfMemoryError 異常。

方法區(Method Area)

方法區(Method Area)與 Java 堆一樣,是各個線程共享的記憶體區域,是 Java 虛拟機中唯二的記憶體共享區域。在 Java 虛拟機規範中是這樣定義方法區的:它存儲了每個類的結構資訊,例如運作時常量池、字段、方法資料、構造函數和普通方法的位元組碼内容,還包括一些在類、執行個體、接口初始化時用到的特殊方法。

方法區在虛拟機啟動的時候被建立,雖然方法區是堆的邏輯組成部分,但是簡單的虛拟機實作可以選擇在這個區域不實作垃圾收集與壓縮,方法區在實際記憶體空間中可以不是連續的,對于方法區的容量,你可以是固定的,也可以随着程式的執行動态擴充,并且在不需要過多空間時自動收縮。

上面都是 Java 虛拟機中的規範,來看看具體的實作,拿我們常用的 HotSpot 虛拟機來說,在 JDK1.8 之前,方法區也被稱作為永久代,這個方法區會發生我們常見的 

java.lang.OutOfMemoryError: PermGen space

 異常,我們也可以通過啟動參數來控制方法區的大小:

  • -XX:PermSize 設定最小空間
  • -XX:MaxPermSize 設定最大空間

在 JDK1.8 之後,HotSpot 虛拟機對方法區進行了不小的改動,徹底移除了永久代,将原來存放在永久代的資料遷移至 Java 堆 或者 Metaspace,方法區被移至到了 Metaspace,字元串常量移至 Java Heap,換句話說就是 JDK1.8 開始,Metaspace 也就是我們所謂的方法區,為什麼要做這個改變呢?也許是基于以下兩點原因:

  • 由于 PermGen 記憶體經常會溢出,引發惱人的 java.lang.OutOfMemoryError: PermGen,是以 JVM 的開發者希望這一塊記憶體可以更靈活地被管理,不要再經常出現這樣的 OOM
  • 移除 PermGen 可以促進 HotSpot JVM 與 JRockit VM 的融合,因為 JRockit 沒有永久代。

我們也可以通過設定參數來控制 Metaspace 的空間大小,主要有以下幾個指令:

  • -XX:MetaspaceSize :配置設定給類中繼資料空間(以位元組計)的初始大小。MetaspaceSize的值設定的過大會延長垃圾回收時間。垃圾回收過後,引起下一次垃圾回收的類中繼資料空間的大小可能會變大。
  • -XX:MaxMetaspaceSize: 配置設定給類中繼資料空間的最大值,超過此值就會觸發Full GC,此值預設沒有限制,但應取決于系統記憶體的大小。JVM會動态地改變此值。
  • -XX:MinMetaspaceFreeRatio:表示一次GC以後,為了避免增加中繼資料空間的大小,空閑的類中繼資料的容量的最小比例,不夠就會導緻垃圾回收。
  • -XX:MaxMetaspaceFreeRatio:表示一次GC以後,為了避免增加中繼資料空間的大小,空閑的類中繼資料的容量的最大比例,不夠就會導緻垃圾回收。

Java 虛拟機棧(JVM Stacks)

每一條 Java 虛拟機線程都有自己私有的 Java 虛拟機棧,這個 Java 虛拟機棧跟線程同時建立,是以它跟線程有相同的生命周期。Java 虛拟機棧描述的是 Java 方法執行的記憶體模型:每一個方法在執行的同時都會建立一個棧幀,用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊,每一個方法從調用直至執行完成的過程,就對應着一個棧幀在 Java 虛拟機棧中的入棧到出棧的過程。

局部變量表存放了編譯期可知的各種基本資料類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference 類型,它不等同于對象本身,根據不同的虛拟機實作,它可能是一個指向對象起始位址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和 returnAddress 類型(指向了一條位元組碼指令的位址)。

其中 64 位長度的 long 和 double 類型的資料會占用 2 個局部變量空間(Slot),其餘的資料類型隻占用 1 個。局部變量表所需的記憶體空間在編譯期間完成配置設定,當進入一個方法時,這個方法需要在幀中配置設定多大的局部變量空間是完全确定的,在方法運作期間不會改變局部變量表的大小。

Java 虛拟機棧既允許被實作成固定的大小,也允許根據計算動态來擴充和收縮,如果采用固定大小的話,每一個線程的 Java 虛拟機棧容量可以線上程建立的時候獨立標明。在 Java 虛拟機棧中會發生兩種異常,這個在虛拟機規範中有指出:

  • 如果線程請求配置設定的棧容量超過 Java 虛拟機棧允許的最大容量,Java 虛拟機将會抛出 StackOverflowError 異常;
  • 如果 Java 虛拟機棧可以動态擴充,并且在嘗試擴充的時候無法申請到足夠的記憶體或者在建立新的線程時沒有足夠的記憶體去建立對應的 Java 虛拟機棧,那麼虛拟機将會抛出 OutOfMemoryError 異常。

程式計數器(Program Counter Register)

程式計數器也是線程私有的,它隻需要一塊較小的記憶體空間,你可以把它看作目前線程所執行的位元組碼的行号訓示器,在虛拟機的概念模型裡(僅是概念模型,各種虛拟機可能會通過一些更高效的方式去實作),位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。

我們知道在多線程的情況下,并不是一條線程一直執行完,而是多個線程輪流切換執行,是以為了線程切換後能夠恢複到正确的執行位置,我們就需要程式計數器來告訴線程接下來該執行哪條指令。如果線程正在執行的是一個Java 方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址,如果正在執行的是 Natvie 方法,這個計數器值則為空(Undefined)。

需要特别注意的是,程式計數器是唯一一個在Java虛拟機規範中沒有規定任何OutOfMemoryError 情況的區域。

本地方法棧(Native Method Stacks)

本地方法棧(Native Method Stacks)與 Java 虛拟機棧所發揮的作用是非常相似的,其差別不過是 Java 虛拟機棧為虛拟機執行 Java 方法(也就是位元組碼)服務,而本地方法棧則是為虛拟機使用到的 Native 方法服務。虛拟機規範中對本地方法棧中的方法使用的語言、使用方式與資料結構并沒有強制規定,是以具體的虛拟機可以自由實作它。甚至有的虛拟機(譬如Sun HotSpot虛拟機)直接就把本地方法棧和虛拟機棧合二為一。

與 Java 虛拟機棧一樣,本地方法棧區域也會抛出 StackOverflowError 和 OutOfMemoryError 異常。