JVM 是 Java 程序運(yùn)行基礎(chǔ)误阻,面試時(shí)一定會遇到 JVM 相關(guān)的題。本文會先對面試中 JVM 的考察點(diǎn)進(jìn)行匯總介紹晴埂。然后對 JVM 內(nèi)存模型究反、Java 的類加載機(jī)制、常用的 GC 算法這三個(gè)知識點(diǎn)進(jìn)行詳細(xì)講解儒洛。
1. JVM知識點(diǎn)匯總
如上圖所示精耐,JVM 知識點(diǎn)有 6 個(gè)大方向,其中琅锻,內(nèi)存模型卦停、類加載機(jī)制、GC 垃圾回收是比較重點(diǎn)的內(nèi)容浅浮。性能調(diào)優(yōu)部分偏重實(shí)際應(yīng)用,重點(diǎn)突出實(shí)踐能力捷枯。編譯器優(yōu)化和執(zhí)行模式部分偏重理論基礎(chǔ)滚秩,主要掌握知識點(diǎn)。
在開始下文前淮捆,看下你是否能夠回答以下知識點(diǎn):
- 內(nèi)存模型:程序計(jì)數(shù)器郁油、方法區(qū)、堆攀痊、棧桐腌、本地方法棧的作用,保存哪些數(shù)據(jù)苟径;
- 類加載:雙親委派的加載機(jī)制案站,以及常用類加載器分別加載哪種類型的類;
- GC:堆內(nèi)存劃分棘街,分代回收的思想和依據(jù)蟆盐,以及不同垃圾回收算法實(shí)現(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)存模型主要指運(yùn)行時(shí)的數(shù)據(jù)區(qū)痹愚,包括如下5個(gè)部分
- 棧:
也叫方法棧,線程私有,線程在執(zhí)行每個(gè)方法時(shí)拯腮,都會創(chuàng)建一個(gè)棧楨窖式,用于存儲整個(gè)執(zhí)行過程和狀態(tài);調(diào)用方法時(shí)執(zhí)行入棧疾瓮,方法返回時(shí)執(zhí)行出棧脖镀; - 本地方法棧:
和方法棧類似,不同的時(shí)狼电,執(zhí)行Java方法使用的是棧蜒灰,而執(zhí)行native方法時(shí)使用的是本地方法棧; - 程序計(jì)數(shù)器:
當(dāng)前線程執(zhí)行字節(jié)碼的行號指示器肩碟,通過它可以知道下一條要執(zhí)行的指令强窖,每個(gè)線程獨(dú)占互不影響,保證線程切換后能恢復(fù)到正確的執(zhí)行位置削祈; - 堆:
JVM管理的內(nèi)存中最大的一塊翅溺,存放的是對象實(shí)例;根據(jù)對象存活的周期不同髓抑,JVM把堆內(nèi)存進(jìn)行分帶管理咙崎,由垃圾收集器進(jìn)行對象的回收管理; - 方法區(qū):
存儲已被虛擬機(jī)加載的類信息吨拍、常量褪猛、靜態(tài)變量等數(shù)據(jù);JDK8之前使用堆上的永久代作為方法區(qū)羹饰,而JDK8使用元空間(Meta-space)來代替伊滋;運(yùn)行時(shí)常量池是方法區(qū)的一部分,用于存放編譯期生成的各種字面量與符號引用(類被加載時(shí)觸發(fā))队秩,字符串常量池也在方法區(qū)中笑旺;
注意:Class對象(Class.forName)是放在堆中的,而不是方法區(qū)馍资,class對象是生成的最終實(shí)例筒主,一切實(shí)例對象都放在堆中,方法區(qū)是存儲Class的基本信息鸟蟹;
3. 類加載機(jī)制
類的加載過程是指將編譯好的class類文件的字節(jié)碼讀入到內(nèi)存中物舒,將其存在方法區(qū)并創(chuàng)建對應(yīng)的Class對象;類的加載分為加載戏锹、鏈接冠胯、初始化,其中鏈接又包含驗(yàn)證锦针、準(zhǔn)備荠察、解析三步置蜀,如圖所示
- 加載
1)通過類的全限定名獲取定義此類的二進(jìn)制字節(jié)流(類加載器做的事);
2)將字節(jié)流說代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu);
3)在內(nèi)存中創(chuàng)建這個(gè)類的Class對象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口症虑; - 驗(yàn)證
- 準(zhǔn)備
- 解析
- 初始化
主要完成靜態(tài)代碼塊的執(zhí)行和靜態(tài)變量的賦值桃焕,只有對類主動使用時(shí)匪凉,才進(jìn)行初始化;初始化的觸發(fā)條件:
1)創(chuàng)建類的實(shí)例
2)訪問類的靜態(tài)變量或靜態(tài)方法
3)Class.forName反射類
4)某個(gè)子類被初始化 - 卸載
當(dāng)類對象(注意不是類的實(shí)例)不再被使用時(shí)是會被GC卸載回收的,需要注意的時(shí)JVM自帶的三個(gè)類加載器加載的類在虛擬機(jī)的整個(gè)生命周期中是不會被卸載的,只有用戶自定義的類加載器加載的類才會被卸載灼卢;
3.1 類加載器
如下圖,JVM自帶的三個(gè)類加載器分別是:BootStrap啟動類加載器来农、擴(kuò)展類加載器鞋真、應(yīng)用加載器;以及分別對應(yīng)的加載目錄沃于;
雙親委派
java的類加載使用雙親委派模式涩咖,即一個(gè)類加載器在加載類時(shí),會遞歸的委托給父類加載器去執(zhí)行繁莹,直至頂層的啟動類加載器檩互,如果父類加載器能夠加載,則返回成功咨演,否則子加載器才會自己嘗試加載闸昨;
對于兩個(gè)不同的類加載器(自定義的、沒有繼承關(guān)系)雪标,加載同一個(gè)類零院,會導(dǎo)致兩個(gè)類不等溉跃;
雙親委派的好處
- 避免重復(fù)加載
- 防止對JDK核心類進(jìn)行篡改村刨,比如String.class由啟動類加載器加載,如果想篡改String類撰茎,那么不會生效嵌牺;
4. GC
4.1 對象已死?
判定對象的存活都與“引用”有關(guān)龄糊,有兩種方法去判斷一個(gè)對象已經(jīng)“死了”逆粹;
- 引用計(jì)數(shù)算法
已經(jīng)被淘汰的算法,通過添加一個(gè)引用計(jì)數(shù)器來判斷對象是否還在被引用炫惩,解決不了循環(huán)引用的問題僻弹; - 可達(dá)性分析算法
從GC roots往下搜索,所走的路徑叫引用鏈他嚷,如果有有對象沒有與引用鏈相連的話蹋绽,證明對象是不可用的芭毙;GC roots包括:
棧中引用的對象、方法區(qū)靜態(tài)引用指向的對象卸耘、方法區(qū)常量引用指向的對象退敦、本地方法棧Native引用的對象
再談引用
如果一個(gè)對象只被定義為“被引用”或者“未被引用”兩種狀態(tài),那么對于一些“食之無味蚣抗,棄之可惜”的對象就無能無力侈百;由此產(chǎn)生了四種引用類型:
- 強(qiáng)引用:傳統(tǒng)“引用”的定義,只要被強(qiáng)引用關(guān)聯(lián)的對象永遠(yuǎn)不會被回收翰铡;
- 軟引用:內(nèi)存不夠時(shí)被回收钝域;
- 弱引用:比軟引用更弱,下一次垃圾收集時(shí)被回收两蟀;
- 虛引用:最弱网梢,用來跟蹤對象被垃圾回收的活動(對象被回收時(shí)收到一個(gè)通知);
4.2 分代回收
JVM的堆內(nèi)存被分代管理赂毯,包括新生代和老年代战虏,這樣做主要是為了兼顧垃圾收集的時(shí)間開銷和內(nèi)存的空間有效利用;大部分對象很快就不再使用党涕;
- Minor GC:對新生代的對象的收集烦感;
- Major GC:對舊生代的對象的收集,出現(xiàn)Major GC通常會出現(xiàn)至少一次Minor GC膛堤;
- Full GC:全局范圍的GC手趣,程序中主動調(diào)用System.gc()強(qiáng)制執(zhí)行的GC;出發(fā) Full GC的條件有:當(dāng)年輕代晉升到老年代放不下時(shí)肥荔、老年代使用率超過閾值绿渣、永久代/元空間不足時(shí)、System.gc()燕耿;
新生代區(qū)分為3個(gè)部分:1個(gè)eden區(qū)中符、2個(gè)Survivor區(qū)(from和to,復(fù)制算法)誉帅,新創(chuàng)建的對象都會被分到Eden區(qū)淀散,這些對象經(jīng)過一次Minor GC后,如果仍然存活蚜锨,則會被分配到Survivor區(qū)档插,然后在Survivor區(qū)每熬過一次Minor GC后年齡就會增長一歲,達(dá)到一定年齡后亚再,就被移動到老年代中郭膛。
- 詳細(xì)過程:
GC開始前,對象只會存在于Eden區(qū)和from區(qū)氛悬,to是空的则剃,當(dāng)GC開始時(shí)凄诞,Eden中所有存活的對象都會被移動到To里,而from區(qū)域中仍然存活的對象會根據(jù)年齡來決定去向忍级,年齡達(dá)到閥值的帆谍,則移動到老年區(qū),沒有的移動到to區(qū)域轴咱,經(jīng)過gc后汛蝙,eden和from區(qū)域被清空,然后from和to對換朴肺,保證to區(qū)域?yàn)榭战呀!inor GC會一直重復(fù)這樣的過程,直到“To”區(qū)被填滿戈稿,“To”區(qū)被填滿之后西土,會將所有對象移動到年老代中。
4.3 垃圾回收算法
- 標(biāo)記-清除
老年代常用回收算法鞍盗;最基本的算法需了,兩個(gè)階段:先標(biāo)記要回收的對象,然后一次性回收
缺點(diǎn):效率低般甲,清除后會產(chǎn)生大量的內(nèi)存碎片(空間碎片太多可能會導(dǎo)致當(dāng)程序需要分配大對象時(shí)無法找到連續(xù)的內(nèi)存而不得不提前觸發(fā)一次GC)肋乍; - 復(fù)制算法
年輕代常用回收算法;把內(nèi)存劃分為兩等分敷存,只使用其中一個(gè)區(qū)域墓造,垃圾回收時(shí),將使用區(qū)域里存活的對象復(fù)制到另一個(gè)區(qū)域中锚烦,然后清除使用區(qū)域觅闽,類似Survivor的from和to;
缺點(diǎn):需要兩倍內(nèi)存空間涮俄,內(nèi)存使用率較低 - 標(biāo)記-整理
結(jié)合了標(biāo)記-清除和復(fù)制的優(yōu)點(diǎn)
將根節(jié)點(diǎn)開始標(biāo)記被引用的對象蛉拙,然后掃描整個(gè)堆,清除未標(biāo)記對象禽拔,然后把存活對象“壓縮”到堆的其中一塊刘离,順序排放
缺點(diǎn):效率低
4.4 常見垃圾收集器
JVM 中提供的年輕代垃圾收集器 Serial室叉、ParNew睹栖、Parallel Scavenge 都是復(fù)制算法,而 CMS茧痕、G1野来、ZGC 都屬于標(biāo)記清除算法。
JDK版本默認(rèn)垃圾收集器
jdk1.7 默認(rèn)垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默認(rèn)垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9踪旷、10曼氛、11 默認(rèn)垃圾收集器G1
-XX:+PrintCommandLineFlagsjvm
參數(shù)可查看默認(rèn)設(shè)置收集器類型
-XX:+PrintGCDetails
亦可通過打印的GC日志的新生代豁辉、老年代名稱判斷
CMS
JDK1.7之前最主流的垃圾回收器;使用標(biāo)記-清除算法舀患,并發(fā)收集停頓谢占丁;
三色標(biāo)記算法
G1
G1取消了堆中年輕代與老年代的物理劃分聊浅,但它依然屬于分代收集器餐抢;G1算法將堆劃分為若干Region區(qū)域,一部分作為新生代一部分作為老年代低匙;
ZGC
JDK11提供的高效垃圾回收算法旷痕,針對大堆內(nèi)存設(shè)計(jì);主要特點(diǎn):著色指針顽冶、讀屏障欺抗、并發(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)歷壓棧和出棧绞呈,這會帶來一定的時(shí)間和空間方面開銷,那么對于那些代碼體量不大间景,又頻繁調(diào)用的方法报强,這個(gè)時(shí)間和空間的消耗會很大;
方法內(nèi)聯(lián)的優(yōu)化就是將那些代碼體量小的方法代碼復(fù)制到發(fā)起調(diào)用的方法之中拱燃,避免真實(shí)調(diào)用秉溉;
-
-XX:CompileThreshold
:設(shè)置熱點(diǎn)方法閾值,連續(xù)調(diào)用多少才能成為熱點(diǎn)方法碗誉; -
-XX:MaxFreqInlineSize
:經(jīng)常執(zhí)行的方法召嘶,內(nèi)聯(lián)優(yōu)化最大方法體,默認(rèn)JVM不會對方法體太大的方法做內(nèi)聯(lián)優(yōu)化哮缺; -
-XX:MaxInlineSize
:不經(jīng)常執(zhí)行的方法弄跌,內(nèi)聯(lián)優(yōu)化最大方法體
熱點(diǎn)方法能提高系統(tǒng)性能,提高方法內(nèi)聯(lián)的幾種方式:
- 通過JVM參數(shù)來減少熱點(diǎn)閾值或增加方法體閾值尝苇,使更多的方法進(jìn)行內(nèi)聯(lián)铛只,但這會增加內(nèi)存開銷;
- 編程中避免一個(gè)方法中寫大量代碼糠溜,習(xí)慣使用小方法體淳玩;
- 盡量使用final、private非竿、static關(guān)鍵字修飾方法蜕着,編碼方法因?yàn)槔^承,會需要額外的類型檢查红柱;
5.1.2 逃逸分析
逃逸分析(Escape Analysis)是判斷一個(gè)對象是否被外部方法引用或外部線程訪問的分析技術(shù)承匣,編譯器會根據(jù)逃逸分析的結(jié)果對代碼進(jìn)行優(yōu)化.
棧上分配
java對象默認(rèn)分配在堆上蓖乘,這會帶來垃圾回收的時(shí)間和空間消耗,如果一個(gè)對象只在方法類使用(未發(fā)生逃逸)韧骗,比如方法類的局部變量嘉抒,這個(gè)時(shí)候?qū)ο蠓峙涞骄€程棧上,隨著椗郾空間的回收而回收众眨,帶來性能提升;
開啟方式:-XX:+DoEscapeAnalysis
(JVM默認(rèn)是開啟的)
關(guān)閉方式:-XX:-DoEscapeAnalysis
鎖消除
當(dāng)一個(gè)線程安全容器容诬,比如StringBuffer娩梨,在未發(fā)生逃逸時(shí),JIT編譯(運(yùn)行時(shí))會自動進(jìn)行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進(jìn)行回收(garbage first);
- 如果物理機(jī)支持大堆內(nèi)存躲叼,可以用ZGC提高效率芦缰;
5.3 內(nèi)存分配及參數(shù)調(diào)優(yōu)
根據(jù)實(shí)際情況設(shè)置JVM的啟動參數(shù),常用的JVM優(yōu)化參數(shù):
配置參數(shù) | 功能 |
---|---|
-Xms | 初始化堆大小枫慷,如:-Xms256m让蕾,一般和Xmx保持一樣 |
-Xmx | 最大堆大小,最好設(shè)置為容器最大內(nèi)存的80% |
-Xmn | 新生代大小或听,推薦設(shè)置Xmx的3/8 |
-Xss | 每個(gè)線程的堆棧大小探孝,默認(rèn)1M |
-xx:+PrintGCDetail | 打印GC日志 |
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/-dump.hprof | 發(fā)生OOM時(shí)自動生成Dump文件 |
todo ...
5.4 常用的JVM分析工具
Linux 命令 - top
查看進(jìn)程的CPU使用率、內(nèi)存使用率誉裆、系統(tǒng)負(fù)載
Linux 命令 - vmstat
jstat 命令
檢測java應(yīng)用程序?qū)崟r(shí)運(yùn)行情況顿颅,包括堆內(nèi)存信息及垃圾回收信息
jstack 命令
查看線程的堆棧信息
jmap 命令
查看堆內(nèi)存初始化配置及堆內(nèi)存的使用情況,可以把堆內(nèi)存中對象的信息足丢、對象的數(shù)量等dump到文件中粱腻,使用工具進(jìn)行分析;
jmap -dump:format=b.file=/tmp/heap.hprof 28557
jps 命令
JVM Process Status Tool斩跌,顯示當(dāng)前java進(jìn)程情況以及進(jìn)程pid绍些,可以看到啟動了多少java進(jìn)程(每個(gè)java進(jìn)程獨(dú)占一個(gè)jvm實(shí)例),類比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運(yùn)行跟蹤(很難找到原因)
2萝快、保存問題現(xiàn)場,發(fā)生OOM時(shí)記錄堆信息(導(dǎo)出Dump文件信息)著角,內(nèi)存溢出時(shí)jvm指令執(zhí)行bat發(fā)送郵件
解決方式
- 增加jvm內(nèi)存大小 -xmx -xms
- 觀察gc日志揪漩,配置新生代老年代大小比例。如果程序new的比較頻繁吏口,那么新生代設(shè)置大一點(diǎn)
- 程序優(yōu)化奄容,避免死循環(huán)。