JVM - 記憶體模型
1. JVM 記憶體模型
Java虛拟機(Java Virtual Machine,簡稱JVM)在執行Java程式的過程中會把它所管理的記憶體劃分為若幹個不同的資料區域。
這些區域都有各自的用途,以及建立和銷毀的時間,有的區域随着虛拟機程序的啟動而存在,有些區域則是依賴使用者線程的啟動和結束而建立和銷毀。
這些記憶體區域即JVM記憶體也被稱之為運作時資料區(Runtime data area)。

- 藍色區域為線程共享
- 白色區域為線程私有
1.1 程式計數器
程式計數器(Program Counter Register)是一塊較小的記憶體空間,記錄目前線程執行程式的位置, 改變計數器的值來确定執行下一條指令, 比如循環、分支、方法跳轉、異常處理, 線程恢複都是依賴程式計數器來完成。
特點
- 線程私有,多線程是通過輪流切換并配置設定處理器執行時間的方式來實作的,任何一個确定的時刻隻會執行一條線程中的指令,為了切換後能恢複正确位置,互不影響是以是線程私有的。
- 占用記憶體空間非常小,可以戶略不計。
- 唯一一個java虛拟機規範中沒有OOM情況的區域。
- 執行的若是java方法,記錄虛拟機位元組碼指令位址,若是native,則計數器值為空(undefined)
1.2 虛拟機棧
java虛拟機棧(java virtual Machine stacks)也叫棧記憶體,描述的是java方法執行的記憶體模型。
特點
- 線程私有,随着線程的建立而建立,結束而釋放,生命周期和線程一緻。不存在垃圾回收。
- 方法執行時會建立一個棧幀,每個方法的執行對應着一個棧幀進棧出棧的過程。
- 當線程請求的棧深度超過最大值 -> StackOverflowError (無限遞歸)。
- 當棧動态擴充而無法申請足夠的記憶體時 -> OutOfMemoryError。
局部變量(基本類型,對象引用),方法的引用符号
1.2.1 棧幀(stack frame)
用于支援虛拟機進行方法調用和方法執行的資料結構。棧幀存儲了方法的局部變量表、操作數棧、動态連接配接和方法傳回位址等資訊。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛拟機棧中從入棧到出棧的過程。
特點
棧幀,用于存儲局部變量表,操作棧,動态連接配接,方法出口等資訊,局部變量表(基本類型,對象引用,傳回值)在編譯期間完成配置設定,運作期間不會改變局部變量表的大小。
位于JVM虛拟機棧棧頂的元素才是有效的,即稱為目前棧幀,與這個棧幀相關連的方法稱為目前方法,定義這個方法的類叫做目前類。
棧幀存儲内容
- 局部變量表: 輸入參數,輸出參數,方法内變量(基本類型變量,對象變量的引用)。局部變量表中的變量隻在目前函數調用中有效,當函數調用結束後,随着函數棧幀的銷毀,随之銷毀。本質是一個數組,0号下标存儲的是this。
- 基本類型: boolean、byte、char、short、int、float、long、double。(long,double占用兩個局部變量空間(slot),其餘占用一個)
- 對象引用(reference類型): 根據不同的虛拟機實作,它可能是一個指向對象起始位址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)
- 傳回值(returnAddress): 指向了一條位元組碼指令的位址
- 操作數棧: 記錄出棧,入棧的操作(後進先出),執行引擎計算需要。
- 與局部變量表一樣,均以字長為機關的數組。不過局部變量表用的是索引,操作數棧是彈棧/壓棧來完成一次資料的通路。操作數棧可了解為java虛拟機棧中的一個用于計算的臨時資料存儲區。
- 每一個操作數棧都會擁有一個明确的棧深度用于存儲數值,一個slot的數值使用一個棧深度,2個slot的數值需要兩個棧深度,該深度在編譯後即确定下來。
- 存儲的資料與局部變量表一緻含int、long、float、double、reference、returnType,操作數棧中byte、short、char壓棧前(bipush)會被轉為int。
-
操作數棧就是JVM執行引擎的一個工作區,當一個方法被調用的時候,一個新的棧幀也會随之被建立出來,但這個時候棧幀中的操作數棧卻是空的,隻有方法在執行的過程中,
才會有各種各樣的位元組碼指令往操作數棧中執行入棧和出棧操作。比如在一個方法内部需要執行一個簡單的加法運算時,首先需要從操作數棧中将需要執行運算的兩個數值出棧,待運算執行完成後,再将運算結果入棧。
- 資料運算的地方,大多數指令都在操作數棧彈棧運算,然後結果壓棧。
- java虛拟機棧是方法調用和執行的空間,每個方法會封裝成一個棧幀壓入占中。其中裡面的操作數棧用于進行運算,目前線程隻有目前執行的方法才會在操作數棧中調用指令(可見java虛拟機棧的指令主要取于操作數棧)。
- 為什麼會用棧,因為jvm是基于棧的指令架構,基于寄存器的指令架構能完成更複雜的指令但是和硬體依賴比較強。
- 方法出口資訊: 正常傳回,異常傳回,記錄資訊用于恢複它的上層方法執行狀态。
- 目前棧幀出棧
- 恢複上層方法的局部變量表和操作數棧,把傳回值(如果有的話)壓入調用者的操作數棧中
- 調整PC計數器的值指向方法調用指令後的下一條指令
- 動态連接配接: 棧幀中存儲着指向運作時常量池(jdk7)該棧所屬方法的引用符号,當在類加載或第一次使用時轉化為直接引用時稱之為靜态解析,而在每次運作期間轉化為直接引用即為動态連接配接。
- 附加資訊: 虛拟機中增加一些規範中沒有的描述資訊到棧幀中,例如和調試相關的資訊。
JVM - 記憶體模型JVM - 記憶體模型
指令
配置線程配置設定記憶體的大小,即虛拟機棧的大小
-Xss1m jdk5.0後預設時1M 一個程序内的線程數是有限制的,不能無限生成,經驗值在3000~5000左右。
1.3 本地方法棧
本地方法棧(Native Method Stack),和虛拟機棧類似,差別在于它服務的時Native方法。
特點
- 和虛拟機棧差不多
- 之服務于Native方法,當執行引擎執行時會加載native libraries。
- 和JVM棧一樣,這個區域也會抛出StackOverflowError和OutOfMemoryError異常。
1.4 方法區
方法區(Method Area),别稱永久代,非堆。用于存放已被加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。
特點
- 線程共享記憶體區域。
- 不需要連續的記憶體,可以動态擴充。
- 當方法區無法滿足記憶體配置設定需求時,會抛出OutOfMemoryError。
- 對這塊區域進行垃圾回收的主要目标是對常量池的回收和對類的解除安裝,HotSpot 虛拟機把它當成永久代(Permanent Generation)來進行垃圾回收。
- 方法區和永久代,本質上兩者并不等價。之是以稱為永久代是因為HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分記憶體。
- JDK7已經開始轉移到堆或者Native堆中,JDK8後徹底移除稱之為中繼資料區(Metaspace),元空間并不在虛拟機中,而是使用本地記憶體。是以,預設情況下,元空間的大小僅受本地記憶體限制。
- JDK8 是HotSpot VM 與 JRockit VM 的融合, 因為 JRockit 沒有永久代是以這也是 PermGen 最終被移出一個原因。
存儲
靜态變量: 被static修飾的變量
類資訊: 版本、字段、方法、接口等描述資訊
常量池
字面值常量: 文本字元串,被final修飾的類變量
符号引用量: 即時編譯器編譯後的代碼(各種符号引用: 類的權限定名、字段名和屬性、方法名和屬性)等資料
1.4.1 常量池
class檔案中除了包含類的版本、字段、方法、接口等描述資訊外,還有一項資訊就是常量池(constant pool table),用于存放編譯器生成的各種字面量(Literal)和符号引用(Symbolic References)。
指令
JDK 1.7通過 -XX:PermSize和-XX:MaxPermSize來調節
JDK8 使用-XX:MetaspaceSize和-XX:MaxMetaspaceSize 配置記憶體大小
1.4.1.1 靜态常量池
在java檔案編譯成為class檔案後,class檔案中存儲的即為class常量池又叫靜态常量池。
該常量池中通常存儲一下資訊:
字面量: 文本字元串、被聲明為final的常量值,基本資料類型的值等
符号引用: 一組符号用來描述所引用的目标,符号可以是任何形式的字面量
- 類和接口的全限定名
- 字段的名稱和描述符
方法的名稱和描述符
描述符是描述字段或方法的類型的字元串。
class位元組碼中的常量池
位元組碼常量池類型
序号 | 常量池中資料項類型 | 類型标志 | 類型描述 |
---|---|---|---|
1 | CONSTANT_Utf8 | 1 | UTF-8 編碼的Unicode字元串 |
2 | CONSTANT_Integer | 3 | int 類型字面值 |
3 | CONSTANT_Float | 4 | float 類型字面值 |
4 | CONSTANT_Long | 5 | long 類型字面值 |
5 | CONSTANT_Double | 6 | double 類型字面值 |
6 | CONSTANT_Class | 7 | 對一個類或接口的符号引用 |
7 | CONSTANT_String | 8 | String 類型字面值 |
8 | CONSTANT_Fieldref | 9 | 對一個字段的符号引用 |
9 | CONSTANT_Methodref | 10 | 對一個類中聲明的方法的符号引用 |
10 | CONSTANT_InterfaceMethodref | 11 | 對一個接口中聲明的方法的符号引用 |
11 | CONSTANT_NameAndType | 12 | 對一個字段 或 方法的部分符号引用 |
1.4.1.2 運作時常量池
運作時常量池(Runtime Constant Pool),是方法區的一部分。
jvm虛拟機在将類裝載到記憶體中後,jvm就會将class常量池中的内容轉存到運作時常量池中,類在解析後将把符号引用替換成直接引用,常說的常量池,就是指運作時常量池。運作時常量池也是每個類都有一個。
類和接口的全限定名常量 CONSTANT_Class_info 值 cp_info #63 <java/lang/StringBuilder> 符号
對應#63位置的引用常量 CONSTANT_Utf8_info 值為 java/lang/StringBuilder 符号
運作時将會把 java/lang/StringBuilder 符号替換成 實際記憶體的位址
1.4.1.3 常量池關系
1.4.2 字元串常量池
類加載完成,經過驗證準備階段之後,String 類型将在字元串常量池中建立對象。資料結構是一個hash表,一個JVM中隻有一份,全局的。
字元串常量池:
JDK7之前,存儲在運作時常量池即方法區中,此時hotspot對方法區的實作稱之為永久代。
JDK7時,字元串常量池拿到了堆中,運作時常量池剩下的東西還在方法區, 也就是hotspot中的永久代。
JDK8後,hotspot移除了永久代用元空間(Metaspace)取而代之, 這時候字元串常量池還在堆, 運作時常量池還在方法區, 隻不過方法區的實作從永久代變成了元空間(Metaspace)。
使用字元串常量池的原因:
節省記憶體空間: 常量池中所有相同的字元串常量被合并,隻占用一個空間
節省運作時間: 比較字元串時,== 比equals()快。對于兩個引用變量,隻用 == 判斷引用是否相等,也就可以判斷實際值是否相等。
動态生成常量: String類的intern()方法,将在string pool中尋找,若找到則直接傳回引用,找不到建立之後存儲在常量池中傳回
1.4.3 基本類型常量池
java虛拟機緩存了Integer、Byte、Short、Character、Boolean包裝類在-128~127(Character 0-127)之間的值,如果取值在這個範圍内,
會從int常量池取出一個int并自動裝箱成Integer,超出這個範圍就會重新建立一個。
1.4.4 元空間
元空間(metaspace),JDK8之後用來代替方法區,存儲再本地記憶體。
MetaSpaceSize:初始化元空間大小,控制發生GC門檻值
MaxMetaspaceSize : 限制元空間大小上限,防止異常占用過多實體記憶體
1.5 堆
堆(heap),JVM中占用記憶體最大的一塊區域。主要存儲new關鍵字建立的對象執行個體和數組。
特點
主要的資料存儲區域,線程共享的
主要的垃圾回收對象
記憶體不足時将抛出OutOfMemoryError異常
1.5.1 堆記憶體的劃分
JVM - 記憶體模型JVM - 記憶體模型
-
新生代(Young Generation)
存儲new關鍵字建立的新對象,預設eden:From Survivor:To Survivor=8:1:1
1.1 Eden區
eden即伊甸園,它占用的空間通常比survivor大。對象剛開始建立的時候就會存儲在該區域。
1.2 Survivor區。
1.2.1 From Survivor
實際上作為回收的一個交換區,用于對對象進行多次gc,當gc多次(臨界值 15)還存活的對象将被放入年老代。
1.2.2 To Survivor
minor gc時會先将eden區和from survivor區的對象進行回收,存活的對象将複制到to survivor區中,然後交換From Survivor和To Survivor的角色。
-
舊生代(Old Generation)
也叫年老代,存儲生命周期特别久或者大對象
-
永久代
jdk7之前對方法區的回收處理方式,jdk8之後用metaspace替換了
GC 順序
1> 剛建立的對象将放入Eden區
2> eden空間不足将進行gc, 第一次GC時,将掃描eden和From Survivor區域,将存活的對象複制到To Survivor ,然後互動From Survivor和To Survivor的角色。
To Survivor存儲gc後仍然存活的對象,交換後 -> From Survivor
From Survivor在gc時将存活的對象複制到了to Survivor,此時為空,交換後 -> From Survivor
3> 重複2步驟,當一個對象被多次gc後仍然存活,那麼将該對象放入年老代。
1.5.2 堆的配置參數
參數 | 描述 |
---|---|
-Xms | 堆記憶體初始大小 |
-Xmx(MaxHeapSize) | 堆記憶體最大允許大小,一般不要大于實體記憶體的80% |
-XX:NewSize(-Xns) | 年輕代記憶體初始大小 |
-XX:MaxNewSize(-Xmn) | 年輕代記憶體最大允許大小,也可以縮寫 |
-XX:NewRatio | 新生代和老年代的比值, 值為4 表示 新生代:老年代=1:4,即年輕代占堆的1/5 |
-XX:SurvivorRatio=8 | 年輕代中Eden區與Survivor區的容量比例值,預設為8, 表示兩個Survivor :eden=2:8,即一個Survivor占年輕代的1/10 |
-XX:+HeapDumpOnOutOfMemoryError | 記憶體溢出時,導出堆資訊到檔案 |
-XX:+HeapDumpPath | 堆Dump路徑 |
-XX:OnOutOfMemoryError | 當發生OOM記憶體溢出時,執行一個腳本 |
-XX:MaxTenuringThreshold=7 | 表示如果在幸存區移動多少次沒有被垃圾回收,進入老年代 |
1.6 直接記憶體
直接記憶體(Direct Memory)并不是虛拟機運作時資料區的一部分,也不是Java虛拟機規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導緻OutOfMemoryError 異常出現。
在 JDK 1.4 中新加入了 NIO 類,引入了一種基于通道(Channel)與緩沖區(Buffer)的 I/O方式,它可以使用 Native 函數庫直接配置設定堆外記憶體,然後通過一個存儲在 Java 堆裡的 DirectByteBuffer 對象作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java 堆和 Native 堆中來回複制資料。
1.7 詳細記憶體分布圖
1.8 執行引擎
将jvm虛拟機指令交給OS進行執行。執行引擎包含: 解釋器, 即時編譯器, 垃圾回收器。
2. 常量池詳細
常量池是為了避免頻繁的建立和銷毀對象而影響系統性能,其實作了對象的共享。
雙等号==的含義
基本資料類型之間應用雙等号,比較的是他們的數值。
複合資料類型(類)之間應用雙等号,比較的是他們在記憶體中的存放位址。
2.1 String 常量池
public class StringConstTest {
private static void demo1() {
// 字面值方式建立字元串
String str1 = "abc"; // 将在字元串常量池中尋找 "abc" 若沒有則建立字元串對象 "abc" 并存儲後将該對象的位址傳回給變量
String str2 = "abc"; // 若有直接從常量池中傳回 "abc" 的位址
System.out.println(str1 == str2); // 結果 true
}
}
public class StringConstTest {
private static void demo2() {
// 下面會直接生成 "ABCDEFG" 對象存儲到字元串常量池中 "ABC" "DEF" "G" 均不會建立
String str1 = "ABC" + "DEF" + "G";
String str2 = "ABCDEFG";
System.out.println(str1==str2); // true
}
}
public class StringConstTest {
private static void demo3() {
String str1 = "abc"; // 字元串常量池中将建立"abc"
// 将在堆中new一個對象傳回位址引用給str2,而new的這個對象中value[] 指向字元串常量池中的"abc"
// 這裡共建立了一個對象
String str2 = new String("abc");
// 字元串常量池中建立 "def" 堆中建立對象傳回引用給str3 這裡共建立了2個對象
String str3 = new String("def");
System.out.println(str1 == str2); // false
}
}
public class StringConstTest {
public static void demo4() {
String str1 = "a"; // 儲存在常量池中
String str2 = "b"; // 儲存在常量池中
final String _str1 = "a";
final String _str2 = "b";
String str3 = str1 + str2; // str1 str2 是變量 動态調用 生成新的對象
String _str3 = _str1 + _str2; // _str1 _str2 常量 編譯時替換為 "a" + "b"了 是以結果儲存在常量池了
String str4 = "ab"; // 常量池已經有了 取出引用指派
System.out.println(_str1 == str1); // true
System.out.println(str3 == str4); // false
System.out.println(_str3 == str4); // true
}
}
public class StringConstTest {
public static void demo4() {
String str1 = "a"; // 儲存在常量池中
String str2 = "b"; // 儲存在常量池中
final String _str1 = "a";
final String _str2 = "b";
String str3 = str1 + str2; // str1 str2 是變量 動态調用 生成新的對象
String _str3 = _str1 + _str2; // _str1 _str2 常量 編譯時替換為 "a" + "b"了 是以結果儲存在常量池了
String str4 = "ab"; // 常量池已經有了 取出引用指派
System.out.println(_str1 == str1); // true
System.out.println(str3 == str4); // false
System.out.println(_str3 == str4); // true
}
}
public class StringConstTest {
private static final String str1;
private static final String str2;
static {
str1 = "1";
str2 = "2";
}
public static void demo5() {
String str = "12"; // 存儲在常量池中
// 靜态變量在編譯期不會執行 是以可以看作是變量 運作時 動态調用 生成新的對象
String tmp = str1 + str2;
System.out.println(str == tmp); // false
}
}
public class StringConstTest {
public static void demo6() {
String str = "abcdef";
String str2 = "def";
String str3 = "abc" + str2; // str2變量 動态調用
String str4 = "abc" + new String("def"); // 常量池和堆中的引用做 + 會動态調用
System.out.println(str == str3); // false
System.out.println(str == str4); // false
}
}
public class StringConstTest {
public static void demo7() {
String str = "abc"; // 放入常量池中
// 現在常量池中找"abc" 找到的話直接傳回引用位址給str1 沒找到建立儲存到常量池中 并傳回引用位址
// intern() 運作時動态常量建立
String str1 = new String("abc").intern();
System.out.println(str == str1); // true
}
}
2.2 基本類型常量池
public class OtherConstTest {
public static void main(String[] args) {
int i = 101; // 字面值
Integer i1 = 101; // 傳回常量池中引用
Integer i2 = 101; // 傳回常量池中引用
Integer i3 = new Integer(101); // 建立新對象
System.out.println(i == i2); // 自動拆箱 進行字面值比較
System.out.println(i1 == i2); // true
System.out.println(i1 == i3); // false
int b = 128; // 字面值
Integer b1 = 128; // 自動裝箱建立新對象
Integer b2 = 128; // 自動裝箱建立新對象
System.out.println(b == b1); // 自動拆箱 進行字面值比較
System.out.println(b1 == b2); // false
}
}
4. jclasslib.jar 工具使用
4.1 下載下傳安裝 jclasslib.jar
自行下載下傳并安裝,該工具可以檢視.class檔案的具體位元組碼結構,和 javap -v 效果相同
或者在idea中安裝jclasslib插件也行
4.2 工具的使用
public class JvmDemo01 {
public int demo1(Object obj) {
byte b = 1;
short s = 2;
int i = 1;
long lo = 64L;
boolean flag = true;
float f = 3.14f;
double dou = 3.14159265;
char c = 'A';
for (int j = i; j < lo; j++) {
// 無
}
return i + b;
}
public static void main(String[] args) {
System.out.println("jvm 局部變量表");
}
}
4.2.1 局部變量占用槽
JVM - 記憶體模型JVM - 記憶體模型
4.2.2 局部變量資訊檢視
JVM - 記憶體模型JVM - 記憶體模型
4.2.3 操作數棧的運算
- iconst: 壓入棧中,int類型在-1~5
- bipush: 壓入棧中,int類型在-128~127
- sipush: 壓入棧中,int類型在-32768~32767
- ldc: 壓入棧中,int類型在-2147483648~2147483647
- ldc2_w: 壓入棧中,long double
- iload: 将一個局部變量加載到操縱棧
- istore,fstore,dstore: 将一個數值從操作數棧存儲到局部變量表
- i2l: 類型轉換 int -> long
- lcmp: 比較兩個long的大小
0 iconst_1 # byte b = 1 将1壓入操作數棧中
1 istore_2 # 彈出操作數棧棧頂元素,将 1 儲存到局部變量表索引為2的位置
2 iconst_2 # short s = 2 将2壓入操作數棧中
3 istore_3 # 彈出操作數棧棧頂元素,将 2 儲存到局部變量表索引為3的位置
4 iconst_1 # int i = 1 将1壓入操作數棧中
5 istore 4 # 彈出操作數棧棧頂元素,将 1 儲存到局部變量表索引為4的位置
7 ldc2_w #2 <64> # long lo = 64L 将64壓入操作數棧中
10 lstore 5 # 彈出操作數棧棧頂元素,将 64 儲存到局部變量表索引為5的位置
12 iconst_1 # boolean flag = true 将true壓入操作數棧中
13 istore 7 # 彈出操作數棧棧頂元素,将 1 儲存到局部變量表索引為7的位置 long占用兩個slot
15 ldc #4 <3.14> # float f = 3.14f 将3.14壓入操作數棧中
17 fstore 8 # 彈出操作數棧棧頂元素,将 3.14 儲存到局部變量表索引為8的位置
19 ldc2_w #5 <3.14159265> # 同上
22 dstore 9 # 同上
24 bipush 65 # 同上
26 istore 11 # 同上
28 iload 4 # 從局部變量表中複制索引為4的數值(即 int i = 1)到操作數棧中
30 istore 12 # 彈出操作數棧棧頂元素,将複制的這個值儲存到局部變量表索引為12的位置 其實就是j
32 iload 12 # 從局部變量表中複制索引為12的數值(即 1)到操作數棧中
34 i2l # 類型轉換 int -> long
35 lload 5 # 從局部變量表中複制索引為5的數值(即 long lo = 64L)到操作數棧中
37 lcmp # 比較兩個long類型的大小
38 ifge 47 (+9) # 如果大于0 即如果為真繼續 否則跳轉到47 也就是前進9步
41 iinc 12 by 1 # int類型變量增加指定的值 這裡 j++
44 goto 32 (-12) # 跳轉到32步 也就是後退12步
47 iload 4 # 載入局部變量索引為4的數值 int i = 1
49 iload_2 # 載入局部變量索引為4的數值 byte b = 1
50 iadd # 進行加法計算 入棧的i b 将出棧進行 + 運算 之後将結果入棧 即彈棧運算,然後結果壓棧
51 ireturn # 棧頂元素出棧傳回
附錄參照
- 指令詳細上
- 指令詳細下
- 虛拟機棧
- 方法區
- 常量池
- jvm記憶體結構
- 記憶體模型