關注微信公眾號:CodingTechWork,一起學習進步。
引言
我們經常會被問到一個問題是Java和C++有何區別?我們除了能回答一個是物件導向、一個是程式導向程式設計以外,我們還會從底層記憶體管理和垃圾收集方面作出比較。
對於C++而言,程式設計師既要做程式設計開發又要維護底層記憶體管理;而對於Java而言,程式設計師不需要控制底層,只需要安心寫自己的程式碼即可,因為Java虛擬機器自動實現了記憶體管理以及垃圾回收。
但是,我們寫的程式或者程式環境問題等也時長出現記憶體洩露和溢位,這個時候程式設計師如果不知道虛擬機器如何分配和管理記憶體,排查問題將舉步維艱。下面我們就來一起看看,Java虛擬機器記憶體是如何劃分的。
執行時資料區
概念
Java虛擬機器在執行Java程式時,會將記憶體區域劃分為不同的資料區域(執行時資料區)。有些資料區域是跟隨VM程式
的啟動而存在,有的區域是跟隨使用者執行緒
的啟動和結束而建立和銷燬。
分類
一般執行時資料區分為:程式計數器、虛擬機器棧、本地方法棧、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是將兩者合二為一,都會丟擲StackOverFlowError
和OutOfMemoryError
異常。
Java堆
定義
Java堆(Java Heap)是JVM中最大的一塊記憶體
,是存放物件例項的區域。所有物件例項以及陣列都要在堆上分配
。當然,也有特特殊的情況,JIT編譯器的發展與逃逸分析技術會促進棧上分配及變數替換優化
技術的發展。
結構
執行緒共享
Java堆是被所有執行緒共享的一塊記憶體區域,在VM啟動時建立。執行緒共享可以將Java堆劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。
GC堆
Garbage Collected Heap,Java堆是垃圾收集器管理
的主要區域,簡稱GC堆。Java堆細分為新生代和老年代
,再細分為Eden
空間、From Survivor
空間、To Survivor
空間。
堆空間
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)
- 該區域不屬於虛擬機器執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體預期,但會
頻繁使用且導致OutOfMemoryError
。 - JDK1.4中新加入的
NIO(New Input/Output)
類,引入一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存在Java堆中的DirectByteBuffer物件
作為這塊記憶體的引用進行操作(可避免在Java堆和Native堆中來回複製資料
) - 不會受到Java堆大小的限制,但受本機總記憶體大小以及處理器定址空間的限制。可通過
-Xmx
等引數來設定實際記憶體大小,在我們實際操作過程中,經常忽視直接記憶體,使得整個記憶體區域總和大於實體記憶體限制,導致OutOfMemoryError異常。
總結
執行時資料區 | 執行緒是否私有 | 作用 | 異常 |
---|---|---|---|
程式計數器 | 執行緒私有 | 每個執行緒都有自己的程式計數器,是當前執行緒所執行的位元組碼的行號指示器。 | 無 |
虛擬機器棧 | 執行緒私有 | 是Java方法執行的記憶體模型,每個方法在執行的同時都會建立一個棧幀(Stack Frame)使用者儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。每個方法從呼叫直至執行完成的過程,就是對應著一個棧幀在虛擬機器棧中入棧和出棧的過程。 | 若執行緒請求的棧深度大於虛擬機器所允許的深度,丟擲StackOverflowError異常;若虛擬機器可以動態擴充套件,而擴充套件時無法申請到足夠的記憶體,就丟擲OutOfMemoryError異常 |
本地方法棧 | 執行緒私有 | 為虛擬機器使用本地(Native)方法服務 | 同虛擬機器棧 |
Java堆 | 執行緒共享 | 在虛擬機器啟動時建立,該區域唯一目的是存放物件例項,幾乎所有例項都是在這裡分配記憶體;gc的主要區域 | 若堆中沒有記憶體完成例項分配且堆無法再擴充套件時,將會丟擲OutOfMemoryError異常。 |
方法區 | 執行緒共享 | 儲存已被VM載入的類資訊、常量、靜態變數、即時編譯器後的程式碼等資料 | 當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常 |
至此,JVM執行時資料區全部總結完畢。
參考
《深入理解Java虛擬機器》