jvm - 運行時內存結構
注意 : 本系列文章為學習系列,部分內容會取自相關書籍或者網絡資源,在文章末尾處會有標注
內存模型示意圖
每個區域的作用簡述
pc寄存器 (program counter)
每一條java虛擬機線程都有自己的pc寄存器
在任意時刻,一條java虛擬機線程只會執行一個方法的代碼,正在被線程執行的方法稱為該線程的當前方法
(如果這個方法不是native的,那pc寄存器就保存java虛擬機正在執行的字節碼指令的地址)
(如果這個方法是natice的,那pc寄存器的值是undefined)
pc寄存器的容量至少應當能保存一個returnAddress類型的數據或者一個與平台相關的本地指針的值
虛擬機棧 (virtual machine stack)
每條java虛擬機線程都有自己私有的java虛擬機棧,這個棧與線程同時創建,用於存儲棧幀 (用於存儲局部變量與一些尚未算好的結果)
除了棧幀的入棧和出棧之外,不會再受其他因素影響,所以棧幀可以在堆中分配,java虛擬機棧所使用的內存不需要保證是連續的
java虛擬機規范允許java虛擬機棧被實現成固定大小的,也允許根據計算來動態擴展和收縮
(如果是固定大小的,那每一個線程的java虛擬機棧的容量可以在線程創建時獨立選定)
如果線程請求分配的棧容量超過java虛擬機棧允許的最大容量,java虛擬機將拋出一個StackOverflowError異常
如果java虛擬機棧可以動態擴展,並且在嘗試擴展時無法申請到足夠內存,或者在創建新線程的時候沒有足夠的內存去創建對應的虛擬機棧,那么java虛擬機將會拋出一個OutOfMemoryError異常
堆 (heap)
堆是可供各個線程共享的運行時區域,也是提供所有類實例和數組對象分配內存的區域
堆在java虛擬機啟動的時候被創建,它存儲了被自動內存管理系統(垃圾收集器)所管理的各種對象,這些受管理的對象無需也無法顯示地銷毀
堆的用量可以是固定的,也可以是隨程序執行的需求動態擴展,並在不需要過多空間的時候自動收縮
堆所使用的內存空間不需要保證是連續的
如果實際所需的堆超過了自動內存管理系統能提供的最大容量,那java虛擬機將會拋出一個OutOfMemoryError異常
方法區 (method area)
是堆的邏輯組成部分
可供各個線程共享的運行時區域
存儲了每個類的結構信息
在虛擬機啟動的時候創建
可以選擇不實現垃圾收集和壓縮
用量可以是固定的,也可以是隨程序執行的需求動態擴展,並在不需要過多空間的時候自動收縮
所使用的內存空間不需要保證是連續的
如果方法區的內存空間不能滿足內存分配請求,那java虛擬機將會拋出一個OutOfMemoryError異常
運行時常量 (runtime constant pool)
它是class文件中每一個類或者接口的的常量池表的運行時表示形式,包括了多種不同的常量,從編譯期可知的數值字面量到必須在運行期解析后才能獲得的方法和引用
每一個運行時常量都在java虛擬機的方法區中分配,在加載類和接口到虛擬機后,就創建對應的運行時常量池
創建類或者接口時,如果構造運行時常量池所需要的內存空間超過了方法區所能提供的最大值,那java虛擬機將會拋出一個OutOfMemoryError異常
本地方法棧 (native method stack)
Java虛擬機可能會用到傳統的棧(稱為C stack)來支持native方法的執行,這個棧就是本地方法棧
如果java虛擬機支持本地方法棧,那么每一個線程的本地方法棧容量可以在創建棧的時候獨立選定
如果線程請求分配的棧容量超過本地方法棧的最大容量,java虛擬機將拋出一個StackOverflowError異常
如果本地方法棧可以動態擴展,並且在嘗試擴展時無法申請到足夠內存,或者創建新線程時沒有足夠的內存區創建對應的本地方法棧,那java虛擬機將會拋出一個OutOfMemoryError異常
棧幀 (frame)
棧幀的存儲空間由創建它的線程分配在java虛擬機棧中
棧幀是用來存儲數據和部分過程結果的數據結構,同時也用來處理動態鏈接,方法返回值,異常分派
棧幀隨着方法的調用而創建,隨着方法的結束而銷毀,無論方法是正常完成還是異常完成,都算作方法結束
每個棧幀都有自己的本地變量表,操作數棧,指向當前方法所屬的類的運行時常量池的引用
在某條線程執行過程中的某個時間點上,只有目前正在執行的那個方法的棧幀是活動的
調用新的方法時,新的棧幀會隨之而創建,並且會隨着程序控制權移交到新方法,而稱為新的當前棧幀
方法返回之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,然后虛擬機會丟棄掉當前棧幀,使得前一個棧幀重新稱為當前棧幀
注意 : 棧幀是線程本地私有的數據,不可能在一個棧幀之中引用另外一個線程的棧幀
棧幀 - 局部變量表
每個棧幀內部都包含一組稱為局部變量表的變量列表
棧幀中的局部變量表的長度由編譯期決定
存儲於類或接口的二進制表示之中(class),即通過方法的code屬性保存以及提供給棧幀使用
一個局部變量可以保存一個類型為如下的數據,boolean,byte,char,short,int,float,reference,returnAddress
兩個連續的局部變量可以保存一個類型為long或double的數據
局部變量表使用索引來進行定位訪問
java虛擬機使用局部變量表來完成方法調用時的參數傳遞
棧幀 - 操作數棧
每個棧幀內部都包含一個稱謂操作數棧的后進先出棧
棧幀中的操作數棧的最大深度由編譯期決定,並且通過方法的code屬性保存以及提供給棧幀使用
棧幀在剛剛創建時,操作數棧是空的
java虛擬機提供一些字節碼指令來從局部變量表或者對象實例的字段中復制常量或者變量的值到操作數棧中,也提供了一些指令用於操作數棧,取走數據,操作數據,以及把操作結果重新入棧
在調用方法時,操作數棧也用來准備調用方法的參數以及接收方法的返回結果
操作數棧的每一個位置上可以保存一個java虛擬機定義的任意數據類型的值,包括long和double
任意時刻,操作數棧都會有一個確定的深度,一個long或者double類型的數據會占用兩個單位的棧深度,其他數據類型則會占用一個單位的棧深度
棧幀 - 動態鏈接
每個棧幀內部都包含一個指向當前方法所在類型的運行時常量池的引用,以便對當前方法實現動態鏈接
在class文件里,一個方法若要調用其他方法,或者訪問成員變量,則需要通過符號引用來表示,動態鏈接的作用就是將這些符號引用所表示的方法轉換為對實際方法的直接引用
溢出的實例代碼
棧溢出
public class StackOom {
public int num = 1;
public void stack() {
num++;
this.stack();
}
public static void main(String[] arge) {
StackOom stackOom = new StackOom();
stackOom.stack();
}
}
Exception in thread "main" java.lang.StackOverflowError
at org.itkk.learn.StackOom.stack(StackOom.java:15)
at org.itkk.learn.StackOom.stack(StackOom.java:15)
at org.itkk.learn.StackOom.stack(StackOom.java:15)
at org.itkk.learn.StackOom.stack(StackOom.java:15)
以上這段代碼,stack方法不斷的遞歸調用,最終達到java虛擬機棧的最大容量,就拋出了StackOverflowError異常
堆溢出
public class HeapOom {
private List<byte[]> data = new ArrayList<>();
public void heap() {
while (true) {
data.add(new byte[1024 * 1024]);
}
}
public static void main(String[] arge) {
HeapOom heapOom = new HeapOom();
heapOom.heap();
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at org.itkk.learn.HeapOom.heap(HeapOom.java:21)
at org.itkk.learn.HeapOom.main(HeapOom.java:32)
以上這段代碼,heap方法,以死循環的方式不斷的往data中添加1M大小的數組對象,最終堆中的內存空間不能滿足data存放的要求,而拋出了OutOfMemoryError異常
元空間溢出
public class MetaspaceOom {
static String str = "string";
public static void main(String[] arge) {
List<String> list = new ArrayList<>();
while (true) {
str += str;
list.add(str.intern());
}
}
}
java.lang.OutOfMemoryError: Metaspace
at sun.misc.Launcher.<init>(Unknown Source)
at sun.misc.Launcher.<clinit>(Unknown Source)
at java.lang.ClassLoader.initSystemClassLoader(Unknown Source)
at java.lang.ClassLoader.getSystemClassLoader(Unknown Source)
以上這段代碼使用如下命令執行 :
java -XX:MetaspaceSize=2m -XX:MaxMetaspaceSize=2m org.itkk.learn.MetaspaceOom
在運行MetaspaceOom類時,限定了元空間的大小,而main方法中,則以死循環的方式不停的拷貝生成新的字符串常量(intern方法),而字符串常量存儲於元空間中,最終字符串常量超出了元空間的容量,從而拋出OutOfMemoryError異常(Metaspace)
java8中永久代的變化
在java8中,永久代已經由元空間替代了,在上面章節中的元空間溢出的實例代碼中,在jdk6中,會出現"PermGen Space"溢出,在jdk7和jdk8中,則是出現"Java heap space"溢出
而且在java8中,-XX:PermSize和-XX:MaxPermGen已經提示無效了,如下:
使用如下命令運行
java -XX:PermSize=8m -XX:MaxPermSize=8m -Xmx16m org.itkk.learn.MetaspaceOom
D:\develop\JetBrains\IdeaProjects\learn\leanmain\target\classes>java -XX:PermSize=8m -XX:MaxPermSize=8m -Xmx16m org.itkk.learn.MetaspaceOom
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=8m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=8m; support was removed in 8.0
會得出如下兩個警告,可以得知,Perm在jdk8中已經不存在了
元空間和永久代最大的區別在於,元空間不再java虛擬機內存中,而是直接使用的本地內存,空間大小僅受本地內存的限制,並且可以使用jvm參數來指定元空間的大小
上面元空間溢出的實例代碼中就使用了-XX:MetaspaceSize和-XX:MaxMetaspaceSize來指定元空間的最大和最容量
參考文獻
文章中部分內容取自<<Java虛擬機規范.Java SE 8版>>
結束
俗話說,不了解jvm的java程序員,等於耍流氓
深入的了解底層的能有助於我們更好的理解java程序運行的機制,從而幫助我們寫出更好的代碼.
關於本文內容 , 歡迎大家的意見跟建議
代碼倉庫 (博客配套代碼)
想獲得最快更新,請關注公眾號