前言
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錯誤
修改jvm參數-Xms8m -Xmx8m -XX:+PrintGCDetails會更快發生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); } } }
使用JProfiler工具分析OOM原因
在一個項目中,突然出現了OOM故障
OutOfMemoryError
,那麼該如何排除,研究為什麼出錯?
- 能夠看到代碼第幾行出錯:記憶體快照分析工具,MAT,Jprofiler
- Dubug,一行行分析代碼!
MAT,Jprofiler作用:
- 分析Dump記憶體檔案,快速定位記憶體洩露;
- 獲得堆中的資料
- 獲得大的對象
- ...
具體使用Jprofile流程
執行個體分析(在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虛拟機的快速入門就結束了,希望大家都有所收獲!