JVM記憶體模型和結構詳解(五大模型圖解)

2022-08-08 18:00:44

JVM記憶體模型和Java記憶體模型都是面試的熱點問題,名字看感覺都差不多,實際上他們之間差別還是挺大的。

通俗點說,JVM記憶體結構是與JVM的內部儲存結構相關,而Java記憶體模型是與多執行緒程式設計相關@mikechen

什麼是JVM

JVM是Java Virtual Machine(Java虛擬機器器)的縮寫,JVM是一個虛構出來的計算機,有著自己完善的硬體架構,如處理器、堆疊等。

 

為什麼需要JVM?

Java語言使用Java虛擬機器器遮蔽了與具體平臺相關的資訊,使得Java語言編譯程式只需生成在Java虛擬機器器上執行的目的碼(位元組碼),就可以在多種平臺上不加修改地執行。

Java檔案必須先通過一個叫javac的編譯器,將程式碼編譯成class檔案,然後通過JVM把class檔案解釋成各個平臺可以識別的機器碼,最終實現跨平臺執行程式碼。

 

JVM記憶體模型

JVM記憶體模型可以分為兩個部分,如下圖所示,堆和方法區是所有執行緒共有的,而虛擬機器器棧,本地方法棧和程式計數器則是執行緒私有的。

323009a709984f5e8979faab9fd5fa58?from=pc

在JVM1.8中,圖中的 方法區為後設資料區,下面展開談一談這五個區域的作用。

 

堆(Heap)

在 Java 中,堆被劃分成兩個不同的區域:新生代 ( Young )、老年代 ( Old ),新生代 ( Young ) 又被劃分為三個區域:Eden、From Survivor、To Survivor。

下圖中的Perm代表的是永久代,但是注意永久代並不屬於堆記憶體中的一部分,同時jdk1.8之後永久代也將被移除。

41a3037a558f4cdeb4fb3dc2149058f5?from=pc

堆是java虛擬機器器所管理的記憶體中最大的一塊記憶體區域,也是被各個執行緒共用的記憶體區域,該記憶體區域存放了物件範例及陣列(但不是所有的物件範例都在堆中)。

其大小通過-Xms(最小值)和-Xmx(最大值)引數設定(最大最小值都要小於1G),前者為啟動時申請的最小記憶體,預設為作業系統實體記憶體的1/64,後者為JVM可申請的最大記憶體,預設為實體記憶體的1/4,預設當空餘堆記憶體小於40%時,JVM會增大堆記憶體到-Xmx指定的大小,可通過-XX:MinHeapFreeRation=來指定這個比列。

當空餘堆記憶體大於70%時,JVM會減小堆記憶體的大小到-Xms指定的大小,可通過XX:MaxHeapFreeRation=來指定這個比列,當然為了避免在執行時頻繁調整Heap的大小,通常-Xms與-Xmx的值設成一樣。堆記憶體 = 新生代+老生代+持久代。

在我們垃圾回收的時候,我們往往將堆記憶體分成新生代和老生代(大小比例1:2),新生代中由Eden和Survivor0,Survivor1組成,三者的比例是8:1:1,新生代的回收機制採用複製演演算法,在Minor GC的時候,我們都留一個存活區用來存放存活的物件,真正進行的區域是Eden+其中一個存活區,當我們的物件時長超過一定年齡時(預設15,可以通過引數設定),將會把物件放入老生代,當然大的物件會直接進入老生代,老生代採用的回收演演算法是標記整理演演算法。

 

方法區(Method Area)

其實方法區是在JDK1.8以前的版本里存在的一塊記憶體區域,主要就是存放從class檔案裡載入進來的類的,而且常數池也是在這塊區域內的。

但是在JDK1.8之後,這塊區域搖身一變,換了名字,叫做「Metaspace」,翻譯過來就是「後設資料空間」的意思,當然它只是改了個名,實現的功能是沒變的。

方法區(Method Area)與Java堆一樣,是各個執行緒共用的記憶體區域,它用於儲存已被虛擬機器器載入的型別資訊、常數、靜態變數、即時編譯器編譯後的程式碼快取等資料。

1.型別資訊

對每個載入的型別(類class、介面interface、列舉enum、註解annotation),JVM必須在方法區中儲存以下型別資訊:
①這個型別的完整有效名稱(全名=包名.類名)
②這個型別直接父類別的完整有效名(對於interface或是java.lang.0bject,都沒有父類別)
③這個型別的修飾符(public, abstract,final的某個子集)
④這個型別直接介面的一個有序列表

2.域資訊(Field)成員變數

JVM必須在方法區中儲存型別的所有域的相關資訊以及域的宣告順序。
域的相關資訊包括:域名稱、域型別、域修飾符(public, private,protected,static,final, volatile, transient的某個子集)

3.方法(Method)資訊

JVM必須儲存所有方法的以下資訊,同域資訊一樣包括宣告順序:

  • 方法名稱
  • 方法的返回型別(或void)·方法引數的數量和型別(按順序)
  • 方法的修飾符(public, private,protected,static, final,synchronized,native,abstract的一個子集)
  • 方法的位元組碼(bytecodes)、運算元棧、區域性變數表及大小(abstract和native方法除外)

 

虛擬機器器棧(JVM Stack)

虛擬機器器棧(Java Virtual Machine Stack),早期也叫Java棧,每個執行緒在建立時都會建立一個虛擬機器器棧,其內部儲存一個個的棧幀(Stack Frame),對應著一次次的Java方法呼叫。

虛擬機器器棧的作用:主管Java程式的執行,它儲存方法的區域性變數、部分結果,並參與方法的呼叫和返回。

每個方法被執行的時候都會建立一個」棧幀」,用於儲存區域性變數表(包括引數)、操作棧、方法出口等資訊。

每個方法被呼叫到執行完的過程,就對應著一個棧幀在虛擬機器器棧中從入棧到出棧的過程。

棧幀(Stack Frame) 是用於虛擬機器器執行時方法呼叫和方法執行時的資料結構,它是虛擬棧的基本元素,棧幀由區域性變數區、運算元棧等組成,如下圖所示:

 

每一個方法從呼叫到方法返回都對應著一個棧幀入棧出棧的過程。最頂部的棧幀稱為當前棧幀,棧幀所關聯的方法稱為當前方法,定義這個方法的類稱為當前類,該執行緒中,虛擬機器器有且也只會對當前棧幀進行操作。

棧幀的作用有儲存資料,部分過程結果,處理動態連結,方法返回值和異常分派。

每一個棧幀包含的內容有區域性變數表、運算元棧、動態連結、方法返回地址和一些額外的附加資訊。在編譯程式碼時,棧幀需要多大的區域性變數表,多深的運算元棧都可以完全確定的,並寫入到方法表的code屬性中。

 

本地方法棧(Native Stack)

本地方法棧(Native Method Stacks)與虛擬機器器棧所發揮的作用是非常相似的,其區別不過是虛擬機器器棧為虛擬機器器執行Java 方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器器使用到的Native 方法服務。

虛擬機器器規範中對本地方法棧中的方法使用的語言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器器可以自由實現它。

甚至有的虛擬機器器(譬如Sun HotSpot 虛擬機器器)直接就把本地方法棧和虛擬機器器棧合二為一。

與虛擬機器器棧一樣,本地方法棧區域也會丟擲StackOverflowError 和OutOfMemoryError異常。

 

程式計數器(PC Register)

在JVM的概念模型裡,位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。

分支、迴圈、跳轉、例外處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

JVM的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,為了各條執行緒之間的切換後計數器能恢復到正確的執行位置,所以每條執行緒都會有一個獨立的程式計數器。

當執行緒正在執行一個Java方法,程式計數器記錄的是正在執行的JVM位元組碼指令的地址;如果正在執行的是一個Natvie(本地方法),那麼這個計數器的值則為空(Underfined)。

程式計數器佔用的記憶體空間很少,也是唯一一個在JVM規範中沒有規定任何OutOfMemoryError(記憶體不足錯誤)的區域。

 

 

JVM記憶體模型小結

本篇介紹了JVM虛擬機器器中執行時資料區的五個記憶體區域:堆、方法區、虛擬機器器棧、本地方法棧、程式計數器。

這些地方也是我們平時開發中最常接觸到的地方,所以對其有所掌握瞭解還是很有必要的,也有助於JVM問題排查。

 

作者簡介

mikechen,10年+大廠架構經驗,《BAT架構技術500期》系列文章作者,曾就職於阿里、淘寶、百度等一線網際網路大廠。
 
閱讀mikechen的網際網路架構更多技術文章合集
Java並行|JVM|MySQL|Spring|Redis|分散式|高並行|架構師