JVM(Java Virtual Machine)又被分為三大子系統,類加載子系統,運行時數據區,執行引擎。在這里我們主要講解一下JVM的運行時數據區,也就是我們常說的JVM存儲數據的內存模型。在這里提一點,平常我們常說內存模型,其實在Java中存在兩大內存模型,一個是JVM的內存模型,也就是堆,棧之類的。還有一個是Java線程工作的內存模型,java工作的內存模型指的是主內存,工作內存,兩個是不同的概念
JVM的結構圖
這里重點講一下JVM的運行時數據區,也就是我們所說的JVM內存模型。其實像JVM結構中的類加載子系統,也能夠講出很多東西出來。
首先,JVM的內存模型分為線程私有,線程共享的區域
線程私有部分
圖中所畫的部分程序計數器,虛擬機棧,本地方法棧是每個線程私有的部分,大概講述一下每個部分所起到的作用
程序計數器:線程在運行期間,由於會出發CPU時間片資源搶奪的情況,假如線程A執行到if判斷,循環,異常處理等這些字節碼指令,時間片突然被搶占,出現阻塞,程序計數器會幫助記錄每個線程所執行到下一條字節碼指令,等線程A再次拿到時間片繼續執行。
虛擬機棧:每個線程在執行每個方法時,都會創建一個棧幀,棧幀里面又會包括一些局部變量表,操作數棧,方法出口等信息,每個方法從執行到執行完成,也就是完成我們的入棧和出棧過程
首先看一段代碼,來講解棧幀中每個部分的作用
public class Test {
public static void test() {
int a = 1;
int b = 2;
int c = (a + b) * 10;
}
public static void main(String[] args) {
test();
}
}
以上代碼對應的虛擬棧圖
局部變量表:在我們程序中,方法中定義的一些局部變量都會存放在我們的局部變量表,也就是我們代碼中的a,b,c這種局部變量
將上述代碼用javap -c Test命令可以查看到程序的字節碼指令
public class com.ezhiyang.crm.config.Test {
public com.ezhiyang.crm.config.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void test();
Code:
0: iconst_1 // 將一個常量1加載到操作數棧
1: istore_0 // 將一個數值(也就是常量1)從操作數棧存儲到局部變量 表,也就是我們代碼中的int a = 1
2: iconst_2
3: istore_1
4: iload_0
5: iload_1
6: iadd // 執行加法操作
7: bipush // 將10入操作數棧 10
9: imul // 執行乘法操作
10: istore_2 // 再將乘法出來的結果從操作數棧存儲到局部變量 表,也就是我們的int c = (a + b) * 10
11: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method test:()V
3: return
}
操作數棧:像我們的程序后面都會被編譯成這種字節碼指令,而操作數棧將變量之間的運算入棧,然后存儲計算結果,再出棧賦值給局部變量表
方法出口:代碼中main()方法調用了test()方法,這時test()執行完成后,需要繼續執行main()方法,方法出口記錄了test()方法執行完成后的一個出口,也就是回到main()
本地方法棧:在Java代碼中我們有時可以看到用native修飾的方法,而這些方法並不由Java語言去實現,而是由Java去調用底層的C++語言實現的,跟我們的虛擬機棧有點類似,只不過是執行native方法的線程棧幀
線程共享部分
線程共享的部分就是我們圖中所畫的堆,方法區(又叫做永久代,JDK1.8后被改為元空間)
看一張方法區內存分布圖
方法區:方法區主要存儲的是虛擬機加載的類信息(版本,字段,方法,接口等信息),成員常量,靜態變量等數據,其中又包括一部分是運行時常量池,運行時常量池存放的是編譯期間生成的符號引用,后面經過解析出來的直接引用也會儲存在常量池中。
當虛擬機new指令時,首先會根據這個指令的參數在常量池中定位到一個類的符號引用,如果定位不到,則需要重新對這個類進行加載,解析和初始化了,這里就涉及了Class文件加載的7大過程。如果能找到符號引用,證明這個類已經加載過,並會給該對象分配內存空間,調用構造方法進行初始化。也就是我們new一個對象發生了那些事
JDK1.8后,方法區被叫做元空間,並且內存大小限制不限JVM的內存大小限制,而是直接使用計算機的直接內存,受計算機的內存大小限制
堆:堆是JVM中最大的一塊內存區域了,主要存放我們程序中new出來的對象,提一點並非所有的對象都是在堆上進行分配,隨着線程逃逸,標量替換等技術的發展,對象也有可能在棧上進行分配。下面通過一段代碼進行講解,對象不一定在堆上進行分配
public class Test {
public static void test() {
A a = new A();
}
public static void main(String[] args) {
test();
}
}
class A {
int a = 0;
}
像這段代碼,執行test()方法時,new了一個A對象,但是A對象又只有一個基本類型變量a,A對象也沒有出現線程逃逸現象,因為JVM存在一些指令優化功能,並會把A對象直接進行標量替換成int a = 0,這時就會出現對象在棧上進行分配。
堆也是GC進行回收最主要的區域,容易出現OutOfMemoryError異常
堆中又分為young(新生代),old(老年代),分配的內存比例1:2。young新生代中的對象具有"朝生夕死"的特點,也就是對象剛創建不久,並會被回收掉。是Minor回收的主要區域。而老年代中的對象具有的特點則是不容易被回收掉,一些大對象比如數組並會存放在老年代中。是Full GC回收的主要區域。后面寫一篇JVM GC垃圾回收的文章做介紹,這里不做過多的講解了
young新生代中又分為eden區,兩個survivor區,分配的內存比例8:1:1。我們程序中所new出的對象首先會在eden區進行分配,然后經過Minor GC,eden區沒有被回收的對象又會被放到survivor Form區,survivor Form區經過GC后的對象沒有被回收掉,又會轉移到survivor To區,這兩個區的對象是可以雙向轉移的。新生代采用GC的算法是復制算法,兩個survivor區正是復制算法所要預留出來的區域
這里主要講解的是JVM內存模型中每個部分所起到作用,其實里面還牽涉到了很多知識,像GC,收集器,類加載等很多知識,后面會一一寫文章進行講解。
初次寫博客,如有寫的不對的地方,還望大家指正!!!