天天看點

JVM記憶體結構 —— 運作時資料區一、 程式計數器二、 Java虛拟機棧三、 本地方法棧四、堆五、方法區六、運作時常量池七、直接記憶體

首先需要明白的是 Java記憶體模型和JVM記憶體結構不是一個概念,在面試時要搞清楚面試官問的到底是什麼。

  • Java 記憶體模型,描述的是多線程允許的行為
  • JVM 記憶體結構,描述的是線程運作所設計的記憶體空間

常說的 JVM 記憶體結構指的就是運作時資料區,其中堆、方法區被線程共享,程式計數器、虛拟機棧、本地方法棧被線程獨享。(該結構是《Java虛拟機規範》定義的規範,不是具體某個虛拟機的實作)

JVM記憶體結構 —— 運作時資料區一、 程式計數器二、 Java虛拟機棧三、 本地方法棧四、堆五、方法區六、運作時常量池七、直接記憶體

一、 程式計數器

程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看作是目前線程所執行的位元組碼的行号訓示器。位元組碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,它是程式控制流的訓示器,分支、循環、跳轉、異常處 理、線程恢複等基礎功能都需要依賴這個計數器來完成。

另外,為了線程切換後能恢複到正确的執行位置,每條線程都需要有一個獨立的程式計數器,各線程之間計數器互不影響,獨立存儲,我們稱這類記憶體區域為“線程私有”的記憶體。

如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的位址;如果正在執行的是本地(Native)方法,這個計數器值則應為空(Undefined)。

注意: 程式計數器是唯一一個不會出現 OutOfMemoryError 的記憶體區域,它的生命周期随着線程的建立而建立,随着線程的結束而死亡。

二、 Java虛拟機棧

與程式計數器一樣,Java 虛拟機棧也是線程私有的,它的生命周期和線程相同,描述的是 Java 方法執行的記憶體模型。

每個方法被執行的時候,Java虛拟機都會同步建立一個棧幀(Stack Frame) 用于存儲 局部變量表、操作數棧、動态連接配接、方法出口等資訊。每一個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛拟機棧中從入棧到出棧的過程。

JVM記憶體結構 —— 運作時資料區一、 程式計數器二、 Java虛拟機棧三、 本地方法棧四、堆五、方法區六、運作時常量池七、直接記憶體

1.局部變量表

局部變量表是一組局部變量值存儲空間,用于存放方法參數和方法内部定義的局部變量。

存放類型:局部變量表存放了編譯期可知的各種Java虛拟機

基本資料類型

(boolean、byte、char、short、int、 float、long、double)、

對象引用

(reference類型,它并不等同于對象本身,可能是一個指向對象起始 位址的引用指針,也可能是指向一個代表對象的句柄或者其他與此對象相關的位置)和

returnAddress 類型

(指向了一條位元組碼指令的位址)。

存放方式: 這些資料類型在局部變量表中的存儲空間以

局部變量槽

(Slot)來表示。32位虛拟機中一個Slot可以存放一個32位的資料,其中64位長度的long和 double類型的資料會占用兩個變量槽,其餘的資料類型隻占用一個。(不足一個槽大小的變量轉為int存儲)

大小固定:局部變量表所需的記憶體空間在編譯期間完成配置設定,當進入一個方法時,這個方法需要在棧幀中配置設定多大的局部變量空間是完全确定的,在方法運作期間不會改變局部變量表的大小。這裡說的“大小”是指變量槽的數量, 虛拟機真正使用多大的記憶體空間(譬如按照1個變量槽占用32個比特、64個比特,或者更多)來實作一 個變量槽,這是完全由具體的虛拟機實作自行決定的事情。

2.操作數棧

Java虛拟機的解釋執行引擎被稱為

"基于棧的執行引擎"

,其中所指的棧就是指操作數棧。

操作數棧

,也可以稱之為

表達式棧

(Expression Stack)。操作數棧和局部變量表在通路方式上存在着較大差異,操作數棧并非采用通路索引的方式來進行資料通路的,而是通過标準的入棧和出棧操作來完成一次資料通路。

虛拟機把操作數棧作為它的工作區——大多數指令都要從這裡彈出資料,執行運算,然後把結果壓回操作數棧。比如,iadd指令就要從操作數棧中彈出兩個整數,執行加法運算,其結果又壓回到操作數棧中.

3.動态連接配接

每個棧幀都包含一個指向運作時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法調用過程中的動态連接配接。

在 Class 檔案的常量池中存有大量的符号引用,位元組碼中的方法調用指令就以常量池中指向方法的符号引用為參數。這些符号引用一部分會在類加載階段或第一次使用的時候轉化為直接引用,這種轉化稱為靜态解析。另外一部分将在每一次的運作期期間轉化為直接引用,這部分稱為動态連接配接。

因為 Java 是在運作期間動态連結的,是以為了支援動态連結,需要将方法區裡面的符号引用轉為直接引用(即:給出位址),這就叫動态連結。

4.方法出口

方法正常退出或抛出異常退出,傳回方法被調用的位置

運作時錯誤:Java 虛拟機棧會出現兩種錯誤:

StackOverFlowError

OutOfMemoryError

  • StackOverFlowError: 若 Java 虛拟機棧的記憶體大小不允許動态擴充,那麼當線程請求棧的深度超過目前 Java 虛拟機棧的最大深度的時候,就抛出 StackOverFlowError 錯誤。
  • OutOfMemoryError: 若 Java 虛拟機堆中沒有空閑記憶體,并且垃圾回收器也無法提供更多記憶體的話。就會抛出 OutOfMemoryError 錯誤。

三、 本地方法棧

和虛拟機棧所發揮的作用非常相似,差別是: 虛拟機棧為虛拟機執行 Java 方法 (也就是位元組碼)服務,而本地方法棧則為虛拟機使用到的 Native 方法服務。 在 HotSpot 虛拟機棧和 Java 虛拟機棧合二為一。

本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用于存放該本地方法的局部變量表、操作數棧、動态連結、出口資訊。

方法執行完畢後相應的棧幀也會出棧并釋放記憶體空間,也會出現 StackOverFlowError 和 OutOfMemoryError 兩種錯誤。

四、堆

Java 虛拟機所管理的記憶體中最大的一塊,Java 堆是所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。此記憶體區域的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體以及數組都在這裡配置設定記憶體。

Java世界中“幾乎”所有的對象都在堆中配置設定,但是,随着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上配置設定、标量替換優化技術将會導緻一些微妙的變化,所有的對象都配置設定到堆上也漸漸變得不那麼“絕對”了。從jdk 1.7開始已經預設開啟逃逸分析,如果某些方法中的對象引用沒有被傳回或者未被外面使用(也就是未逃逸出去),那麼對象可以直接在棧上配置設定記憶體。

Java堆是垃圾收集器管理的記憶體區域,是以一些資料中它也被稱作“GC堆”。從回收記憶體的角度看,由于現代垃圾收集器大部分都是基于分代收集理論設計的,是以Java堆中經常會出現“新生代”“老年代”“永久代”“Eden空間”“From Survivor空間”“To Survivor空間”等名詞。這些區域劃分僅僅是一部分垃圾收集器的共同特性或者說設計風格而已,而非某個Java虛拟機具體實作的固有記憶體布局,更不是《Java虛拟機規範》裡對Java堆的進一步細緻劃分。

(具體可見垃圾回收章節)

Java堆既可以被實作成固定大小的,也可以是可擴充的,不過目前主流的Java虛拟機都是按照可擴充來實作的(通過參數-Xmx和-Xms設定)。如果在Java堆中沒有記憶體完成執行個體配置設定,并且堆也無法再 擴充時,Java虛拟機将會抛出OutOfMemoryError異常。

五、方法區

方法區與 Java 堆一樣,是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。雖然 Java 虛拟機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個别名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。

方法區和永久代的關系

《Java 虛拟機規範》隻是規定了有方法區這麼個概念和它的作用,并沒有規定如何去實作它。那麼,在不同的 JVM 上方法區的實作肯定是不同的了。 方法區和永久代的關系很像 Java 中接口和類的關系,類實作了接口,而永久代就是 HotSpot 虛拟機對虛拟機規範中方法區的一種實作方式。 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛拟機規範中的定義,是一種規範,而永久代是一種實作,一個是标準一個是實作,其他的虛拟機實作并沒有永久代這一說法。

參數設定

JDK 1.8 之前永久代還沒被徹底移除的時候通常通過下面這些參數來調節方法區大小

-XX:PermSize=N //方法區 (永久代) 初始大小

-XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值将會抛出 OutOfMemoryError 異常:java.lang.OutOfMemoryError: PermGen

JDK 1.8 的時候,方法區(HotSpot 的永久代)被徹底移除了(JDK1.7 就已經開始了),取而代之是元空間,元空間使用的是直接記憶體。

下面是一些常用參數:

-XX:MetaspaceSize=N //設定 Metaspace 的初始(和最小大小)

-XX:MaxMetaspaceSize=N //設定 Metaspace 的最大大小

元空間代替永久代

整個永久代有一個 JVM 本身設定固定大小上限,無法進行調整,動态生成類的情況比較容易出現永久代的記憶體溢出。而元空間使用的是直接記憶體,受本機可用記憶體的限制,雖然元空間仍舊可能溢出,但是比原來出現的幾率會更小。

考慮到HotSpot未來的發展,在JDK 6的時候HotSpot開發團隊就有放棄永久代,逐漸改為采用本地記憶體(Native Memory)來實作方法區的計劃了,到了JDK 7的HotSpot,已經把原本放在永久代的字元串常量池、靜态變量等移出,而到了JDK 8,終于完全廢棄了永久代的概念,改在本地記憶體中實作的元空間(Meta- space)來代替,把JDK 7中永久代還剩餘的内容(主要是類型資訊)全部移到元空間中。

對于Java8, HotSpots取消了永久代,那麼是不是也就沒有方法區了呢?當然不是,方法區是一個規範,規範沒變,它就一直在。那麼取代永久代的就是元空間。它和永久代有什麼不同的?存儲位置不同,永久代實體上是堆的一部分,和新生代,老年代位址是連續的,而元空間屬于本地記憶體;存儲内容不同,元空間存儲類的元資訊,靜态變量和常量池等并入堆中。相當于永久代的資料被分到了堆和元空間中。

JVM記憶體結構 —— 運作時資料區一、 程式計數器二、 Java虛拟機棧三、 本地方法棧四、堆五、方法區六、運作時常量池七、直接記憶體

雖然JDK8之後永久代的資料被分到了堆和元空間中,但其實這隻是實體空間上的分拆;邏輯上,類的資訊,靜态變量和常量亦然都還是屬于方法區的一部分。具體一些細節問題可以參考字元串常量池和運作時常量池是在堆還是在方法區?

六、運作時常量池

運作時常量池是方法區的一部分。

方法區中的class 檔案中除了有類的版本、字段、方法、接口等描述資訊外,還有常量池表(用于存放編譯期生成的各種字面量和符号引用),這其實是靜态常量池。當class檔案被加載完成後,java虛拟機會将靜态常量池裡的内容轉移到動态常量池(運作時常量池)。運作時常量池和靜态常量池的辨析

既然運作時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體時會抛出 OutOfMemoryError 錯誤。

  • JDK1.7之前運作時常量池邏輯包含字元串常量池存放在方法區, 此時hotspot虛拟機對方法區的實作為永久代
  • JDK1.7 字元串常量池被從方法區拿到了堆中, 這裡沒有提到運作時常量池,也就是說字元串常量池被單獨拿到堆,運作時常量池剩下的東西還在方法區, 也就是hotspot中的永久代 。
  • JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字元串常量池還在堆, 運作時常量池還在方法區, 隻不過方法區的實作從永久代變成了元空間(Metaspace)

七、直接記憶體

直接記憶體并不是虛拟機運作時資料區的一部分,也不是虛拟機規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用。而且也可能導緻 OutOfMemoryError 錯誤出現。

JDK1.4 中新加入的 NIO(New Input/Output) 類,引入了一種基于通道(Channel) 與緩存區(Buffer) 的 I/O 方式,它可以直接使用 Native 函數庫直接配置設定堆外記憶體,然後通過一個存儲在 Java 堆中的 DirectByteBuffer 對象作為這塊記憶體的引用進行操作。這樣就能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆之間來回複制資料。

本機直接記憶體的配置設定不會受到 Java 堆的限制,但是,既然是記憶體就會受到本機總記憶體大小以及處理器尋址空間的限制。

注意:直接記憶體和本地記憶體不是一個概念