JVM 是 Java 程序運行基礎(chǔ),面試時一定會遇到 JVM 相關(guān)的題。本文會先對面試中 JVM 的考察點進行匯總介紹。然后對 JVM 內(nèi)存模型、Java 的類加載機制、常用的 GC 算法這三個知識點進行詳細講解。
1. JVM知識點匯總

如上圖所示,JVM 知識點有 6 個大方向,其中,內(nèi)存模型、類加載機制、GC 垃圾回收是比較重點的內(nèi)容。性能調(diào)優(yōu)部分偏重實際應(yīng)用,重點突出實踐能力。編譯器優(yōu)化和執(zhí)行模式部分偏重理論基礎(chǔ),主要掌握知識點。
在開始下文前,看下你是否能夠回答以下知識點:
- 內(nèi)存模型:程序計數(shù)器、方法區(qū)、堆、棧、本地方法棧的作用,保存哪些數(shù)據(jù);
- 類加載:雙親委派的加載機制,以及常用類加載器分別加載哪種類型的類;
- GC:堆內(nèi)存劃分,分代回收的思想和依據(jù),以及不同垃圾回收算法實現(xiàn)的思路、適合的場景;
- JVM調(diào)優(yōu):常用的JVM優(yōu)化參數(shù)的作用,參數(shù)調(diào)優(yōu)的依據(jù),常用的JVM分析工具分析哪類問題及適用方法;
- 執(zhí)行模式
- 編譯器優(yōu)化
2. JVM 內(nèi)存模型
JVM內(nèi)存模型主要指運行時的數(shù)據(jù)區(qū),包括如下5個部分

- 棧:
也叫方法棧,線程私有,線程在執(zhí)行每個方法時,都會創(chuàng)建一個棧楨,用于存儲整個執(zhí)行過程和狀態(tài);調(diào)用方法時執(zhí)行入棧,方法返回時執(zhí)行出棧; - 本地方法棧:
和方法棧類似,不同的時,執(zhí)行Java方法使用的是棧,而執(zhí)行native方法時使用的是本地方法棧; - 程序計數(shù)器:
當(dāng)前線程執(zhí)行字節(jié)碼的行號指示器,通過它可以知道下一條要執(zhí)行的指令,每個線程獨占互不影響,保證線程切換后能恢復(fù)到正確的執(zhí)行位置; - 堆:
JVM管理的內(nèi)存中最大的一塊,存放的是對象實例;根據(jù)對象存活的周期不同,JVM把堆內(nèi)存進行分帶管理,由垃圾收集器進行對象的回收管理; - 方法區(qū):
存儲已被虛擬機加載的類信息、常量、靜態(tài)變量等數(shù)據(jù);JDK8之前使用堆上的永久代作為方法區(qū),而JDK8使用元空間(Meta-space)來代替;運行時常量池是方法區(qū)的一部分,用于存放編譯期生成的各種字面量與符號引用(類被加載時觸發(fā)),字符串常量池也在方法區(qū)中;
注意:Class對象(Class.forName)是放在堆中的,而不是方法區(qū),class對象是生成的最終實例,一切實例對象都放在堆中,方法區(qū)是存儲Class的基本信息;
3. 類加載機制
類的加載過程是指將編譯好的class類文件的字節(jié)碼讀入到內(nèi)存中,將其存在方法區(qū)并創(chuàng)建對應(yīng)的Class對象;類的加載分為加載、鏈接、初始化,其中鏈接又包含驗證、準備、解析三步,如圖所示

- 加載
1)通過類的全限定名獲取定義此類的二進制字節(jié)流(類加載器做的事);
2)將字節(jié)流說代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu);
3)在內(nèi)存中創(chuàng)建這個類的Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口; - 驗證
- 準備
- 解析
- 初始化
主要完成靜態(tài)代碼塊的執(zhí)行和靜態(tài)變量的賦值,只有對類主動使用時,才進行初始化;初始化的觸發(fā)條件:
1)創(chuàng)建類的實例
2)訪問類的靜態(tài)變量或靜態(tài)方法
3)Class.forName反射類
4)某個子類被初始化 - 卸載
當(dāng)類對象(注意不是類的實例)不再被使用時是會被GC卸載回收的,需要注意的時JVM自帶的三個類加載器加載的類在虛擬機的整個生命周期中是不會被卸載的,只有用戶自定義的類加載器加載的類才會被卸載;
3.1 類加載器
如下圖,JVM自帶的三個類加載器分別是:BootStrap啟動類加載器、擴展類加載器、應(yīng)用加載器;以及分別對應(yīng)的加載目錄;

雙親委派
java的類加載使用雙親委派模式,即一個類加載器在加載類時,會遞歸的委托給父類加載器去執(zhí)行,直至頂層的啟動類加載器,如果父類加載器能夠加載,則返回成功,否則子加載器才會自己嘗試加載;
對于兩個不同的類加載器(自定義的、沒有繼承關(guān)系),加載同一個類,會導(dǎo)致兩個類不等;
雙親委派的好處
- 避免重復(fù)加載
- 防止對JDK核心類進行篡改,比如String.class由啟動類加載器加載,如果想篡改String類,那么不會生效;
4. GC
4.1 對象已死?
判定對象的存活都與“引用”有關(guān),有兩種方法去判斷一個對象已經(jīng)“死了”;
- 引用計數(shù)算法
已經(jīng)被淘汰的算法,通過添加一個引用計數(shù)器來判斷對象是否還在被引用,解決不了循環(huán)引用的問題; - 可達性分析算法
從GC roots往下搜索,所走的路徑叫引用鏈,如果有有對象沒有與引用鏈相連的話,證明對象是不可用的;GC roots包括:
棧中引用的對象、方法區(qū)靜態(tài)引用指向的對象、方法區(qū)常量引用指向的對象、本地方法棧Native引用的對象
再談引用
如果一個對象只被定義為“被引用”或者“未被引用”兩種狀態(tài),那么對于一些“食之無味,棄之可惜”的對象就無能無力;由此產(chǎn)生了四種引用類型:
- 強引用:傳統(tǒng)“引用”的定義,只要被強引用關(guān)聯(lián)的對象永遠不會被回收;
- 軟引用:內(nèi)存不夠時被回收;
- 弱引用:比軟引用更弱,下一次垃圾收集時被回收;
- 虛引用:最弱,用來跟蹤對象被垃圾回收的活動(對象被回收時收到一個通知);
4.2 分代回收
JVM的堆內(nèi)存被分代管理,包括新生代和老年代,這樣做主要是為了兼顧垃圾收集的時間開銷和內(nèi)存的空間有效利用;大部分對象很快就不再使用;
- Minor GC:對新生代的對象的收集;
- Major GC:對舊生代的對象的收集,出現(xiàn)Major GC通常會出現(xiàn)至少一次Minor GC;
- Full GC:全局范圍的GC,程序中主動調(diào)用System.gc()強制執(zhí)行的GC;出發(fā) Full GC的條件有:當(dāng)年輕代晉升到老年代放不下時、老年代使用率超過閾值、永久代/元空間不足時、System.gc();

新生代區(qū)分為3個部分:1個eden區(qū)、2個Survivor區(qū)(from和to,復(fù)制算法),新創(chuàng)建的對象都會被分到Eden區(qū),這些對象經(jīng)過一次Minor GC后,如果仍然存活,則會被分配到Survivor區(qū),然后在Survivor區(qū)每熬過一次Minor GC后年齡就會增長一歲,達到一定年齡后,就被移動到老年代中。
- 詳細過程:
GC開始前,對象只會存在于Eden區(qū)和from區(qū),to是空的,當(dāng)GC開始時,Eden中所有存活的對象都會被移動到To里,而from區(qū)域中仍然存活的對象會根據(jù)年齡來決定去向,年齡達到閥值的,則移動到老年區(qū),沒有的移動到to區(qū)域,經(jīng)過gc后,eden和from區(qū)域被清空,然后from和to對換,保證to區(qū)域為空。Minor GC會一直重復(fù)這樣的過程,直到“To”區(qū)被填滿,“To”區(qū)被填滿之后,會將所有對象移動到年老代中。
4.3 垃圾回收算法
- 標(biāo)記-清除
老年代常用回收算法;最基本的算法,兩個階段:先標(biāo)記要回收的對象,然后一次性回收
缺點:效率低,清除后會產(chǎn)生大量的內(nèi)存碎片(空間碎片太多可能會導(dǎo)致當(dāng)程序需要分配大對象時無法找到連續(xù)的內(nèi)存而不得不提前觸發(fā)一次GC); - 復(fù)制算法
年輕代常用回收算法;把內(nèi)存劃分為兩等分,只使用其中一個區(qū)域,垃圾回收時,將使用區(qū)域里存活的對象復(fù)制到另一個區(qū)域中,然后清除使用區(qū)域,類似Survivor的from和to;
缺點:需要兩倍內(nèi)存空間,內(nèi)存使用率較低 - 標(biāo)記-整理
結(jié)合了標(biāo)記-清除和復(fù)制的優(yōu)點
將根節(jié)點開始標(biāo)記被引用的對象,然后掃描整個堆,清除未標(biāo)記對象,然后把存活對象“壓縮”到堆的其中一塊,順序排放
缺點:效率低
4.4 常見垃圾收集器
JVM 中提供的年輕代垃圾收集器 Serial、ParNew、Parallel Scavenge 都是復(fù)制算法,而 CMS、G1、ZGC 都屬于標(biāo)記清除算法。
JDK版本默認垃圾收集器
jdk1.7 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9、10、11 默認垃圾收集器G1
-XX:+PrintCommandLineFlagsjvm參數(shù)可查看默認設(shè)置收集器類型
-XX:+PrintGCDetails亦可通過打印的GC日志的新生代、老年代名稱判斷
CMS
JDK1.7之前最主流的垃圾回收器;使用標(biāo)記-清除算法,并發(fā)收集停頓小;

三色標(biāo)記算法
G1
G1取消了堆中年輕代與老年代的物理劃分,但它依然屬于分代收集器;G1算法將堆劃分為若干Region區(qū)域,一部分作為新生代一部分作為老年代;

ZGC
JDK11提供的高效垃圾回收算法,針對大堆內(nèi)存設(shè)計;主要特點:著色指針、讀屏障、并發(fā)處理、基于Region、內(nèi)存壓縮(整理)

5. JVM調(diào)優(yōu)
5.1 編譯優(yōu)化
5.1.1 方法內(nèi)聯(lián)
調(diào)用方法要經(jīng)歷壓棧和出棧,這會帶來一定的時間和空間方面開銷,那么對于那些代碼體量不大,又頻繁調(diào)用的方法,這個時間和空間的消耗會很大;
方法內(nèi)聯(lián)的優(yōu)化就是將那些代碼體量小的方法代碼復(fù)制到發(fā)起調(diào)用的方法之中,避免真實調(diào)用;
-
-XX:CompileThreshold:設(shè)置熱點方法閾值,連續(xù)調(diào)用多少才能成為熱點方法; -
-XX:MaxFreqInlineSize:經(jīng)常執(zhí)行的方法,內(nèi)聯(lián)優(yōu)化最大方法體,默認JVM不會對方法體太大的方法做內(nèi)聯(lián)優(yōu)化; -
-XX:MaxInlineSize:不經(jīng)常執(zhí)行的方法,內(nèi)聯(lián)優(yōu)化最大方法體
熱點方法能提高系統(tǒng)性能,提高方法內(nèi)聯(lián)的幾種方式:
- 通過JVM參數(shù)來減少熱點閾值或增加方法體閾值,使更多的方法進行內(nèi)聯(lián),但這會增加內(nèi)存開銷;
- 編程中避免一個方法中寫大量代碼,習(xí)慣使用小方法體;
- 盡量使用final、private、static關(guān)鍵字修飾方法,編碼方法因為繼承,會需要額外的類型檢查;
5.1.2 逃逸分析
逃逸分析(Escape Analysis)是判斷一個對象是否被外部方法引用或外部線程訪問的分析技術(shù),編譯器會根據(jù)逃逸分析的結(jié)果對代碼進行優(yōu)化.
棧上分配
java對象默認分配在堆上,這會帶來垃圾回收的時間和空間消耗,如果一個對象只在方法類使用(未發(fā)生逃逸),比如方法類的局部變量,這個時候?qū)ο蠓峙涞骄€程棧上,隨著棧空間的回收而回收,帶來性能提升;
開啟方式:-XX:+DoEscapeAnalysis(JVM默認是開啟的)
關(guān)閉方式:-XX:-DoEscapeAnalysis
鎖消除
當(dāng)一個線程安全容器,比如StringBuffer,在未發(fā)生逃逸時,JIT編譯(運行時)會自動進行Synchronized鎖消除;比如如下代碼,StringBuffer和StringBuilder的性能差別不大
public static String getString(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
開啟方式:-XX:+EliminateLocks
標(biāo)量替換
5.2 GC調(diào)優(yōu)
5.2.1 降低Minor GC頻率
- 增加新生代大小
5.2.2 降低Full GC頻率
- 減少大對象的創(chuàng)建
- 增加堆內(nèi)存空間
5.2.3 選擇合適的GC回收器
- 如果要求響應(yīng)速度快,選擇CMS和G1
- 如果如果經(jīng)常產(chǎn)生大對象推薦使用G1,G1有專門存儲巨型對象分區(qū),并且會優(yōu)先對可回收空間較大的Region進行回收(garbage first);
- 如果物理機支持大堆內(nèi)存,可以用ZGC提高效率;
5.3 內(nèi)存分配及參數(shù)調(diào)優(yōu)
根據(jù)實際情況設(shè)置JVM的啟動參數(shù),常用的JVM優(yōu)化參數(shù):
| 配置參數(shù) | 功能 |
|---|---|
| -Xms | 初始化堆大小,如:-Xms256m,一般和Xmx保持一樣 |
| -Xmx | 最大堆大小,最好設(shè)置為容器最大內(nèi)存的80% |
| -Xmn | 新生代大小,推薦設(shè)置Xmx的3/8 |
| -Xss | 每個線程的堆棧大小,默認1M |
| -xx:+PrintGCDetail | 打印GC日志 |
| -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/-dump.hprof | 發(fā)生OOM時自動生成Dump文件 |
todo ...
5.4 常用的JVM分析工具
Linux 命令 - top
查看進程的CPU使用率、內(nèi)存使用率、系統(tǒng)負載
Linux 命令 - vmstat
jstat 命令
檢測java應(yīng)用程序?qū)崟r運行情況,包括堆內(nèi)存信息及垃圾回收信息
jstack 命令
查看線程的堆棧信息
jmap 命令
查看堆內(nèi)存初始化配置及堆內(nèi)存的使用情況,可以把堆內(nèi)存中對象的信息、對象的數(shù)量等dump到文件中,使用工具進行分析;
jmap -dump:format=b.file=/tmp/heap.hprof 28557
jps 命令
JVM Process Status Tool,顯示當(dāng)前java進程情況以及進程pid,可以看到啟動了多少java進程(每個java進程獨占一個jvm實例),類比linux的ps命令;
jconsole 命令
阿里出品 - arthas
5.5 OOM的排查
出現(xiàn)原因
- 內(nèi)存中加載的數(shù)據(jù)量過于龐大,如一次性從數(shù)據(jù)庫取出大量數(shù)據(jù);
- 死循環(huán)
- JVM參數(shù)內(nèi)存配置太小
- 根本原因:經(jīng)過一次FullGC后老年代中還是滿的
排查方式
1、通過IDE運行跟蹤(很難找到原因)
2、保存問題現(xiàn)場,發(fā)生OOM時記錄堆信息(導(dǎo)出Dump文件信息),內(nèi)存溢出時jvm指令執(zhí)行bat發(fā)送郵件
解決方式
- 增加jvm內(nèi)存大小 -xmx -xms
- 觀察gc日志,配置新生代老年代大小比例。如果程序new的比較頻繁,那么新生代設(shè)置大一點
- 程序優(yōu)化,避免死循環(huán)。