JVM學習之 內存結構
一、引言
1.什麼是JVM?
- 定義:Java Virtual Machine – java 程序的運行環境(java 二進制位元組碼的運行環境)
- 好處:
- 一次編寫,到處運行
- 自動內存管理, 垃圾回收功能
- 數組下標越界檢查
- 多態
- 比較jvm、jre、jdk
2.學習JVM有什麼用
- 理解底層的實現原理
- 中高級程序員的必備技能
3.常見的JVM
4.學習路線
二、內存結構
1. 程序計數器
1.1 定義
Program Counter Register 程序計數器(寄存器)
在物理上:位於寄存器
作用:是記住下一條jvm指令的執行地址
特點:
- 是線程私有的
- 不會存在內存溢出
1.2作用
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
- 解釋器會解釋指令為機器碼交給 cpu 執行,程序計數器會記錄下一條指令的地址行號,這樣下一次解釋器會從程序計數器拿到指令然後進行解釋執行。
- 多線程的環境下,如果兩個線程發生了上下文切換,那麼程序計數器會記錄線程下一行指令的地址行號,以便於接着往下執行。
2. 虛擬機棧
2.1定義
Java Virtual Machine Stacks (Java 虛擬機棧)
- 每個線程運行時所需要的內存,稱為虛擬機棧
- 每個棧由多個棧幀(Frame)組成,對應着每次方法調用時所佔用的內存
- 每個線程只能有一個活動棧幀,對應着當前正在執行的那個方法
- 問題辨析
-
垃圾回收是否涉及棧內存?
棧內存並不涉及垃圾回收,棧內存的產生就是方法一次一次調用產生的棧幀內存,而棧幀內存在每次方法被調用後都會被彈出棧,自動就被回收掉,不需要垃圾回收。來管理 -
棧內存分配越大越好嗎?
不是,在線程不多的情況下,棧內存分配大在遞歸時能提高運行速度,但他會影響線程的數目,從而影響到整個系統的運行速度 -
方法內的局部變量是否線程安全?
如果方法內局部變量沒有逃離方法的作用訪問,它是線程安全的 如果是局部變量引用了對象,並逃離方法的作用範圍,需要考慮線程安全,如果是共享的需要考慮線程安全,如果是私有的不用考慮線程安全
2.2棧內存溢出
- 棧幀過多導致棧內存溢出 —->一般遞歸的時候容易出現
- 棧幀內存過大導致棧內存溢出 —>不易出現
- StackOverflowError 棧內存溢出異常
- @JsonIgnore
2.3線程運行診斷
案例1:cpu佔用過多
定位
- 用top定位哪個進程對cpu的佔用過高
- ps H -eo pid,tid,%cpu | grep 進程id (用ps命令進一步定位是哪個線程引起的cpu佔用過高)
- jstack 進程id
可以根據線程id 找到有問題的線程,進一步定位到問題代碼的源碼行號
3. 本地方法棧
一些帶有 native 關鍵字的方法就是需要 JAVA 去調用本地的C或者C++方法,因為 JAVA 有時候沒法直接和操作系統底層交互,所以需要用到本地方法棧,服務於帶 native 關鍵字的方法。
為本地的方法提供一個運行的空間
4. 堆
4.1定義
Heap 堆
- 通過 new 關鍵字,創建對象都會使用堆內存
特點 - 它是線程共享的,堆中對象都需要考慮線程安全的問題
- 有垃圾回收機制
4.2堆內存溢出
- jps 工具
查看當前系統中有哪些 java 進程 - jmap 工具
查看堆內存佔用情況 jmap – heap 進程id - jconsole 工具
圖形界面的,多功能的監測工具,可以連續監測 - jvisualvm 工具
5. 方法區
5.1方法區
Java 虛擬機有一個在所有 Java 虛擬機線程之間共享的方法區。方法區類似於傳統語言的編譯代碼的存儲區,或者類似於操作系統進程中的「文本」段。它存儲每個類的結構,例如運行時常量池、字段和方法數據,以及方法和構造函數的代碼,包括類和實例初始化以及接口初始化中使用 的特殊方法。
方法區是在虛擬機啟動時創建的。儘管方法區在邏輯上是堆的一部分,但簡單的實現可能會選擇不進行垃圾收集或壓縮它。本規範不要求方法區域的位置或用於管理已編譯代碼的策略。方法區域可以是固定大小,也可以根據計算需要擴大,如果不需要更大的方法區域,可以縮小。方法區的內存不需要是連續的。
Java 虛擬機實現可以為程序員或用戶提供對方法區域初始大小的控制,以及在方法區域大小可變的情況下,對最大和最小方法區域大小的控制。
以下異常情況與方法區相關:
如果方法區域中的內存無法滿足分配請求,Java 虛擬機將拋出一個OutOfMemoryError.
JVM規範-方法區定義
5.2組成
5.3方法區內存溢出
-
1.8 以前會導致永久代內存溢出
演示永久代內存溢出 java.lang.OutOfMemoryError: PermGen space
-XX:MaxPermSize=8m -
1.8 之後會導致元空間內存溢出
演示元空間內存溢出 java.lang.OutOfMemoryError: Metaspace
-XX:MaxMetaspaceSize=8m
場景:
spring
mybatis
5.4 運行時常量池
// 二進制位元組碼(類基本信息,常量池,類方法定義,包含了虛擬機指令)
public class Test {
public static void main(String[] args) {
System.out.println("hello world");
}
}
然後使用 javap -v Test.class 命令反編譯查看結果:
每條指令都會對應常量池表中一個地址,常量池表中的地址可能對應着一個類名、方法名、參數類型等信息。
- 常量池,就是一張表,虛擬機指令根據這張常量表找到要執行的類名、方法名、參數類型、字面量
等信息 - 運行時常量池,常量池是 *.class 文件中的,當該類被加載,它的常量池信息就會放入運行時常量 池,並把裏面的符號地址變為真實地址
5.5 StringTable
面試題:
String s1 = "a";
String s2 = "b";
String s3 = "a" + "b"; // ab
String s4 = s1 + s2; // new String("ab")
String s5 = "ab";
String s6 = s4.intern();
// 問
System.out.println(s3 == s4); // false
System.out.println(s3 == s5); // true
System.out.println(s3 == s6); // true
String x2 = new String("c") + new String("d"); // new String("cd")
x2.intern();
String x1 = "cd";
// 問,如果調換了【最後兩行代碼】的位置呢,如果是jdk1.6呢
System.out.println(x1 == x2);
// jdk1.6:
// String x1 = "cd"; x2.intern();
// x2.intern(); false String x1 = "cd"; ture
// jdk1.8:
// String x1 = "cd"; x2.intern();
// x2.intern(); false String x1 = "cd"; ture
練習:
// StringTable [ "a", "b" ,"ab" ] hashtable 結構,不能擴容
public class Demo1_22 {
// 常量池中的信息,都會被加載到運行時常量池中, 這時 a b ab 都是常量池中的符號,還沒有變為 java 字符串對象
// ldc #2 會把 a 符號變為 "a" 字符串對象
// ldc #3 會把 b 符號變為 "b" 字符串對象
// ldc #4 會把 ab 符號變為 "ab" 字符串對象
public static void main(String[] args) {
String s1 = "a"; // 懶惰的
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString() new String("ab")
String s5 = "a" + "b"; // javac 在編譯期間的優化,結果已經在編譯期確定為ab
System.out.println(s3 == s4);//s3是在串池中的,而s4則是在堆中,所有不相等
System.out.println(s3 == s5);// true
}
}
使用javap -v Demo1_22.class命令
5.6 StringTable的特性
- 常量池中的字符串僅是符號,第一次用到時才變為對象
- 利用串池的機制,來避免重複創建字符串對象
- 字符串變量拼接的原理是 StringBuilder (1.8)
- 字符串常量拼接的原理是編譯期優化
- 可以使用 intern 方法,主動將串池中還沒有的字符串對象放入串池
1.8 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有則放入串池, 會把串 池中的對象的引用返回
1.6 將這個字符串對象嘗試放入串池,如果有則並不會放入,如果沒有會把此對象複製一份, 放入串池, 會把串池中的對象返回
5.7 StringTable 位置
jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。
5.8 StringTable 垃圾回收
-Xmx10m 指定堆內存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次數,耗費時間等信息
演示StingTable垃圾回收:
public static void main(String[] args) throws InterruptedException {
int i = 0;
try {
for (int j = 0; j < 100000; j++) { // j=100, j=10000
String.valueOf(j).intern();
i++;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
5.9 StringTable 性能調優
調整 -XX:StringTableSize=桶個數
考慮將字符串對象是否入池
6. 直接內存
6.1 定義
Direct Memory
- 常見於 NIO 操作時,用於數據緩衝區
- 分配回收成本較高,但讀寫性能高
- 不受 JVM 內存回收管理
文件讀寫過程(IO):
因為 java 不能直接操作文件管理,需要切換到內核態,使用本地方法進行操作,然後讀取磁盤文件,會在系統內存中創建一個緩衝區,將數據讀到系統緩衝區, 然後在將系統緩衝區數據,複製到 java 堆內存中。缺點是數據存儲了兩份,在系統內存中有一份,java 堆中有一份,造成了不必要的複製。
使用了 DirectBuffer 文件讀取流程:
直接內存是操作系統和 Java 代碼都可以訪問的一塊區域,無需將代碼從系統內存複製到 Java 堆內存,從而提高了效率。
6.2 分配和回收原理
- 使用了 Unsafe 對象完成直接內存的分配回收,並且回收需要主動調用 freeMemory 方法
- ByteBuffer 的實現類內部,使用了 Cleaner (虛引用)來監測 ByteBuffer 對象,一但ByteBuffer 對象被垃圾回收,那麼就會由 ReferenceHandler 線程通過 Cleaner 的 clean 方法調用 freeMemory方法 來釋放直接內存。