JVM虛擬機-運行時數據區概述

運行時數據區域

總覽

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虛擬機》第三版 ——周志明 (吹爆)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章