我們在開發 Java 程式的程序基本不用關心 Java 運行時的記憶體管理,是因為 Java 程式在運行時記憶體都由虛擬機來進行管理,Java 虛擬機在執行 Java 程式的程序中會把它所管理的記憶體劃分為若干個不同的資料區域,我們稱之為運行時資料區域,
運行時資料區域
根據《Java虛擬機規范(Java SE 7版)》的規定,Java虛擬機所管理的記憶體將會包括以下幾個運行時資料區域,

我們可以通過不同的幾個維度對上圖稍作一下分析:
- JVM 記憶體結構由虛擬機堆疊、本地方法堆疊、方法區、堆、程式計數器組成,
- 方法區和堆為執行緒共享記憶體,虛擬機堆疊,本地方法堆疊和程式計數器是執行緒獨享的記憶體,
- JVM 不同記憶體區域所占大小不同,其中堆記憶體最大,程式計數器占記憶體最小,
程式計數器(Program Counter Register)
程式計數器是一塊較小的記憶體空間是,它可以看做是當前執行緒所執行的位元組碼的行號指示器,是執行緒私有的,每條執行緒都會有一個獨立的程式計數器,是為了在多執行緒情況下,執行緒切換后能夠恢復到正確的執行位置,此記憶體區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域,
程式計數器在執行緒執行不同方法時儲存的內容會有所不同:如果執行緒正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機位元組碼的記憶體地址;如果正在執行的是一個 native 方法,這個計數器值則為未定義(Undefined),
Q:Java多執行緒執行native方法時程式計數器為空,那么執行緒切換后如何找到之前執行到哪里了?
A: 對native方法而言,它的方法體并不是由Java位元組碼構成的,自然無法應用上述的“Java位元組碼地址”的概念,所以JVM規范規定,如果當前執行的方法是native的,那么pc暫存器的值未定義——是什么值都可以,Java執行緒總是需要以某種形式映射到OS執行緒上,映射模型可以是1:1(原生執行緒模型)、n:1(綠色執行緒 / 用戶態執行緒模型)、m:n(混合模型),
以HotSpot VM的實作為例,它目前在大多數平臺上都使用1:1模型,也就是每個Java執行緒都直接映射到一個OS執行緒上執行,此時,native方法就由原生平臺直接執行,并不需要理會抽象的JVM層面上的“pc暫存器”概念——原生的CPU上真正的PC暫存器是怎樣就是怎樣,就像一個用C或C++寫的多執行緒程式,它在執行緒切換的時候是怎樣的,Java的native方法也就是怎樣的, -Java執行緒總是需要以某種形式映射到OS執行緒上,映射模型可以是1:1(原生執行緒模型)、n:1(綠色執行緒 / 用戶態執行緒模型)、m:n(混合模型),
以HotSpot VM的實作為例,它目前在大多數平臺上都使用1:1模型,也就是每個Java執行緒都直接映射到一個OS執行緒上執行,此時,native方法就由原生平臺直接執行,并不需要理會抽象的JVM層面上的“pc暫存器”概念——原生的CPU上真正的PC暫存器是怎樣就是怎樣,就像一個用C或C++寫的多執行緒程式,它在執行緒切換的時候是怎樣的,Java的native方法也就是怎樣的, - RednaxelaFX
本地方法堆疊(Native Method Stack)
本地方法堆疊是為虛擬機使用到的 Native 方法服務,關于 Native 方法,官方給的說明是"",即在虛擬機規范中對本地方法堆疊中方法使用的語言、使用方式與資料結構并沒有強制規定,因此具體的虛擬機可以自由實作它,本地方法堆疊區域會拋出StackOverflowError和OutOfMemoryError例外
Native Method:
A native method is a Java method whose implementation is provided by non-java code.
public static native double getDouble(Object array, int index) throws IllegalArgumentException, ArrayIndexOutOfBoundsException;這些方法的宣告描述了一些非java代碼在這些java代碼里看起來像什么樣子,
虛擬機堆疊(VM Stack)
Java 虛擬機堆疊是執行緒私有的,描述的是 Java 方法執行程序的記憶體模型:每個方法在執行同時都會創建一個堆疊幀,每個方法從呼叫到執行完成的程序,就對應著一個堆疊幀在虛擬機中入堆疊到出堆疊的程序,堆疊幀中儲存區域變數表、運算元堆疊、動態鏈接、方法出口等資訊,
區域變數表(Local Variable Table)
是一組變數值存盤空間,用于存放方法引數和方法內部定義的區域變數,存放了編譯時期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)和物件的參考,區域變數表在編譯階段就確定了需要分配的最大容量,
區域變數表的容量以變數槽(Variable Slot)為最小單位,每個 Slot 都應該能存放一個 boolean、byte、char、short、int、float、reference 或 returnAddress 型別的資料,
虛擬機通過索引定位的方式使用區域變數表,索引值的范圍是從 0 開始至區域變數表最大的 Slot 數量,
下面我們看一個具體的代碼示例:
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
上述代碼反編譯后:
public int calc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 100
2: istore_1
3: sipush 200
6: istore_2
7: sipush 300
10: istore_3
11: iload_1
12: iload_2
13: iadd
14: iload_3
15: imul
16: ireturn
LineNumberTable:
line 10: 0
line 11: 3
line 12: 7
line 13: 11
LocalVariableTable:
Start Length Slot Name Signature
0 17 0 this Lai/advance/common/VariableLocal;
3 14 1 a I
7 10 2 b I
11 6 3 c I
我們注意到反編譯后的代碼第五行locals=4就說明了我們我們區域變數表的大小為 4 個 Slot,最下面的LocalVariableTable 展示了區域變數表里面具體的內容,對于實體方法(非 static)區域變數表第 0 位索引的 Slot 默認值實體的參考,也就是我們一直使用的this關鍵,其余區域變數依次排序,
運算元堆疊
運算元堆疊也稱操作堆疊,它是一個后入先出(Last In First Out,LIFO)堆疊,同區域變數表一樣,運算元堆疊的最大深度也在編譯時期寫入到 Code屬性中,具體可參考上述代碼示例中反編譯后stack=2,
在一個方法剛開始執行的時候,這個方法的運算元堆疊是空的,在方法執行程序中,會有各種位元組碼指令往運算元堆疊中寫入和提取內容,也就是說出堆疊/入堆疊操作,
動態鏈接
動態鏈接是程式在運行期間將位元組碼中的符號參考轉化為直接參考的程序,Class 檔案的常量池中有大量的符號參考,位元組碼中的方法的呼叫指令呼叫常量池中存放的字面量符號,而這些字面量符號指向具體的方法,
- 部分符號在類加載或者第一次使用的時候就轉化為直接參考,這種稱之為靜態鏈接,
- 部分符號參考在運行期間轉化為直接參考,這種轉化為動態鏈接
方法回傳地址
一個方法開始執行后只有兩種方式可以退出這個方法,
正常完成出口(Normal Method Invocation Completion)
執行引擎遇到任意一個方法回傳的位元組碼指令,根據回傳指令來決定是否將回傳值和回傳值的型別傳遞給上層的方法呼叫者,
一般來說,方法正常退出時,呼叫者的 PC 計數器的值可以作為回傳地址,堆疊幀中很可能會保存這個計數器值,
例外完成出口(Abrupt Method Invocation Completion)
在方法執行程序中遇到了例外,并且這個例外沒有在方法體內得到處理,例外完成出口的方式是不會給上層呼叫者產生任何回傳值的,
例外退出時,回傳地址是要通過例外處理表來確定的,所以堆疊幀中一般不會保存這部分資訊,
堆(Heap)
Java 堆是 Java 虛擬機所管理的記憶體中最大的一塊,堆為所有執行緒共享的記憶體區域,在虛擬機啟動時創建,堆唯一的目的就是存放物件的實體,幾乎所有的物件都在堆記憶體區域存放,此區域也是發生 OOM 的重災區,

堆的記憶體結構可以劃分為新生代和老年代,默認1:2,其中新生代又被細分為 Eden 和兩個個Survivor區域,兩個Servivor分別以from to 來進行區分,Eden:from:to 默認為 8:1:1,
JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來為物件服務,所以無論什么時候,總是有一塊 Survivor 區域是空閑著的,
方法區(Method Area)
方法區和堆一樣,是各個執行緒共享的記憶體區域,它用于儲存已被虛擬機加載的類資訊(InstanceKlass)、常量、靜態變數、及時編譯期編譯后的代碼等資料(比如spring 使用IOC或者AOP創建bean時,或者使用cglib,反射的形式動態生成class資訊等),對于方法區的具體實作,會根據不同的 JVM 以及不同的版本而有所區別,在目前已經發布的JDK 1.7的HotSpot中,已經把原本放在永久代的字串常量池移出,根據Java虛擬機規范的規定,當方法區無法滿足記憶體分配需求時,將拋出OutOfMemoryError例外,
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用于存放編譯期生成的各種字面量和符號參考,這部分內容將在類加載后進入方法區的運行時常量池中存放,運行時常量池具備動態性,在運行期間也可以將新的常量放入池中,當常量池無法再申請到記憶體時會拋出OutOfMemoryError例外,
補充說明:
方法區在邏輯上屬于堆的一部分,但是為了與堆進行區分,通常又叫“非堆”(Non-Heap),
永久代(Permanent Generation):永久帶是 Hotspot 虛擬機獨有的概念,因為 Hotspot 虛擬機把 GC 分代收集擴展至方法區,或者說用永久代來實作方法區,而對于其他虛擬機(BEA JRockit IBM J9等)是沒有永久代的概念的,
同樣對于 Hotspot 虛擬機在jdk1.6、jdk1.7、jdk1.8 版本對方法區的實作時有所不同的:
- 永久帶的概念存在小于等于1.7 版本,對于字串常量池來講 1.7 以前的版本存放于方法區,1.7版本字串常量池和類的靜態變數存放于堆中,符號參考(Symbols)轉移到了native heap,
- 1.8 沒有了永久帶的概念,方法區又元空間(Metaspace)來實作,不過元空間與永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地記憶體,因此,默認情況下,元空間的大小僅受本地記憶體限制,
- 1.8 元空間在儲存內容方面和 1.7 沒有發生改變,依然靜態變數和字串常量池在堆記憶體放,符號參考在 Native Heap 存放,元空間只存放儲類和類加載器的元資料資訊,只是在記憶體限制、垃圾回收等機制上改變較大,元空間的出現就是為了解決突出的類和類加載器元資料過多導致的OOM問題,
JDK 8 中永久代向元空間的轉換
- 字串存在永久代中,容易出現性能問題和記憶體溢位,
- 類及方法的資訊等比較難確定其大小,因此對于永久代的大小指定比較困難,太小容易出現永久代溢位,太大則容易導致老年代溢位,
- 永久代會為 GC 帶來不必要的復雜度,并且回收效率偏低,
- Oracle 可能會將HotSpot 與 JRockit 合二為一,
總結
本文只是講述了一些 JVM 的記憶體結構,以及不同記憶體區域的基本概念,對于不同記憶體區域的引數調整,以及會出現怎么樣的例外等內容會專門用一篇文章來講解,上面我們已經講明白了JVM 記憶體區域的劃分以及概念,下面根據腦圖歸納總結一下:

參考:
[1] 周志明.深入理解Java虛擬機:JVM高級特性與最佳實踐.北京:機械工業出版社,2013.
[2] RednaxelaFX.Java多執行緒執行native方法時程式計數器為空,那么執行緒切換后如何找到之前執行到哪里了?.
[3] RednaxelaFX.JVM符號參考轉換直接參考的程序?.
[4] secbro2 .JVM之記憶體結構詳解.
文章首發于陳建源的博客,歡迎訪問,
文章作者:陳建源
文章鏈接:https://www.techstack.tech/post/jvm-nei-cun-jie-gou/
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/77567.html
標籤:Java
上一篇:NativeXml
