Java虛擬機器詳解(二)------執行時記憶體結構

YSOcean發表於2019-07-05

  首先通過一張圖瞭解 Java程式的執行流程:

  

  我們編寫好的Java原始碼程式,通過Java編譯器javac編譯成Java虛擬機器識別的class檔案(位元組碼檔案),然後由 JVM 中的類載入器載入編譯生成的位元組碼檔案,載入完畢之後再由 JVM 執行引擎去執行。在載入完畢到執行過程中,JVM會將程式執行時用到的資料和相關資訊儲存在執行時資料區(Runtime Data Area),這塊區域也就是我們常說的JVM記憶體結構,垃圾回收也是作用在該區域。

  關於這幅圖涉及到的:

  ①、class檔案

  ②、類載入器

  ③、執行時資料區

  ④、執行引擎

  ⑤、垃圾回收器

  這都是接下來將要介紹的重點。

  本篇部落格我們將首先介紹什麼是執行時資料區。

  PS:下面介紹的是根據 Java虛擬機器規範 定義的執行時資料區,上一篇部落格我們講過根據虛擬機器規範實現的虛擬機器有很多個,而不同的虛擬機器其執行時資料區定義也會有所不同。比如預設的 HotSpot 在實現 JDK1.7 虛擬機器規範時,其常量池的定義不在方法區中,而是移到了堆中;到了 HotSpot JDK1.8 中,則徹底移除了持久代(方法區)而使用Metaspace(後設資料區)來進行替代等等,關於這些區別本篇部落格也會在文章末尾進行相應的說明。

1、執行時資料區結構圖

  ①、Java虛擬機器規範定義的執行時資料區

   

  ②、HotSpot JDK1.8定義的執行時資料區

  

  注意:HotSpot實現的執行時資料區和Java虛擬機器規範定義的還是有所不同的,

  ①、將Java虛擬機器棧和本地方法棧合二為一;

  ②、後設資料區取代了方法區,並且後設資料區不在Java虛擬機器中,而是在本地記憶體中。

  ③、執行時常量池由方法區中移到了堆中

2、程式計數器

  程式計數器(Program Conputer Register)這是一塊較小的記憶體空間,可以看做是當前執行緒所執行的位元組碼的行號指示器,在虛擬機器的概念模型裡,位元組碼直譯器的工作就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

  ①、執行緒私有

  Java虛擬機器支援多執行緒,是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任一確定的時刻,一個處理器只會執行一條執行緒中的指令,因此為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要一個獨立的程式計數器。因此執行緒啟動時,JVM 會為每個執行緒分配一個PC暫存器(Program Conter,也稱程式計數器)。

  ②、記錄當前位元組碼指令執行地址

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

  ③、不拋 OutOfMemoryError 異常

  程式計數器的空間大小不會隨著程式執行而改變,始終只是儲存一個 returnAdress 型別的資料或者一個與平臺相關的本地指標的值。所以該區域是Java執行時記憶體區域中唯一一個Java虛擬機器規範中沒有規定任何 OutOfMemoryError 情況的區域。

3、虛擬機器棧

  Java虛擬機器棧(Java Virtual Machine stack),這塊區域也是執行緒私有的,與執行緒同時建立,用於儲存棧幀。Java 每個方法執行的時候都會同時建立一個棧幀(Stack Frame)用於儲存區域性變數表、操作棧、動態連結、方法出口等資訊,每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

  

  ①、執行緒私有

  隨執行緒建立而建立,宣告週期和執行緒保持一致。

  ②、由棧幀組成

  執行緒每個方法被執行的時候都會建立一個棧幀,用於儲存區域性變數表、操作棧、動態連結、方法出口等資訊,每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

  ③、丟擲 StackOverflowError 和 OutOfMemoryError 異常

  如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲 StackOverflowError 異常;如果虛擬機器棧可以動態擴充套件,當擴充套件時無法申請到足夠的記憶體時會丟擲 OutOfMemoryError  異常。

4、本地方法棧

  本地方法棧(Native Method Stacks)作用和虛擬機器棧型別,虛擬機器棧執行的是Java方法,本地方法棧執行的是 Native 方法,本地方法棧也會丟擲丟擲 StackOverflowError 和 OutOfMemoryError 異常。

  注意:由於虛擬機器規範並沒有對本地方法棧中的方法使用語言、使用方式和資料結構強制規定,因此具體的虛擬機器可以自由實現它。上圖我們也給出在 HotSpot 虛擬機器中,本地方法棧和虛擬機器棧合為一體了。

5、Java堆

  Java堆是Java虛擬機器所管理記憶體最大、被所有執行緒共享的一塊區域,目的是用來存放物件,基本上所有的物件例項和陣列都在堆上分配(不是絕對)。Java堆也是垃圾回收器管理的主要區域。

  ①、執行緒共享

  堆存放的物件,某個執行緒修改了物件屬性,另外一個執行緒從堆中獲取的該物件是修改後的物件,為什麼堆要設計成執行緒共享呢?

  我們可以假設堆是執行緒私有的,很顯然一個系統建立的物件會有很多,而且有些物件會比較大,如果設計成執行緒私有的,那麼如果有很多執行緒同時工作,那麼都必須給他們分配相應的私有記憶體,我相信記憶體很快就撐爆了,很顯然將堆設計為執行緒共享是最好不過了,不過凡事都具有兩面性,執行緒共享的設計這也帶來了多執行緒併發資源衝突問題,關於這個問題由於不是本系列部落格的主旨,這裡就不做詳細介紹了。

  ②、存放物件

  基本上所有的物件例項和陣列都要在堆上進行分配,但是隨著 JIT 編譯器的發展和逃逸分析技術的成熟,棧上分配、標量替換等優化技術會導致物件不一定在堆上進行分配。

  ③、垃圾收集

  Java堆也被稱為“GC堆”,是垃圾回收器的主要操作記憶體區域。當前垃圾回收器都是使用的分代收集演算法,所以Java堆還可以分為:新生代和老年代,而新生代又可以分為 Eden 空間、From Survivor 空間、To Survivor空間。這是為了更好的回收記憶體,關於垃圾回收演算法在後續部落格會詳細介紹。

  

  ④、丟擲 OutOfMemoryError 異常

  根據Java虛擬機器規範,Java堆可以處於物理上不連續的記憶體空間中,只要邏輯上連續即可,實現時既可以實現成固定大小,也可以是擴充套件的。如果在堆中沒有完成例項分配,並且堆也無法擴充套件,將丟擲OutOfMemoryError 異常。

6、方法區

   方法區(Method Area)用來儲存已被Java虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。

  方法區也稱為“永久代”,這是因為垃圾回收器對方法區的垃圾回收比較少,主要是針對常量池的回收以及對型別的解除安裝,回收條件比較苛刻。經常會導致對此記憶體未完全回收而導致記憶體洩露,最後當方法區無法滿足記憶體分配時,將丟擲 OutOfMemoryError 異常。

  PS:在Java虛擬機器規範中把方法區描述為堆的一個邏輯部分(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4),在很多虛擬機器中(JRockit、IBM J9等虛擬機器不存在永久代的概念)。

     在JDK1.8 的 HotSpot 虛擬機器中,已經去掉了方法區的概念,用 Metaspace 代替,並且將其移到了本地記憶體來規劃了。

7、執行時常量池

   在Java虛擬機器規範中,執行時常量池(Runtime Constant Pool)用於存放編譯期生成的各種字面量和符號引用,是方法區的一部分。但是Java虛擬機器規範對其沒有做任何細節的要求,所以不同虛擬機器實現商可以按照自己的需求來實現該區域,比如在 HotSpot 虛擬機器實現中,就將執行時常量池移到了堆中。

  ①、存放字面量、符號引用、直接引用

  通常來說,該區域除了儲存Class檔案中描述的引用外,還會把翻譯出來的直接引用也儲存在執行時常量池,並且Java語言並不要求常量一定只能在編譯器產生,執行期間也可能將常量放入池中,比如String類的intern()方法,當呼叫intern方法時,如果池中已經包含一個與該String確定的字串相同equals(Object)的字串,則返回該字串。否則,將此String物件新增到池中,並返回此物件的引用。關於該方法的介紹可以看我這篇部落格

  ②、丟擲 OutOfMemoryError 異常

  執行時常量池是方法區的一部分,會受到方法區記憶體的限制,當常量池無法申請到記憶體時,會丟擲該異常。

8、直接記憶體

  直接記憶體(Direct Memory)並不是虛擬機器執行時資料區的一部分,它也不是Java虛擬機器規範定義的記憶體區域。我們可以看到在 HotSpot 中,就將方法區移除了,用後設資料區來代替,並且將後設資料區從虛擬機器執行時資料區移除了,轉到了本地記憶體中,也就是說這塊區域是受本機實體記憶體的限制,當申請的記憶體超過了本機實體記憶體,才會丟擲 OutOfMemoryError 異常。

  直接記憶體也是受本機實體記憶體的限制,在JDK1.4中新加入的 NIO(new input/output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可以使用 Native 函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆裡面的 DirectByteBuffer 物件作為這塊記憶體的引用操作,這樣避免了在Java堆和Native堆中來回複製資料,顯著提高效能。

 

相關文章