JVM快速入門
前言
JVM是JavaVirtualMachine(Java虛擬機)的縮寫,JVM是一種用於計算設備的規範,它是一個虛構出來的電腦,是通過在實際的電腦上模擬模擬各種電腦功能來實現的。
理解JAVA虛擬機對我們學習Java有巨大的幫助,接下來我們來快速入門Java虛擬機,更深入的理解還需要大家在不斷地學習中探索。
註:本篇文章大部分參考於【狂神說Java】JVM快速入門篇
部分參考於尚矽谷的教程尚矽谷宋紅康JVM全套教程(詳解java虛擬機),如果問題,歡迎大家踴躍提出!
JVM常見面試題
- 請你談談你對JVM的理解?java8虛擬機和之前的變化更新?
- 什麼是OOM,什麼是棧溢出StackOverFlowError?怎麼分析?
- JVM的常用調優參數有哪些?
- 記憶體快照如何抓取,怎麼分析Dump文件?
- 談談JVM中,類載入器你的認識?
以下我們會對這些問題進行詳細的分析
接下來我們進入正題!!!
1、JVM的位置

2、JVM的體系結構
-
簡要過程

-
具體過程

3、類載入器
作用:載入Class文件

new一個對象的過程(對象實例化的過程)
public class Car {
public int age;
public static void main(String[] args) {
//類是模板,對象是具體的
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
car1.age = 1;
car2.age = 2;
car3.age = 3;
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class<? extends Car> aClass1 = car1.getClass();
Class<? extends Car> aClass2 = car1.getClass();
Class<? extends Car> aClass3 = car1.getClass();
System.out.println(aClass1.hashCode());
System.out.println(aClass2.hashCode());
System.out.println(aClass3.hashCode());
}
}
結果:

類載入器
1、虛擬機自帶的載入器
2、啟動類(根)載入器
3、擴展類載入器
4、應用程式(系統類)載入器
類載入器的常用方法
ClassLoader類,它是一個抽象類,其後所有的類載入器都繼承自ClassLoader(不包括啟動類載入器)
| 方法名稱 | 描述 |
|---|---|
| getParent() | 返回該類載入器的超類載入器 |
| loadClass(String name) | 載入名稱為name的類,返回結果為java.lang.Class類的實例 |
| findClass(String name) | 查找名稱為name的類,返回結果為java.lang.Class類的實例 |
| findLoadedClass(String name) | 查找名稱為name的已經被載入過的類,返回結果為java.lang.Class類的實例 |
| defineClass(String name,byte[] b,int off,int len) | 把位元組數組b中的內容轉換為一個Java類 ,返回結果為java.lang.Class類的實例 |
| resolveClass(Class<?> c) | 連接指定的一個java類 |
程式碼實例
public class Car {
public int age;
public static void main(String[] args) {
//類是模板,對象是具體的
Car car1 = new Car();
Car car2 = new Car();
Car car3 = new Car();
car1.age = 1;
car2.age = 2;
car3.age = 3;
System.out.println(car1.hashCode());
System.out.println(car2.hashCode());
System.out.println(car3.hashCode());
Class<? extends Car> aClass1 = car1.getClass();
ClassLoader classLoader = aClass1.getClassLoader();
System.out.println(classLoader); //AppClassLoader 應用程式(系統類)載入器
System.out.println(classLoader.getParent()); //ExtClassLoader 擴展類載入器
System.out.println(classLoader.getParent().getParent()); //null 1.不存在 2.java程式獲取不到
}
}
4、雙親委派機制
4.1、簡介:
Java虛擬機對class文件採用的是按需載入的方式,
也就是說當需要使用該類時才會將它的class文件載入到記憶體生成的class對象。
而且載入某個類的class文件時,java虛擬機採用的是雙親委派模式。
即把請求交由父類處理,它是一種任務委派模式
4.2、工作原理:

-
如果一個類載入器收到了類載入的請求,它並不會自己載入,而是先把請求委託給父類的載入器執行
-
如果父類載入器還有父類,則進一步向上委託,依次遞歸,請求到達最頂層的引導類載入器。
-
如果頂層類的載入器載入成功,則成功返回。如果失敗,則子載入器會嘗試載入。直到載入成功。
-
如果步驟3不成功,則就可能會報
Class Not Found
-
查看最頂層父類ClassLoader的loaderClass方法,我們可以驗證雙親委派機制。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先檢查此類是否被載入過了 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 調用父類的載入器方法 if (parent != null) { c = parent.loadClass(name, false); } else { // 此時是最頂級的啟動類載入器 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 拋出異常說明父類無法載入 } if (c == null) { //父類無法載入的時候,由子類進行載入。 // to find the class. long t1 = System.nanoTime(); c = findClass(name); //記錄載入時間已經載入耗時 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
4.3、雙親委派機制的優勢
-
避免類的重複載入
當自己程式中定義了一個和Java.lang包同名的類,此時,由於使用的是雙親委派機制,會由啟動類載入器去載入
JAVA_HOME/lib中的類,而不是載入用戶自定義的類。此時,程式可以正常編譯,但是自己定義的類無法被載入運行。 -
保護程式安全,防止核心API被隨意篡改
5、沙箱安全機制(了解)
5.1、定義
自定義String類,但是在載入自定義String類的時候會率先使用引導類載入器載入, 而引導類載入器在載入過程中會先載入jdk自帶的文件(rt.jar包中的java\lang\String.class), 報錯資訊說沒有main方法就是因為載入的是rt.jar包中的String類。 這樣可以保證對java核心源程式碼的保護,這就是沙箱安全機制.
5.2、類的主動使用和被動使用
java程式對類的使用方式分為:主動使用和被動使用
- 主動使用,分為七種情況
- 創建類的實例
- 訪問某各類或介面的靜態變數,或者對靜態變數賦值
- 調用類的靜態方法
- 反射 比如Class.forName(com.dsh.jvm.xxx)
- 初始化一個類的子類
- java虛擬機啟動時被標明為啟動類的類
- JDK 7 開始提供的動態語言支援:
- java.lang.invoke.MethodHandle實例的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化
- 除了以上七種情況,其他使用java類的方式都被看作是對類的被動使用,都不會導致類的初始化。
6、Native、方法區
Native

public class Demo {
public static void main(String[] args) {
new Thread(() ->{
},"my thread name").start();
}
//native:凡是帶了native關鍵字的,說明java的作用範圍達不到了,會去調用底層C語言的庫
//會進入本地方法棧,會調用本地方法介面,JNI
//JNI(JavaNativeInterface)的作用:擴展Java的使用,融合不同的程式語言為Java所用(像C,C++)
//它在記憶體區域中專門開闢了一塊標記區域:Native Method Stack,登記 native 方法
//在最終執行的時候,通過JNI載入本地方法庫中的方法
//Java程式驅動印表機,管理系統,掌握即可,在企業級應用中較為少見
private native void start0();
}
- start()方法
// Thread類中的start方法,底層是把執行緒加入到執行緒組,然後去調用本地方法start0
public class Thread implements Runnable {
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
}
程式計數器
程式計數器:Program Counter Register
每個執行緒都有一個程式計數器,是執行緒私有的,就是一個指針,指向方法區中的方法位元組碼(用來存儲指向像一條指令的地址,也即將要執行的指令程式碼),在執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不計
方法區

Method Area方法區(此區域屬於共享區間,所有定義的方法的資訊都保存在該區域)
方法區是被所有執行緒共享,所有欄位、方法位元組碼、以及一些特殊方法(如構造函數,介面程式碼)也在此定義。
靜態變數、常量、類資訊(構造方法、介面定義)、運行時的常量池存在方法區中,但是實例變數存在堆記憶體中,和方法區無關。
static final Class 常量池
7、棧
7.1、棧的作用
棧記憶體,主管程式的運行,生命周期和執行緒同步;
執行緒結束,棧記憶體也就釋放了,對於棧來說,不存在垃圾回收問題。
7.2、棧存儲的東西
8大基本類型、對象引用,實例的方法
7.3、棧運行原理
簡單結構圖

詳細結構圖

棧+堆+方法區的交互關係

8、堆
8.1、三種JVM(了解)
-
Sun公司
HotSpot(常用)

-
BEA
JRockit -
IBM
J9VM
8.2、堆
Heap,一個JVM只有一個堆記憶體,堆記憶體的大小是可以調節的。
類載入器讀取了類文件後,一般會把什麼東西放到堆中?
類、方法、常量、變數、保存我們所有引用類型的真實對象。
堆記憶體中細分為三個區域:
- 新生區(伊甸園區)Young/New
- 養老區 old
- 永久區 Perm

GC垃圾回收,主要是在新生區和老年區
假設記憶體滿了,OOM,堆記憶體不夠!
新生區
-
類:誕生和成長的地方,甚至是死亡
-
伊甸園,所有對象都是在伊甸園區new出來的
-
倖存者區(from,to動態交換)
老年區
老年區是新生區剩下來的被淘汰的了(沒有被殺死)
永久區
(1)什麼是永久代和元空間??
方法區是一種規範,不同的虛擬機廠商可以基於規範做出不同的實現,永久代和元空間就是出於不同jdk版本的實現。
方法區就像是一個介面,永久代與元空間分別是兩個不同的實現類。
只不過永久代是這個介面最初的實現類,後來這個介面一直進行變更,直到最後徹底廢棄這個實現類,由新實現類—元空間進行替代。
jdk1.8之前:永久代

jdk1.8之後:元空間(邏輯上存在,物理上不存在)

(2)常量池
-
jdk1.6之前:永久代,運行時常量池+字元串常量池是存放在方法區中,HotSpot VM對方法區的實現稱為永久代。

-
jdk1.7:永久代,但是慢慢退化了,
去永久代,字元串常量池從方法區移到堆中,運行時常量池保留在方法區中
-
jdk1.8之後:無永久代,常量池在元空間,字元串常量池保留在堆中,運行時常量池保留在方法區中,只是實現不一樣了,JVM記憶體變成了直接記憶體。

(3)永久區
這個區域是常駐記憶體的。
用來存放JDK自身攜帶的Class對象、Interface元數據,存儲的是Java運行時的一些環境或類資訊。
這個區域不存在垃圾回收。
關閉JVM虛擬機就會釋放這個區域的記憶體。
什麼情況下,在永久區就崩了?
- 一個啟動類,載入了大量的第三方jar包。
- Tomcat部署了太多的應用。
- 大量動態生成的反射類;不斷的被載入,直到記憶體滿,就會出現OOM
9、堆記憶體調優
修改jvm參數
常見的jvm參數:
-
標準參數選項
-d32 使用 32 位數據模型 (如果可用) -d64 使用 64 位數據模型 (如果可用) -server 選擇 "server" VM 默認 VM 是 server. -cp <目錄和 zip/jar 文件的類搜索路徑> -classpath <目錄和 zip/jar 文件的類搜索路徑> 用 ; 分隔的目錄, JAR 檔案 和 ZIP 檔案列表, 用於搜索類文件。 -D<名稱>=<值> 設置系統屬性 -verbose:[class|gc|jni] 啟用詳細輸出 -version 輸出產品版本並退出 -version:<值> 警告: 此功能已過時, 將在 未來發行版中刪除。 需要指定的版本才能運行 -showversion 輸出產品版本並繼續 -jre-restrict-search | -no-jre-restrict-search 警告: 此功能已過時, 將在 未來發行版中刪除。 在版本搜索中包括/排除用戶專用 JRE -? -help 輸出此幫助消息 -X 輸出非標準選項的幫助 -ea[:<packagename>...|:<classname>] -enableassertions[:<packagename>...|:<classname>] 按指定的粒度啟用斷言 -da[:<packagename>...|:<classname>] -disableassertions[:<packagename>...|:<classname>] 禁用具有指定粒度的斷言 -esa | -enablesystemassertions 啟用系統斷言 -dsa | -disablesystemassertions 禁用系統斷言 -agentlib:<libname>[=<選項>] 載入本機代理庫 <libname>, 例如 -agentlib:hprof 另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help -agentpath:<pathname>[=<選項>] 按完整路徑名載入本機代理庫 -javaagent:<jarpath>[=<選項>] 載入 Java 程式語言代理, 請參閱 java.lang.instrument -splash:<imagepath> 使用指定的影像顯示啟動螢幕 -
-X參數選項
-Xmixed 混合模式執行(默認) -Xint 僅解釋模式執行 -Xbootclasspath:<用 ; 分隔的目錄和 zip/jar 文件> 設置引導類和資源的搜索路徑 -Xbootclasspath/a:<用 ; 分隔的目錄和 zip/jar 文件> 附加在引導類路徑末尾 -Xbootclasspath/p:<用 ; 分隔的目錄和 zip/jar 文件> 置於引導類路徑之前 -Xdiag 顯示附加診斷消息 -Xnoclassgc 禁用類垃圾收集 -Xincgc 啟用增量垃圾收集 -Xloggc:<file> 將 GC 狀態記錄在文件中(帶時間戳) -Xbatch 禁用後台編譯 -Xms<size> 設置初始 Java 堆大小 -Xmx<size> 設置最大 Java 堆大小 -Xss<size> 設置 Java 執行緒堆棧大小 -Xprof 輸出 cpu 分析數據 -Xfuture 啟用最嚴格的檢查,預計會成為將來的默認值 -Xrs 減少 Java/VM 對作業系統訊號的使用(請參閱文檔) -Xcheck:jni 對 JNI 函數執行其他檢查 -Xshare:off 不嘗試使用共享類數據 -Xshare:auto 在可能的情況下使用共享類數據(默認) -Xshare:on 要求使用共享類數據,否則將失敗。 -XshowSettings 顯示所有設置並繼續 -XshowSettings:system (僅限 Linux)顯示系統或容器 配置並繼續 -XshowSettings:all 顯示所有設置並繼續 -XshowSettings:vm 顯示所有與 vm 相關的設置並繼續 -XshowSettings:properties 顯示所有屬性設置並繼續 -XshowSettings:locale 顯示所有與區域設置相關的設置並繼續 -
列印設置的參數
-
-XX:+PrintCommandLineFlags 表示程式運行前列印出JVM參數
-
-XX:+PrintFlagsInitial 表示列印出所有參數的默認值
-
-XX:+PrintFlagsFinal 列印出最終的參數值
-
-XX:+PrintVMOptions 列印JVM的參數
-
-
棧
- -Xss128k
-
堆
-
-Xms600m 設置堆的初始大小
-
-Xmx600m 設置堆的最大大小
-
-XX:NewSize=1024m 設置年輕代的初始大小
-
-XX:MaxNewSize=1024m 設置年輕代的最大值
-
-XX:SurvivorRatio=8 伊甸園和倖存者的比例
-
-XX:NewRatio=4 設置老年代和新生代的比例
-
-XX:MaxTenuringThreshold=15 設置晉陞老年代的年齡條件
-
-
方法區
-
永久代
- -XX:PermSize=256m 設置永久代初始大小
- -XX:MaxPernSize=256m 設置永久代的最大大小
-
元空間
- -XX:MetasapceSize=256m 設置初始元空間大小
- -XX:MaxMatespaceSize=256m 設置最大元空間大小 默認無限制
-
-
直接記憶體
- -XX:MaxDirectMemorySize 設置直接記憶體的容量,默認與堆最大值一樣。
我們來看看JVM所佔的記憶體
public class Demo02 {
public static void main(String[] args) {
//返回虛擬機試圖使用的最大記憶體
long maxMemory = Runtime.getRuntime().maxMemory();//位元組 1024*1024=1M
//返回JVM初始化的總記憶體
long totalMemory = Runtime.getRuntime().totalMemory();
System.out.println("maxMemory="+maxMemory+"位元組\t"+(maxMemory/(double)1024/1024)+"MB");
System.out.println("totalMemory="+maxMemory+"位元組\t"+(totalMemory/(double)1024/1024)+"MB");
// 默認情況下:分配的最大記憶體是電腦記憶體的1/4;初始化的記憶體是電腦記憶體的1/64
// 分析OOM:
// 1.嘗試擴大堆記憶體,看結果
// 2.分析記憶體,看一下哪個地方出現了問題(專業工具)JProfiler
//手動調參:
//-Xms設置堆的最小空間大小。
//-Xmx設置堆的最大空間大小。
// -Xms1024m -Xmx1024m -XX:+PrintGCDetails
// 305664K+699392K = 1005056K 981.5M(新生區加老年區,物理上不存在元空間)
}
}
結果:

- 接下來我們手動修改參數

再運行程式查看結果

-
我們來看一個OOM錯誤
public class Test { public static void main(String[] args) { String str = "sdagnvsihlnvuxjvns"; while (true){ str += str + new Random().nextInt(888888888) + new Random().nextInt(888888888); } } }修改jvm參數-Xms8m -Xmx8m -XX:+PrintGCDetails會更快發生OOM錯誤

使用JProfiler工具分析OOM原因
在一個項目中,突然出現了OOM故障OutOfMemoryError,那麼該如何排除,研究為什麼出錯?
-
能夠看到程式碼第幾行出錯:記憶體快照分析工具,MAT,Jprofiler
-
Dubug,一行行分析程式碼!
MAT,Jprofiler作用:
- 分析Dump記憶體文件,快速定位記憶體泄露;
- 獲得堆中的數據
- 獲得大的對象
- ...
實例分析(在idea中要下載Jprofiler插件並在官網下載Jprofiler):
public class Demo03 {
byte[] array = new byte[1024*1024];
public static void main(String[] args) {
ArrayList<Demo03> list = new ArrayList<>();
int count = 0;
try {
while (true){
list.add(new Demo03());
count = count + 1;
}
}catch (Error e){
System.out.println("count:"+count);
e.printStackTrace();
}
}
}


註:使用Jprofiler查看完分析文件後記得刪除文件,因為會產生大量的分析文件佔用存儲空間
10、GC
10.1、垃圾回收的作用區域

JVM在進行GC時,並不是對這三個區域統一回收。大部分時候,回收都是新生代~
-
新生代
-
倖存區(form,to)
-
老年區
GC兩種類:輕GC(普通的GC),重GC(全局GC)
題目:
- JVM的記憶體模型和分區~詳細到每個區放什麼?
- 堆裡面的分區有哪些?Eden,from,to,老年區,說說他們的特點
- GC的演算法有哪些?標記清除法,標記壓縮,複製演算法,引用計數器
- 輕GC,重GC分別在什麼時候發生?
10.2、引用計數法(不高效)

10.3、複製演算法
複製演算法簡述:

複製演算法輕GC流程:

複製演算法小結:
- 好處:沒有記憶體的碎片。
- 壞處:浪費了記憶體空間(多了一半空間to永遠是空)。假設對象100%存活(極端情況(全部複製)),不適合使用複製演算法
- 複製演算法最佳使用場景:對象存活度較低的時候(新生區)
10.4、標記壓縮清除演算法
標記清除

- 優點:不需要額外的空間。
- 缺點:兩次掃描,嚴重浪費時間,會產生記憶體碎片
標記壓縮

標記清除壓縮
可以進行多次標記清除,再進行一次壓縮
10.4、GC演算法總結
- 記憶體效率:複製演算法>標記清除演算法>標記壓縮演算法(時間複雜度)
- 記憶體整齊度:複製演算法=標記壓縮演算法>標記清除演算法
- 記憶體利用率:標記壓縮演算法=標記清除演算法>複製演算法
思考一個問題:難道沒有最優演算法嗎?
答案:沒有,沒有最好的演算法,只有最合適的演算法——>GC:分代收集演算法
年輕代:
- 存活率低
- 複製演算法
老年代:
- 區域大:存活率高
- 標記清除(記憶體碎片不是太多)+標記壓縮混合實現
11、JMM
11.1、 JMM是什麼
JMM(Java Memory Model),Java的記憶體模型。
JVM虛擬機規範中曾經試圖定義一種Java記憶體模型,來屏蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平台下都可以達到一致性的記憶體訪問效果。
11.2、 JMM的作用
快取一致性的協議,用來定義數據讀寫的規則。
JMM定義了執行緒工作記憶體和主記憶體的抽象關係:執行緒的共享變數存儲在主記憶體中,每個執行緒都有一個私有的本地工作記憶體。

使用volatile關鍵字來解決共享變數的可見性的問題。
Java記憶體模型是圍繞著並發編程中原子性、可見性、有序性這三個特徵來建立的。
11.3、 JMM的操作

JMM定義了8種操作來完成(每一種操作都是原子的、不可再拆分的)。
- lock(鎖定):作用於主記憶體的變數,它把一個變數標識為一條執行緒獨佔的狀態。
- unlock(解鎖):作用於主記憶體的變數,它把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
- read(讀取):作用於主記憶體的變數,它把一個變數的值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用。
- load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
- use(使用):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳遞給執行引擎(每當虛擬機遇到一個需要使用到該變數的值的位元組碼指令時將會執行這個操作)。
- assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦給工作記憶體的變數(每當虛擬機遇到一個給該變數賦值的位元組碼指令時執行這個操作)。
- store(存儲):作用於工作記憶體的變數,它把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用。
- write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中得到的變數的值放入主記憶體的變數中。
11.4、 JMM定義的規則
8種操作必須滿足的規則:
- 不允許read和load、store和write操作之一單獨出現。(不允許一個變數從主記憶體讀取了但工作記憶體不接受;或者從工作記憶體發起回寫了但主記憶體不接受的情況出現)
- 不允許一個執行緒丟棄它的最近的assign操作。(變數在工作記憶體中改變了值之後,必須把該變化同步回主記憶體)
- 不允許一個執行緒無原因地(沒有發生過任何assign操作)把數據從執行緒的工作記憶體同步回主記憶體。
- 一個新的變數只能在主記憶體中「誕生」,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。(就是對一個變數實施use、store操作之前,必須先執行過了load和assign操作)
- 一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。
- 如果對一個變數執行lock操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值。
- 如果一個變數事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他執行緒鎖定住的變數。
- 對一個變數執行unlock操作之前,必須先把此變數同步回主記憶體中(執行store、write操作)。
11.5、 並發編程的三大特性
- 原子性
一個或多個程式指令,要麼全部正確執行完畢不能被打斷,或者全部不執行
- 可見性
當一個執行緒修改了某個共享變數的值,其它執行緒應當能夠立即看到修改後的值。
- 有序性
程式執行程式碼指令的順序應當保證按照程式指定的順序執行,即便是編譯優化,也應當保證程式源語一致。
12、總結
這裡有一張思維導圖基本涵蓋了Java虛擬機的內容,大家可以通過這張圖來鞏固上面學習的知識

到這裡Java虛擬機的快速入門就結束了,希望大家都有所收穫!








