JVM虛擬機器-執行時資料區概述

阿dun發表於2021-05-07

執行時資料區域

總覽

JDK. 1.7 之後版本略有不同

Java 虛擬機器在執行 Java 程式的過程中會把它管理的記憶體劃分成若干個不同的資料區域。

有必要深入瞭解這塊的內容,因為它將決定伺服器效能,除此之外還有助於快速定位虛擬機器的相關Error。

首先來對整個執行時區域有一個整體的認識。

如下圖

JDK 1.7 之前:

image-20210507090340086

JDK 1.7 以及之後(1.8正式使用,1.7還需要手動設定一下) :

image-20210507091500982
  • 執行緒私有的(圖中紅色)

  • 執行緒共享的(圖中綠色、藍色)

概念掃盲

什麼是棧幀(Stack Frame)

每一次函式的呼叫,都會在呼叫棧上維護一個獨立的棧幀,每個獨立的棧幀一般包括:

  • 函式的返回地址和引數
  • 臨時變數
  • 函式呼叫的上下文

棧是從高地址向低地址延伸,一個函式的棧幀用ebpesp 這兩個暫存器來劃定範圍。

ebp 指向當前的棧幀的底部,esp 始終指向棧幀的頂部。

  • ebp 暫存器又被稱為幀指標(Frame Pointer)
  • esp 暫存器又被稱為棧指標(Stack Pointer)

JVM常見出現兩種錯誤

  • StackOverFlowError 若 Java 虛擬機器棧的記憶體大小不允許動態擴充套件,那麼當執行緒請求棧的深度超過當前 Java 虛擬機器棧的最大深度的時候,就丟擲 StackOverFlowError 錯誤。
  • OutOfMemoryError Java 虛擬機器棧的記憶體大小可以動態擴充套件, 如果虛擬機器在動態擴充套件棧時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常異常。

程式計數器

程式計數器佔用較小的一塊記憶體空間,每條執行緒都需要有一個獨立的程式計數器,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被來回切換的時候,能夠知道該執行緒上次執行到哪兒了。

位元組碼直譯器工作時通過改變這個計數器的值,來選取下一條需要執行的位元組碼指令,從而實現程式碼的流程控制,如:順序執行、選擇、迴圈、異常處理。

它的生命週期隨著執行緒的建立而建立,隨著執行緒的結束而死亡。

程式計數器是唯一一個不會出現 OutOfMemoryError 的記憶體區域。

虛擬機器棧

結構

虛擬機器棧也是執行緒私有,而且生命週期與執行緒相同。

每個Java方法在執行的時候都會建立一個棧幀,用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊

image-20210507144625064

區域性變數表

  • 存放編譯器可知的各種基本資料型別(boolean、byte等)
  • 物件引用(reference型別,它不等同於物件本身)
    • 可能是一個指向物件起始地址的引用指標
    • 也可能是指向另一個代表物件的控制程式碼
    • 其他次物件相關的位置
  • returnAddress型別,指向了一條位元組碼指令的地址

方法是如何呼叫的

每一次函式呼叫都會有一個對應的棧幀被壓入 Java 棧,每一個函式呼叫結束後,都會有一個棧幀被彈出。

Java 方法有兩種返回方式:

  1. return 語句。
  2. 丟擲異常。

不管哪種返回方式都會導致棧幀被彈出。

本地方法棧

主要為虛擬機器使用到的Native方法服務,作用其實類似虛擬機器棧,其結構也和虛擬機器棧一樣

二者的區別是虛擬機器棧為虛擬機器執行位元組碼服務

本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的區域性變數表、運算元棧、動態連結、出口資訊。

方法執行完畢後相應的棧幀也會出棧並釋放記憶體空間。

在 HotSpot 虛擬機器中和虛擬機器棧合二為一

Java 堆是所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立

此記憶體區域的目的是存放物件例項幾乎所有的物件例項以及陣列都在這裡分配記憶體。

說是幾乎是因為由於多項技術的進步與成熟,如:逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術,一些物件也可能在棧上分配記憶體。

Java 堆是JVM中最大的一塊記憶體區域,也是是垃圾回收(Garbage Collected)管理的主要區域,故又叫做GC堆

淺堆和深堆

淺堆和深堆是兩個非常重要的概念,理解他們之前需要先了解什麼是保留集。

保留集,即為單一物件所持有的物件的集合,如圖:

image-20210507154003916
  • 淺堆是指一個物件所消耗的記憶體。如上圖
  • 深堆是指物件的保留集中所有的物件淺堆大小之和

堆的細分

HotSpot中還有永久代的概念,不過已經是歷史了。

JDK 8 HotSpot 的永久代被徹底移除,取而代之是元空間,元空間使用的是直接記憶體。

現在垃圾收集器基本都採用分代垃圾收集演算法,所以 Java 堆還可以細分,堆分為新生代(佔堆1/3),老生代(佔堆2/3)

  • 新生代(內部比例8:1:1)
    • Eden 空間
    • From Survivor 空間
    • To Survivor 空間
  • 老年代

進一步劃分的目的是更好地回收記憶體,或者更快地分配記憶體。

流程:

  • 大多數情況,物件都會首先在 Eden 區域分配
  • 在一次新生代垃圾回收後,如果物件還存活,則會進入兩個Survivor中的一個,然後物件的年齡加 1
  • 它的年齡增加到年齡閾值(預設為 15 ),就會被晉升到老年代中

物件晉升到老年代的年齡閾值,可以通過引數 -XX:MaxTenuringThreshold 設定

方法區

方法區與 Java 堆一樣,也是所有執行緒共享的。

主要用於儲存類的資訊、常量池、方法資料、方法程式碼等。

方法區邏輯上屬於堆的一部分,但是為了與堆進行區分,有一個別名叫做 Non-Heap(非堆)

該區域的記憶體回收目標主要針對常量池的回收型別的解除安裝

在HotSpot虛擬機器中,用永久代來實現方法區,但是這樣容易遇到記憶體溢位的問題,所以在Java 8之後就取消了方法區。

方法區和永久代的關係

摘自《深入理解Java虛擬機器》第三版

《Java 虛擬機器規範》只是規定了有方法區這麼個概念和它的作用,並沒有規定如何去實現它。那麼,在不同的 JVM 上方法區的實現肯定是不同的了。 方法區和永久代的關係很像 Java 中介面和類的關係,類實現了介面,而永久代就是 HotSpot 虛擬機器對虛擬機器規範中方法區的一種實現方式。 也就是說,永久代是 HotSpot 的概念,方法區是 Java 虛擬機器規範中的定義,是一種規範,而永久代是一種實現,一個是標準一個是實現,其他的虛擬機器實現並沒有永久代這一說法。

為什麼要將永久代替換為元空間 ?

  • 永久代記憶體有一個JVM固定的上限,經常會出現OutOfMemoryError
  • 元空間使用的是直接記憶體,受本機可用記憶體的限制,雖然元空間仍舊可能溢位,但是比原來出現的機率會更小。
  • 元空間裡面存放的是類的後設資料,由系統的實際可用空間來控制,這樣能載入的類就變多了。
  • 在 JDK8,合併 HotSpot 和 JRockit 的程式碼時,JRockit 沒有永久代,如果強行保留實現起來困難重重。

當元空間溢位時會得到如下錯誤: java.lang.OutOfMemoryError: MetaSpace

執行時常量池

執行時常量池用於存放編譯期間生成的各種字面量符號引用,是方法區的一部分。

執行時常量池用來動態獲取類資訊,包括:

  • Class檔案元資訊描述
  • 編譯後的程式碼資料
  • 引用型別資料
  • 類檔案常量池

執行時常量池是在類載入完成之後,將每個Class常量池中的符號引用值轉存到執行時常量池中。

每個Class都有一個執行時常量池,類在解析之後將符號引用替換成直接引用,與全域性常量池中的引用值保持一致

執行時常量池相的另外一個重要特性是具備動態性,Java語言並不要求常量一定只有編譯器才能產生,也就是並非預置入Class檔案中的常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中。

直接記憶體

直接記憶體並不是虛擬機器執行時資料區的一部分,也不是虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用。

使用的方式是通過 JDK1.4 中加入的NIO(New Input/Output)類,它可以直接使用 Native 函式庫直接分配堆外記憶體

通過一個儲存在 Java 堆中的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。

避免了在 Java 堆Native 堆之間來回複製資料,在一些場景中顯著提高了效能,

本機直接記憶體的分配不受 Java 堆的限制,但受到本機總記憶體大小,以及處理器定址空間的限制,因此也可能導致 OutOfMemoryError 錯誤出現。

總結

以上的各個分割槽,各司其職,是瞭解Java虛擬機器的基礎。

理解各區域的指責和作用,對JVM後續的學習有非常大的幫助,如果這些沒搞懂,後面學起來是真頭大?‍?。

結合圖例,相信可以較為清晰了理解各分割槽的架構和指責,覺得有用歡迎點個推薦、點個贊。

參考:

《深入理解Java虛擬機器》第三版 ——周志明 (吹爆)

相關文章