一、概念
運行時數據區,Runtime Data Area,用於保存java程序運行過程中需要用到的數據和相關信息;經常說的把數據讀到內存,包括類加載之后的信息,從磁盤讀取文件信息等。
二、內存布局
三、各區域詳解
1.程序計數器(Program Counter)
線程私有的一小塊內存區域,用於存放執行指令的位置;
由於現代分時操作系統一般都采用時間片輪轉執行的方式進行調度,對於單核CPU來說,在某一時刻只能有一個處於就緒狀態的線程能獲取到CPU並執行,執行完一個時間片或者主動放棄CPU時,就切換到其他線程執行,也就是說在一個時間片內線程不一定運行完成,需要等待下一次調度,所以需要程序計數器來記錄下一次被調度時需要執行指令的地址。
java虛擬機循環運行的偽代碼可以粗略表示為:
while(not end){ 從PC中取出指針指向的地址; 執行該地址對應的指令; PC中的指針指向下一條將要被執行的指令; if(時間片結束){ PC記錄下一條需要執行指令的位置 }else{ //線程結束或主動放棄CPU end } }
2.虛擬機棧(JVM Stacks)
用於描述Java中方法執行的過程,包括方法調用,返回值等,是線程私有的一塊內存區域。
當執行或調用一個方法時,都會創建一個棧幀(Stack Frame),官方描述如下:
a frame is used to store data and partial results,as well as to perform dynamic linking,return values for method,and dispatch exceptions.
也就是用於存儲局部變量和部分計算結果,包括局部變量表、操作數棧、動態鏈接,返回值地址等。
(1)局部變量表(Local Variable Table)
保存方法參數和內部使用到的局部變量(必須初始化),作用域僅在這個方法內,方法執行完成結束生命周期;
用jclasslib觀察不同的方法編譯后的字節碼
測試類
public class Test {
//非static帶形參的方法 public void add(String prev,int next){ String result = prev + next; System.out.println(result); } //static方法,包括main方法 public static void append(String prev,int next){ String result = prev + next; System.out.println(result); } }
概覽
Methods中存放的就是編譯后各方法的字節碼
無參構造方法局部變量表中只有一個this。
static方法
非static的普通方法
可以看出非static方法(包括構造方法)局部變量表下標為0的位置使用存放的都是this,代表本實例,這也是為什么我們能在這些方法里面直接使用this,調用當前實例的其他方法;其他變量按出現的先后順序依次存儲在局部變量中;
cp_info#xxx,表示符號引用存放在字符串常量池中的xxx下標位置處。
(2)操作數棧(Operand stack)
每一個棧幀都對應着一個操作數棧,用於方法體中操作數運算時的入棧和出棧,代表的是運算的過程。
用一個典型的筆試題來理解操作數棧,++x和x++字節碼指令的執行順序
測試類
public static void main(String[] args) { int x = 8; x = x++; System.out.println(x); } public static void main(String[] args) { int x = 8; x = ++x; System.out.println(x); }
x=x++字節碼
//測試x=x++ 0 bipush 8 //把常量8壓棧 2 istore_1 //常量8出棧並存儲到局部變量表下標為1的位置,也就是把8賦值給x 注:前兩條指令代表int x=8執行完成,雖然賦值操作是兩條指令,但是由於8壓棧是不能被修改的,所以總體也是原子性的。 3 iload_1 //把局部變量表下標為1的位置的變量值拿出來壓棧 4 iinc 1 by 1 //局部變量表下標為1的位置執行自增1的操作 注:此處操作的不是壓入棧中的數據,而是局部變量表中的數據,也就是局部變量表中的x從8變為了9 7 istore_1 //把棧中的數據8重新存到局部變量表中下標為1的位置,這個時候x又從9變為了8 8 getstatic #2 <java/lang/System.out> //調用System.out進行輸出 11 iload_1 12 invokevirtual #3 <java/io/PrintStream.println> 15 return
x=++x字節碼
//測試x=++x 0 bipush 8 2 istore_1 注意:這里少了一條iload_1指令,也就是沒有將8壓棧 3 iinc 1 by 1 //這里同樣的是把局部變量表下標為1的位置執行自增1的操作 6 iload_1 //這里取出來的數據是自增之后的值9 注:所以x=++x最后執行的結果是9 7 istore_1 8 getstatic #2 <java/lang/System.out> 11 iload_1 12 invokevirtual #3 <java/io/PrintStream.println> 15 return
總結:可以看出x=x++比x=++x多執行了一條指令,把原數據先壓棧,然后自增,最后又把原數據進行壓棧,導致結果沒變;x=x++是把原數據賦值給x,而x=++x是把自增后的數據賦值給x。
x=1是原子性操作,因為1在壓棧的過程中是不能被改變的;而x++和++x都不是原子性操作,是因為自增操作和賦值操作是兩條指令,CPU執行這兩條指令時可能會重排序(CPU執行指令的效率會比從內存中讀取數據高很多)。
(3)動態鏈接(Dynamic Linking)
指向常量池,用於標識變量,方法名,類名等的符號引用;Java中的類經過加載后會將符號引用進行解析,然后存於常量池中。
(4)返回值地址(Return Address)
被調用方法執行結束后返回值存放的地址以及調用方法應該繼續執行的指令位置;比如被調用方法return new Object(),那么會把new Object()在堆中的位置記錄下來。
注:JVM的指令集可以到https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html(不同jdk版本)進行查閱。
遞歸執行(方法調用)的指令
測試代碼
字節碼指令集
main方法調用
fibo方法遞歸調用
0 iload_1 1 ifeq 9 (+8) 4 iload_1 5 iconst_1 -> 把1作為常量; 6 if_icmpne 11 (+5) -> 出棧並比較兩個值,如果不相等則調到第11條指令; 9 iconst_1 -> 相等,則再次把1壓棧; 10 ireturn -> 返回1; 11 aload_0 -> aload表示引用壓棧,0的位置是this,把this壓棧 12 iload_1 ->下標為1的位置(變量n)壓棧 13 iconst_1 ->常量1壓棧 14 isub ->兩個數相減 15 invokevirtual #4 <cn/merson/jvm/JvmStackTest.fibo> ->從常量池中找到fibo方法並調用,參數為上一步相減的結果 18 aload_0 19 iload_1 20 iconst_2 ->常量2壓棧 21 isub 22 invokevirtual #4 <cn/merson/jvm/JvmStackTest.fibo> ->調用fibo方法 25 iadd -> 將15和22執行的結果相加,這里是在15和22都執行完成了之后才會執行,也就是棧楨一直位於虛擬機棧中。 26 ireturn -> 返回到main中 注意:如果main中是用變量接受返回值,這里會把值直接寫到main方法中的局部變量表(xstore指令); 指令15和22會分別執行自己的fibo指令,一直執行到遞歸出口,也就是第六條指令相等的時候。
JVM Stack規定的兩種異常
如果線程請求的棧深度大於虛擬機所允許的深度,拋出 StackOverflowError 異常;
如果虛擬機棧可以動態擴展(當前大部分的 Java 虛擬機都可動態擴展),如果擴展時無法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。
3.本地方法棧(Native Method Stacks)
和虛擬機棧類似,只不過虛擬機棧用於執行Java本身的方法,而本地方法棧用於執行Native方法(非Java代碼實現),是線程私有的內存空間。
4.堆(Heap)
所有線程共享的內存空間,絕大多數Java對象的分配區域,垃圾回收的主要區域;具體的可以閱讀https://www.cnblogs.com/merson1314/p/13680056.html,對象分配的過程及垃圾回收過程;https://www.cnblogs.com/merson1314/p/13673444.html,堆內存邏輯分區。
5.方法區(Method Area)
所有線程共享的內存空間,用於存放Class對象等,是一個邏輯概念,在JDK1.8前后分為PermSpace和MetaSpace兩種不同的實現方式。
官方描述:The JVM has a method area that is shared among all JVM threads.It stores per-class structures.
PermSpace,永久代:
JDK1.8之前運行時常量池也存放在永久代中,在程序啟動時指定永久代的大小並且不能動態擴容(運行期間也不能修改),垃圾回收不會清理這塊內存區域,當永久代內存不夠用時會拋出OOM異常。
MetaSpace,元數據區:
JDK1.8之后運行時常量池存放於堆中,便於垃圾回收,如果啟動時不指定元數據區大小,理論上來說最大可用內存就是機器的物理內存,當然,MetaSpace支持動態申請內存,Full GC會回收MetaSpace。
運行時常量池:
主要用於存放類,變量等的符號引用和直接引用,通過下標進行識別,符號引用解析之后的直接引用也會存放在運行時常量池中。
方法區回收主要是回收廢棄的常量和無用的類;
廢棄的常量表示沒有任何一個地方引用這個常量,當內存不夠用時,會回收。
無用的類必須同時滿足:
①Java堆中不存在該類的任何實例,也就是該類的所有實例都已經被回收;
②加載該類的ClassLoader已經被回收;
③該類對應的Class對象在任何地方沒有引用了,也不能通過反射訪問該類的方法。
6.直接內存(Direct Memory)
直接內存不屬於運行時數據區,Java中直接分配堆外內存,也就是用戶空間(線程)直接訪問操作系統或者內核空間,多用於網絡訪問提升效率。
NIO使用的就是直接內存,zero copy用把數據從內核空間拷貝到JVM的內存空間。
7.內存溢出和內存泄露
內存溢出(OutOfMemoryError)
當堆內存已經不能再接受分配新的對象,或者老年代不能接受年輕代准備升級到老年代的對象,並且Full GC之后堆內存還是不夠用,會拋出OOM。
內存泄露(Memory Leak)
在內存中對象已經不需要的時候(應該被GC),但實際上仍然保留着這塊內存和它的訪問方式(引用),沒有被回收;不一定產生OOM。