JVM——記憶體區域:運行時數據區域詳解

關注微信公眾號:CodingTechWork,一起學習進步。
在這裡插入圖片描述

引言

  我們經常會被問到一個問題是Java和C++有何區別?我們除了能回答一個是面向對象、一個是面向過程編程以外,我們還會從底層記憶體管理和垃圾收集方面作出比較。
  對於C++而言,程式設計師既要做程式設計開發又要維護底層記憶體管理;而對於Java而言,程式設計師不需要控制底層,只需要安心寫自己的程式碼即可,因為Java虛擬機自動實現了記憶體管理以及垃圾回收。
  但是,我們寫的程式或者程式環境問題等也時長出現記憶體泄露和溢出,這個時候程式設計師如果不知道虛擬機如何分配和管理記憶體,排查問題將舉步維艱。下面我們就來一起看看,Java虛擬機記憶體是如何劃分的。

運行時數據區

概念

  Java虛擬機在執行Java程式時,會將記憶體區域劃分為不同的數據區域(運行時數據區)。有些數據區域是跟隨VM進程的啟動而存在,有的區域是跟隨用戶執行緒的啟動和結束而建立和銷毀。

分類

  一般運行時數據區分為:程式計數器、虛擬機棧、本地方法棧、Java堆、方法區、運行時常量池。

結構

Java虛擬機運行時數據區

記憶體劃分

運行時數據區域大小
參數說明

程式計數器

定義

  程式計數器(Program Counter Register)是一塊較小的記憶體空間,是當前執行緒所執行的位元組碼的行號指示器。位元組碼解釋器工作時就是通過改變這個計數器的值來選擇下一條需要執行的位元組碼指令(分支、循環、跳轉、異常處理、執行緒恢復等功能)

執行緒私有

  多執行緒是通過執行緒輪換交替並分配處理器執行時間的方式實現,任何時刻,一個處理器都只會執行一條執行緒的指令,每條執行緒需要一個獨立的程式計數器,這樣各條執行緒之間的計數器互不影響,獨立存儲,從而保證執行緒切換後可以恢復到正確的執行位置。
  反例:執行緒A和執行緒B交替執行,執行緒A計數到5,切換執行,執行緒B計數到9,如果兩個執行緒共享程式計數器,則執行緒A此時計數共享執行緒B的計數值9,則無法恢復到正常的執行位置。
  正例:執行緒A和執行緒B交替執行,執行緒A計數到5,切換執行,執行緒B計數到9,執行緒A再切換執行時,繼續計數到5開始遞增,恢復到正確的執行位置。

異常

  此記憶體區域無OutOfMemoryError異常。

用途

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

Java虛擬機棧

定義

  虛擬機棧(Java Virtual Machine Stacks)是Java方法執行的記憶體模型,每個方法在執行的同時都會創建一個棧幀,用於存儲局部變數表、操作數棧、動態鏈接、方法出口等資訊。每個方法的從調用一直到執行完成,都對應著一個棧幀在VM棧中入棧到出棧的過程。

結構

虛擬機棧結構

執行緒私有

  Java虛擬機棧是執行緒私有的,其生命周期和執行緒同步,隨著執行緒的啟動而創建,隨執行緒的結束而銷毀。

局部變數表

  Loca Variable Table,虛擬機棧中的局部變數表通常就是所說的虛擬機棧,是一組變數值存儲空間,用於存放方法參數和方法內部定義的局部變數。其中存放了編譯期間可知的基本數據類型、對象引用類型和方法返回地址類型。
  局部變數表所佔用的記憶體空間是在編譯期間就已經分配好了,進入一個方法時,這個方法需要在幀中分配多大的局部變數空間是確定的,在方法運行期間也不會去改變這個局部變數表的大小。
  基本數據類型:8種基本數據類型是boolean、byte 8位、char、short 16位、int 32位、float 32位、long 64位、double 64位。其中64位長度的long和double類型數據會佔用2個局部變數空間(slot),其餘的都是佔用1個slot。
  對象引用類型:reference類型,與對象引用不等價,一般是指向對象起始地址的引用指針,或者是指向一個代表對象的句柄、其他與此對象相關的的位置。
  方法返回地址類型:returnAddress類型指向一條位元組碼指令的地址。

操作數棧

  Operand Stack,操作數棧也稱為操作棧,是一個後入先出棧,其中村的每個元素可以是任意的Java數據類型。當一個方法剛開始執行時,該方法的操作數棧是空的,當方法執行過程中,會有各種位元組碼指令往操作數棧中寫入和提取內容(入棧/出棧操作)

動態鏈接

  Dynamic Linking,每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的應用,持有這個引用就是為了支援方法調用過程中的動態鏈接,常量池中的符號引用在每一次運行期間轉化為直接引用即為動態鏈接。

方法返回地址

  returnAddress,當一個方法開始執行後,會有兩種方式退出該方法:正常完成出口異常完成出口
  正常完成出口:程式執行過程中遇到任意一個方法返回的位元組碼指令,返回值傳遞給上一層調用者,正常退出程式方法。
  異常完成出口:程式方法執行過程中遇到異常,這個異常沒有在方法體內得到處理(沒有try…catch,沒有throw異常),導致方法退出。

異常

  StackOverflowError異常:執行緒請求的棧深度大於虛擬機所允許的深度,拋出該異常。
  OutOfMemoryError異常:虛擬機可以動態擴展時,不能夠擴展申請到足夠的記憶體,拋出該異常。

本地方法棧

定義

  本地方法棧(Native Method Stack)是為虛擬機使用的Native方法服務

異常

  StackOverflowError異常:執行緒請求的棧深度大於虛擬機所允許的深度,拋出該異常。
  OutOfMemoryError異常:虛擬機可以動態擴展時,不能夠擴展申請到足夠的記憶體,拋出該異常。

與虛擬機棧異同


  虛擬機棧是為VM執行Java方法(位元組碼)服務;本地方法棧是為VM執行的Native方法。
:
  與虛擬機作用類似,有的VM是將兩者合二為一,都會拋出StackOverFlowErrorOutOfMemoryError異常。

Java堆

定義

  Java堆(Java Heap)是JVM中最大的一塊記憶體,是存放對象實例的區域。所有對象實例以及數組都要在堆上分配。當然,也有特特殊的情況,JIT編譯器的發展與逃逸分析技術會促進棧上分配及變數替換優化技術的發展。

結構

Java堆

執行緒共享

  Java堆是被所有執行緒共享的一塊記憶體區域,在VM啟動時創建。執行緒共享可以將Java堆劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。

GC堆

  Garbage Collected Heap,Java堆是垃圾收集器管理的主要區域,簡稱GC堆。Java堆細分為新生代和老年代,再細分為Eden空間、From Survivor空間、To Survivor空間。

Java堆劃分空間

堆空間

  Java堆可以處於物理上不連續的記憶體空間,只需要邏輯連續即可,有點類似於磁碟空間。實現時,既可以是固定大小,也可以擴展(-Xmx和-Xms參數進行控制)。

異常

  若堆中沒有記憶體完成實例分配,堆也無法擴展時,會拋出OutOfMemoryError異常。

方法區

定義

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

執行緒共享

  和Java堆一樣,也是執行緒共享區域。

持久代

  對於HotSpot虛擬機而言,方法區可以稱作為「永久代」或者「持久代」,這是因為HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至了方法區,使用永久代來實現方法區,垃圾收集器可以像管理Java堆一樣管理這部分記憶體區域。
持久代區域

限制

  方法區和Java堆一樣不需要連續的物理記憶體空間,可以選擇固定大小也可以選擇擴展,同時,可選擇不實現垃圾收集,方法區永久代中的數據不代表永久存在,該記憶體區域的記憶體回收目標是針對常量池的回收和對類型的卸載

運行時常量池

  運行時常量池(Runtime Constant Pool)是方法區的一部分,相比較而言,Class文件中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊就是常量池(Constant Pool Table)Class文件常量池用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的運行時常量池中存放。
  JVM規範對運行時常量池限制比較寬鬆,不同的廠商可以根據自己的需求自行實現該記憶體區域,一般而言,除了保存Class文件中描述的符號引用外,還會將轉化出的直接引用存儲在其中。
  與Class文件常量池相比較而言,運行時常量池還具備動態性,常量不一定在編譯器產生,即並非預置入Class文件常量池的內容才能進入方法區運行時常量池,運行期間也可以將新的常量放入池子中,如String類的intern()方法。

異常

  無法申請到記憶體時,會拋出OutOfMemoryError異常。

補充

直接記憶體(Direct Memory)

  1. 該區域不屬於虛擬機運行時數據區的一部分,也不是java虛擬機規範中定義的記憶體預期,但會頻繁使用且導致OutOfMemoryError
  2. JDK1.4中新加入的NIO(New Input/Output)類,引入一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,可以使用Native函數庫直接分配堆外記憶體,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作(可避免在Java堆和Native堆中來回複製數據
  3. 不會受到Java堆大小的限制,但受本機總記憶體大小以及處理器定址空間的限制。可通過-Xmx等參數來設置實際記憶體大小,在我們實際操作過程中,經常忽視直接記憶體,使得整個記憶體區域總和大於物理記憶體限制,導致OutOfMemoryError異常。

總結

運行時數據區 執行緒是否私有 作用 異常
程式計數器 執行緒私有 每個執行緒都有自己的程式計數器,是當前執行緒所執行的位元組碼的行號指示器。
虛擬機棧 執行緒私有 是Java方法執行的記憶體模型,每個方法在執行的同時都會創建一個棧幀(Stack Frame)用戶存儲局部變數表、操作數棧、動態鏈接、方法出口等資訊。每個方法從調用直至執行完成的過程,就是對應著一個棧幀在虛擬機棧中入棧和出棧的過程。 若執行緒請求的棧深度大於虛擬機所允許的深度,拋出StackOverflowError異常;若虛擬機可以動態擴展,而擴展時無法申請到足夠的記憶體,就拋出OutOfMemoryError異常
本地方法棧 執行緒私有 為虛擬機使用本地(Native)方法服務 同虛擬機棧
Java堆 執行緒共享 在虛擬機啟動時創建,該區域唯一目的是存放對象實例,幾乎所有實例都是在這裡分配記憶體;gc的主要區域 若堆中沒有記憶體完成實例分配且堆無法再擴展時,將會拋出OutOfMemoryError異常。
方法區 執行緒共享 存儲已被VM載入的類資訊、常量、靜態變數、即時編譯器後的程式碼等數據 當方法區無法滿足記憶體分配需求時,將拋出OutOfMemoryError異常

至此,JVM運行時數據區全部總結完畢。

參考
《深入理解Java虛擬機》