面試官:JVM調優,主要針對是哪一個區域?JVM記憶體結構是怎樣的?

程序员世杰發表於2024-07-03

作為一個Java程式設計師,在日常的開發中,不必像C/C++程式設計師那樣,為每一個記憶體的分配而操心,JVM會替我們進行自動的記憶體分配和回收,方便我們開發。但是一旦發生記憶體洩漏或者記憶體溢位,如果對Java記憶體結構不清楚,那將會是一件非常麻煩的事情!本文筆者將為大家詳解Java記憶體結構。

面試tips

  1. 聊聊Java記憶體結構?都有哪些組成部分?哪些是執行緒共享?哪些是執行緒私有?
  2. 我們通常說的JVM調優,主要針對是哪一個區域?這個區域中那一塊是最大的?主要用於存放什麼內容?
  3. Java虛擬機器棧儲存的內容是什麼?
  4. 程式計數器的作用是什麼?當記憶體不足時,程式計數器會發生OOM嗎?
  5. 聊聊你對方法區的看法?在不同JDK版本中,方法區的演進過程是什麼?

你是否對這些問題都瞭如指掌?看完本文相信你心中就會有答案!

JVM架構

JVM的平臺無關性

jvm與作業系統

  1. 計算機的CPU、記憶體、顯示卡等等屬於硬體
  2. 常用的MacOs、Windows、Linux屬於計算機的作業系統
  3. 而Java的虛擬機器,也就是JVM是執行在作業系統之上的,與硬體沒有直接聯絡,JVM也是Java能夠跨平臺的根本原因。

JVM架構

image-20240630174946980

1. Class Loader 類載入器

類載入器的作用是載入類檔案到記憶體,比如編寫一個 HelloWord.java 檔案,然後透過 javac 編譯成 class 檔案,那怎麼才能載入到記憶體中被執行呢?答案就是 Class Loader。當然,不是任何 .class 檔案就能被載入的,Class Loader 載入的 class 檔案是有格式要求

2. Execution Engine 執行引擎

Class Loader 只負責載入,只要符合檔案結構就載入,至於說能不能執行,則不是它負責的,那是由 Execution Engine 負責的。執行引擎也叫做直譯器 (Interpreter),負責解釋命令,提交作業系統執行

3. Native Interface 本地介面

本地介面的作用是融合不同的程式語言為 Java 所用,它的初衷是融合 C/C++ 程式,Java 誕生的時候是 C/C++ 橫行的時候,於是就在記憶體中專門開闢了一塊區域處理標記為 native 的程式碼

4. Runtime data area 執行時資料區

執行時資料區是整個 JVM 的重點。我們所有寫的程式都被載入到這裡,之後才開始執行,下面會重點講解執行時資料區。

JVM執行流程

當然不同的VM的具體實現細節也不是不一樣的,現在使用的比較多的JDK8版本就是Sun HotSpot VM與BEA JRockit VM合併之後開發出的JDK版本。

下面就是一個Java檔案載入並執行的流程

JVM架構

執行時資料區

執行時資料區是JVM中最為重要的部分。也是我們在調優時需要重點關注的區域。

執行時資料區分為:程式計數器Java虛擬機器棧本地方法棧Java堆區方法區

其中

  • 執行緒私有:程式計數器、虛擬機器棧、本地方法棧
  • 執行緒共享:堆、方法區, 堆外記憶體(Java7的永久代或JDK8的元空間、直接記憶體)

JDK 1.8 和之前的版本略有不同,我們這裡以 JDK 1.7 和 JDK 1.8 這兩個版本為例介紹。

JDK 1.7

java-runtime-data-areas-jdk1.7

JDK 1.8

java-runtime-data-areas-jdk1.8

程式計數器

程式計數暫存器(Program Counter Register),Register 的命名源於 CPU 的暫存器,暫存器儲存指令相關的執行緒資訊,CPU 只有把資料裝載到暫存器才能夠執行。它是一塊很小的記憶體空間,幾乎可以忽略不計。也是執行速度最快的儲存區域

  1. JVM 中的 PC 暫存器是對物理 PC 暫存器的一種抽象模擬。可以看作是當前執行緒所執行的位元組碼的行號指示器。直譯器工作時透過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、執行緒恢復等功能都需要依賴這個計數器來完成。

  2. 由於Java虛擬機器的多執行緒是透過執行緒輪流切換、分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)都只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器

  3. 任何時間一個執行緒都只有一個方法在執行,也就是所謂的當前方法。如果當前執行緒正在執行的是 Java 方法,程式計數器記錄的是 JVM 位元組碼指令地址,如果是執行 native 方法,則是未指定值(undefined)

  4. 程式計數器是唯一一個不會出現 OutOfMemoryError 的記憶體區域,它的生命週期與執行緒保持一致。

jvm-pc-counter

Java虛擬機器棧

每個執行緒在建立的時候都會建立一個虛擬機器棧,其內部儲存一個個的棧幀(Stack Frame),對應著一次次 Java 方法呼叫,是執行緒私有的,生命週期和執行緒一致。

1、棧的內部結構

每個棧幀(Stack Frame)中儲存著:

  • 區域性變數表(Local Variables):主要存放了編譯期可知的各種資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(reference 型別,它不同於物件本身,可能是一個指向物件起始地址的引用指標,也可能是指向一個代表物件的控制代碼或其他與此物件相關的位置)

  • 運算元棧(Operand Stack):主要用於存放方法執行過程中產生的中間計算結果。另外,計算過程中產生的臨時變數也會放在運算元棧中。如果被呼叫的方法帶有返回值的話,其返回值將會被壓入當前棧幀的運算元棧中

  • 動態連結(Dynamic Linking):指向執行時常量池的方法引用。Class 檔案的常量池裡儲存有大量的符號引用比如方法引用的符號引用,當一個方法要呼叫其他方法,需要將常量池中指向方法的符號引用轉化為其在記憶體地址中的直接引用。這個過程也被稱為 動態連線

  • 方法返回地址(Return Address):方法正常退出或異常退出的地址

PS: 區域性變數表中的變數也是重要的垃圾回收根節點,只要被區域性變數表中直接或間接引用的物件都不會被回收

img

2、棧的執行流程

  • JVM 直接對虛擬機器棧的操作只有兩個:方法呼叫入棧,方法執行結束出棧
  • 線上程中,同一時間只會有一個活動的棧幀,即(棧頂棧幀)是有效的,這個棧幀被稱為當前棧幀(Current Frame),與當前棧幀對應的方法就是當前方法(Current Method),定義這個方法的類就是當前類(Current Class)
  • 如果在該方法中呼叫了其他方法,對應的新的棧幀會被建立出來,放在棧的頂端,稱為新的當前棧幀
  • 不同執行緒中所包含的棧幀是不允許相互引用的,即不可能在一個棧幀中引用另外一個執行緒的棧幀

jvm-stack-frame

3、棧的異常

  • StackOverFlowError 若棧的記憶體大小不允許動態擴充套件,那麼當執行緒請求棧的深度超過當前 Java 虛擬機器棧的最大深度的時候,就丟擲 StackOverFlowError 錯誤。

  • OutOfMemoryError 如果棧的記憶體大小可以動態擴充套件, 如果虛擬機器在動態擴充套件棧時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常。

本地方法棧

本地方法棧和Java虛擬機器棧所發揮的作用非常相似

  • 二者區別在於: 虛擬機器棧為虛擬機器執行 Java 方法 (也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的 Native 方法服務

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

  • 方法執行完畢後相應的棧幀也會出棧並釋放記憶體空間,也會出現 StackOverFlowErrorOutOfMemoryError 兩種異常。

  • Hotspot JVM 中,直接將本地方法棧和虛擬機器棧合二為一

Java堆區

棧是執行時的單位,而堆是儲存的單位

Java 堆是 Java 虛擬機器管理的記憶體中最大的一塊,被所有執行緒共享

PS:關於Java堆有很多細節可以深挖,例如堆的分代和物件的建立和回收等,後續我還會專門開一篇文章展開講

1、堆的儲存內容

此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項以及資料都在這裡分配記憶體。成員變數名和值儲存於堆中,其生命週期和物件的是一致的。

Java 世界中“幾乎”所有的物件都在堆中分配,但是,隨著 JIT 編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換最佳化技術將會導致一些微妙的變化,所有的物件都分配到堆上也漸漸變得不那麼“絕對”了。從 JDK 1.7 開始已經預設開啟逃逸分析,如果某些方法中的物件引用沒有被返回或者未被外面使用(也就是未逃逸出去),那麼物件可以直接在棧上分配記憶體。

2、堆的分割槽和垃圾回收

為了進行高效的垃圾回收,虛擬機器把堆記憶體邏輯上劃分成三塊區域(分代的唯一理由就是最佳化 GC 效能):

  • 新生帶(年輕代):新物件和沒達到一定年齡的物件都在新生代
  • 老年代(養老區):被長時間使用的物件,老年代的記憶體空間應該要比年輕代更大
  • 元空間(JDK1.8 之前叫永久代):一些方法中的操作臨時物件等,JDK1.8 之前是佔用 JVM 記憶體,JDK1.8 之後直接使用實體記憶體

img

3、堆出現的異常

堆這裡最容易出現的就是 OutOfMemoryError 錯誤,比如:

  • java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:當 JVM 花太多時間執行垃圾回收並且只能回收很少的堆空間時,就會發生此錯誤。

  • java.lang.OutOfMemoryError: Java heap space :假如在建立新的物件時, 堆記憶體中的空間不足以存放新建立的物件, 就會引發此錯誤。(和配置的最大堆記憶體有關,且受制於實體記憶體大小。最大堆記憶體可透過-Xmx引數配置,若沒有特別配置,將會使用預設值,詳見:Default Java 8 max heap sizeopen in new window)

方法區

方法區屬於是 JVM 執行時資料區域的一塊邏輯區域,是各個執行緒共享的記憶體區域

1、方法區和永久代以及元空間是什麼關係呢?

方法區和永久代以及元空間的關係很像 Java 中介面和類的關係,類實現了介面,這裡的類就可以看作是永久代和元空間,介面可以看作是方法區,也就是說永久代以及元空間是 HotSpot 虛擬機器對虛擬機器規範中方法區的兩種實現方式。並且,永久代是 JDK 1.8 之前的方法區實現,JDK 1.8 及以後方法區的實現變成了元空間。

method-area-implementation

2、方法區的儲存內容

當虛擬機器要使用一個類時,它需要讀取並解析 Class 檔案獲取相關資訊,再將資訊存入到方法區。方法區會儲存已被虛擬機器載入的 類資訊、欄位資訊、方法資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等資料。在載入類和結構到虛擬機器後,就會建立對應的執行時常量池

執行時常量池(Runtime Constant Pool)是虛擬機器規範中是方法區的一部分,在載入類和結構到虛擬機器後,就會建立對應的執行時常量池;而字串常量池是這個過程中常量字串的存放位置。所以從這個角度,字串常量池屬於虛擬機器規範中的方法區,它是一個邏輯上的概念;而堆區,永久代以及元空間是實際的存放位置。

3、方法區在 JDK6、7、8中的演進細節

JDK版本 是否有永久代,字串常量池放在哪裡? 方法區邏輯上規範,由哪些實際的部分實現的?
jdk1.6及之前 有永久代,執行時常量池(包括字串常量池),靜態變數存放在永久代上 這個時期方法區在HotSpot中是由永久代來實現的,以至於這個時期說方法區就是指永久代
jdk1.7 有永久代,但已經逐步“去永久代”,字串常量池、靜態變數移除,儲存在堆中; 這個時期方法區在HotSpot中由永久代(型別資訊、欄位、方法、常量)和(字串常量池、靜態變數)共同實現
jdk1.8及之後 取消永久代,型別資訊、欄位、方法、常量儲存在本地記憶體的元空間,但字串常量池、靜態變數仍在堆中 這個時期方法區在HotSpot中由本地記憶體的元空間(型別資訊、欄位、方法、常量)和(字串常量池、靜態變數)共同實現

method-area-jdk1.6

method-area-jdk1.7

參考文章

  1. 【002】十分鐘搞懂Java記憶體結構

  2. JVM的記憶體分割槽/記憶體結構/記憶體區域/JVM記憶體模型

  3. JVM 基礎 - JVM 記憶體結構

  4. Java記憶體區域詳解(重點)

  5. 淺談JVM整體架構與調優引數

相關文章