象判活到三色標(biāo)記與收集器選型)
JVM垃圾回收面試題幾乎可以說是Java面試的“必考大題”。無論校招還是社招面試官基本都會(huì)從內(nèi)存模型切入一路追問到垃圾回收的算法、收集器、調(diào)優(yōu)參數(shù)。很多候選人基礎(chǔ)題背得滾瓜爛熟一到“為什么這樣設(shè)計(jì)”“兩者對(duì)比怎么選”就卡殼。這篇文章把JVM垃圾回收環(huán)節(jié)的高頻面試題按知識(shí)鏈路拆開從對(duì)象判活講到三色標(biāo)記從Serial收集器一路聊到ZGC配合我實(shí)際面試和被面試的經(jīng)驗(yàn)給出一份可以直接拿來復(fù)習(xí)、也能當(dāng)“考前沖刺提綱”的完整梳理。如果你是準(zhǔn)備面試的Java開發(fā)或者工作兩三年想系統(tǒng)補(bǔ)一下JVM底層的同學(xué)這文章值得花二十分鐘從頭讀到尾。已經(jīng)對(duì)GC有經(jīng)驗(yàn)的人可以直接跳到第五章的調(diào)優(yōu)實(shí)戰(zhàn)和第六章的高頻問答速查。1. 對(duì)象判活垃圾回收先回答“回收誰”1.1 引用計(jì)數(shù)法為什么被淘汰面試官特別喜歡從“怎么判斷一個(gè)對(duì)象該被回收”開始問。最直觀的方案是引用計(jì)數(shù)每個(gè)對(duì)象維護(hù)一個(gè)計(jì)數(shù)器被引用就加一引用失效就減一歸零就回收。聽起來很完美但有一個(gè)繞不過去的坑——循環(huán)引用。兩個(gè)對(duì)象互相持有對(duì)方外部沒有任何引用了引用計(jì)數(shù)依然不為零這兩個(gè)對(duì)象就永遠(yuǎn)無法回收。經(jīng)典場(chǎng)景是父子節(jié)點(diǎn)互相關(guān)聯(lián)class Node { public Node parent; public ListNode children new ArrayList(); } // 構(gòu)建互相引用的對(duì)象圖后從外部移除引用這段代碼在實(shí)際運(yùn)行中引用計(jì)數(shù)法無法回收這個(gè)對(duì)象圖。所以主流JVM都沒有采用引用計(jì)數(shù)而是用可達(dá)性分析。1.2 可達(dá)性分析與GC Roots可達(dá)性分析的思路很樸素從一組稱為“GC Roots”的根對(duì)象出發(fā)沿著引用鏈往下走能走到的對(duì)象就視為存活走不到的就是可回收垃圾。這個(gè)概念用生活化類比就是地鐵路網(wǎng)GC Roots是始發(fā)站能到達(dá)的所有站點(diǎn)都是活人住的到不了的站就是廢棄站。面試常問的進(jìn)階點(diǎn)是“GC Roots包含哪些”。按HotSpot實(shí)現(xiàn)主要包括虛擬機(jī)棧中局部變量表引用的對(duì)象正在執(zhí)行的方法里的參數(shù)、局部變量靜態(tài)變量引用的對(duì)象方法區(qū)中類靜態(tài)屬性常量引用的對(duì)象字符串常量池等JNI引用的對(duì)象Native方法中引用的Java對(duì)象同步監(jiān)視器鎖持有的對(duì)象被synchronized鎖住的對(duì)象JVM內(nèi)部的引用系統(tǒng)類加載器、基本類型Class對(duì)象等回答這個(gè)問題的關(guān)鍵不是背全而是理解“根”的共同特征它們都是當(dāng)前存活的、外界可以直接觸達(dá)的入口。面試官接著問“GC Roots管理和誰相關(guān)”你可以順帶提一句和線程棧、方法區(qū)、JNI這三塊運(yùn)行時(shí)數(shù)據(jù)區(qū)強(qiáng)相關(guān)這也解釋了為什么判斷對(duì)象存活必須STW——因?yàn)橐WC引用關(guān)系在分析期間不變化。1.3 四種引用類型與ThreadLocal內(nèi)存泄漏判活之后常跟著“四種引用有什么區(qū)別”。強(qiáng)引用、軟引用、弱引用、虛引用很多候選人能背定義但說不清場(chǎng)景。強(qiáng)引用就是正常Object obj new Object()只要強(qiáng)引用還在GC寧可拋OOM也不回收。軟引用在內(nèi)存充足時(shí)不回收內(nèi)存不足時(shí)才回收典型場(chǎng)景是緩存框架像MyBatis的緩存、一些圖片加載庫都會(huì)用SoftReference做內(nèi)存敏感的緩存。弱引用更短命每次GC只要發(fā)現(xiàn)就被回收典型場(chǎng)景是ThreadLocal的ThreadLocalMap。ThreadLocal的Entry為什么設(shè)計(jì)成弱引用這是面試官最愛追問的細(xì)節(jié)。ThreadLocalMap里Entry繼承WeakReferencekey是ThreadLocal的弱引用value還是強(qiáng)引用。好處是ThreadLocal對(duì)象失去外部強(qiáng)引用后key在下一次GC即被清理Entry變?yōu)椤発ey為null”value雖然還存在但至少避免了key無法回收的問題。遺留的value就是“內(nèi)存泄漏”隱患——這也是必須在finally里調(diào)用remove()的原因?;卮鸬竭@里把“弱引用解決key回收問題remove解決value殘留問題”講清楚這一串基本就是加分回答。虛引用不能通過get()拿到對(duì)象唯一用途是對(duì)象被回收時(shí)收到系統(tǒng)通知。主要用于堆外內(nèi)存的回收追蹤比如NIO的DirectByteBuffer就是通過虛引用關(guān)聯(lián)Cleaner實(shí)現(xiàn)堆外內(nèi)存釋放。安全校驗(yàn)注意不要寫成“虛引用能讓對(duì)象復(fù)活”那是finalize的機(jī)制下文單獨(dú)講。1.4 finalize與對(duì)象自救finalize是古老的兜底機(jī)制面試常問但實(shí)際工作中沒人用。一個(gè)對(duì)象覆蓋了finalize()方法并且第一次標(biāo)記為不可達(dá)時(shí)會(huì)進(jìn)入F-Queue等待執(zhí)行finalize執(zhí)行期間如果重新被引用就能“自救”不被回收。但注意finalize只執(zhí)行一次第二次不可達(dá)就直接回收了。這個(gè)機(jī)制設(shè)計(jì)的初衷是類似C析構(gòu)函數(shù)的資源釋放但問題很多執(zhí)行時(shí)機(jī)不確定、性能差、可能復(fù)活意外對(duì)象所以早已被官方標(biāo)記為不推薦使用。面試答到這個(gè)點(diǎn)時(shí)補(bǔ)充一句“用try-with-resources或者Cleaner機(jī)制替代”就能體現(xiàn)你了解現(xiàn)代寫法。2. 回收算法與分代收集面試官遞進(jìn)提問的套路2.1 三大基礎(chǔ)GC算法對(duì)比對(duì)象判活解決“誰是垃圾”接下來就是“怎么回收”。基礎(chǔ)算法就三個(gè)標(biāo)記-清除、標(biāo)記-復(fù)制、標(biāo)記-整理。標(biāo)記-清除是兩階段先標(biāo)記可回收對(duì)象再統(tǒng)一回收。問題有兩個(gè)一是產(chǎn)生大量不連續(xù)的內(nèi)存碎片后續(xù)分配大對(duì)象可能因找不到連續(xù)空間提前觸發(fā)GC二是標(biāo)記和清除兩個(gè)階段效率都不高。這個(gè)算法是后面所有方案的“地基”但實(shí)際使用中很少單獨(dú)用。標(biāo)記-復(fù)制把內(nèi)存分成兩塊只使用一塊回收時(shí)把存活對(duì)象整體復(fù)制到另一塊再清空當(dāng)前塊。優(yōu)點(diǎn)是沒有碎片、分配簡(jiǎn)單缺點(diǎn)是浪費(fèi)一半空間。HotSpot的新生代就是這個(gè)算法的變體——不按1:1分而是Eden:Survivor 8:1用一塊Eden加兩塊Survivor把浪費(fèi)壓縮到10%。標(biāo)記-整理面向老年代標(biāo)記存活對(duì)象后把存活對(duì)象往一端移動(dòng)再清理邊界外的內(nèi)存。避免了復(fù)制的高開銷也解決了碎片問題但移動(dòng)對(duì)象需要更新所有引用這個(gè)過程中必須STW。面試真題往往是對(duì)比題“復(fù)制算法和整理算法各自適合哪一代為什么”新生代對(duì)象存活率低復(fù)制成本小老年代存活率高復(fù)制一遍太貴整理更劃算。這個(gè)答案能引到分代收集。2.2 分代假說與堆內(nèi)存劃分分代收集不是什么高深概念核心是兩條經(jīng)驗(yàn)統(tǒng)計(jì)規(guī)律絕大多數(shù)對(duì)象朝生夕滅熬過多次GC的對(duì)象越難被回收。用IBM研究的數(shù)據(jù)來說新生代對(duì)象98%活不過第一輪GC。既然存活率這么低用復(fù)制算法很劃算老年代對(duì)象存活率高用標(biāo)記-整理或標(biāo)記-清除。堆內(nèi)存劃分為新生代Young和老年代Old。新生代又拆為Eden和兩個(gè)Survivor區(qū)默認(rèn)比例Eden:S0:S1 8:1:1可以通過-XX:SurvivorRatio調(diào)整。為什么需要兩個(gè)Survivor因?yàn)閺?fù)制算法需要一塊空的保留區(qū)來放存活對(duì)象一塊不夠用。兩個(gè)Survivor輪流當(dāng)from和to保證每次復(fù)制后總有一塊干凈區(qū)域。還有一塊常被忽略的“元空間”JDK8以后方法區(qū)被移到了元空間。元空間使用本地內(nèi)存默認(rèn)不受堆大小限制但依然需要GC來回收廢棄的類和無用的常量。2.3 Minor GC完整流程與對(duì)象晉升規(guī)則把對(duì)象分配和GC流程串起來是面試中“連環(huán)問”的高發(fā)區(qū)。新對(duì)象分配路徑優(yōu)先在Eden區(qū)分配Eden不夠時(shí)觸發(fā)Minor GC也叫Young GC。Minor GC過程可以背誦式描述Eden區(qū)存活對(duì)象復(fù)制到S0from區(qū)年齡1S0from區(qū)的存活對(duì)象復(fù)制到S1to區(qū)年齡1原Eden和from區(qū)清空from和to交換身份年齡達(dá)到閾值默認(rèn)15的對(duì)象晉升到老年代這里面試官會(huì)立刻追問晉升條件只有一個(gè)年齡閾值嗎當(dāng)然不是。至少還有四個(gè)大對(duì)象直接進(jìn)老年代動(dòng)態(tài)年齡判定空間分配擔(dān)保失敗Survivor區(qū)裝不下的對(duì)象直接晉升老年代。動(dòng)態(tài)年齡判定是容易被忽略的點(diǎn)。HotSpot不是等所有對(duì)象都到15歲才晉升而是統(tǒng)計(jì)Survivor中同齡對(duì)象大小總和如果超過Survivor空間的一半年齡大于等于這些對(duì)象的就直接晉升。這個(gè)設(shè)計(jì)是為了適應(yīng)不同應(yīng)用的對(duì)象年齡分布。大對(duì)象默認(rèn)閾值臨界點(diǎn)是-XX:PretenureSizeThreshold大于該值的對(duì)象直接進(jìn)老年代避免在Eden和Survivor之間來回復(fù)制。但注意這個(gè)參數(shù)只對(duì)Serial和ParNew有效搭配Parallel收集器時(shí)不生效。空間分配擔(dān)保是另一個(gè)高頻考點(diǎn)。Minor GC前JVM會(huì)檢查老年代最大可用連續(xù)空間是否大于新生代所有對(duì)象總大小如果大于本次Minor GC可以確保安全如果小于再看-XX:HandlePromotionFailure是否允許擔(dān)保失敗。允許時(shí)冒險(xiǎn)試試不行再觸發(fā)Full GC不允許就直接Full GC。JDK6以后這個(gè)開關(guān)默認(rèn)開啟且廢棄當(dāng)出現(xiàn)“擔(dān)保失敗”時(shí)老年代會(huì)進(jìn)行一次Full GC回收空間代價(jià)是停頓時(shí)間顯著上升。2.4 Full GC的觸發(fā)條件Full GC是老年代的GC一般伴隨Metaspace回收。觸發(fā)條件包括老年代空間不足、Metaspace空間不足、調(diào)用System.gc()只是建議但通常也會(huì)觸發(fā)、CMS并發(fā)模式失敗、分配擔(dān)保失敗等。面試常踩的坑是把Full GC和Major GC混為一談。嚴(yán)謹(jǐn)?shù)卣fMajor GC指老年代的GCFull GC指對(duì)整個(gè)堆新生代老年代元空間的GC。比如Parallel收集器下Full GC實(shí)際同時(shí)回收新生代和老年代用的還是單線程的Serial Old算法。性能殺手就是Full GC全堆掃描、STW時(shí)間長(zhǎng)、執(zhí)行完后可用空間依然不足時(shí)會(huì)連續(xù)Full GC直至OOM。3. 并發(fā)回收的三座大山STW、安全點(diǎn)、三色標(biāo)記3.1 為什么垃圾回收必須STW“為什么GC要暫停所有用戶線程”是面試官必問的底層題。直接原因是可達(dá)性分析必須基于一個(gè)穩(wěn)定快照如果在分析過程中引用關(guān)系不斷變化要么漏標(biāo)存活對(duì)象導(dǎo)致誤回收致命要么多標(biāo)垃圾對(duì)象導(dǎo)致回收不徹底可接受但會(huì)積累浮動(dòng)垃圾。為了保準(zhǔn)確HotSpot最樸素的方案就是暫停業(yè)務(wù)線程也就是STWStop The World。這里常有個(gè)誤解STW不是所有收集器的固有問題而是“枚舉根節(jié)點(diǎn)”和“移動(dòng)對(duì)象”時(shí)的必要手段。CMS的初始標(biāo)記和最終標(biāo)記階段依然要STW只是把最耗時(shí)的并發(fā)標(biāo)記階段做成了用戶線程同時(shí)運(yùn)行。G1、ZGC更是通過并發(fā)根掃描和并發(fā)標(biāo)記進(jìn)一步縮短STW。3.2 安全點(diǎn)與安全區(qū)域STW不是想停就能停線程必須在“安全點(diǎn)”停下。安全點(diǎn)是JVM預(yù)設(shè)的指令位置比如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等。在這些位置停下才能保證線程狀態(tài)一致不會(huì)出現(xiàn)棧幀正在被修改、寄存器中的引用無法枚舉的情況。一個(gè)線程從開始執(zhí)行到安全點(diǎn)之間的時(shí)間被稱為“安全區(qū)域外的運(yùn)行時(shí)間”。如果用-XX:UseCountedLoopSafetyCheck這類參數(shù)優(yōu)化循環(huán)次數(shù)過多可能導(dǎo)致線程遲遲進(jìn)不了安全點(diǎn)這在高并發(fā)低延遲場(chǎng)景是個(gè)隱患。除了安全點(diǎn)還有安全區(qū)域的概念線程進(jìn)入Sleep或Blocked狀態(tài)時(shí)無法響應(yīng)中斷請(qǐng)求因此只要進(jìn)入安全區(qū)域GC就可以認(rèn)為該線程“已暫停”不需要等它響應(yīng)。面試答到這個(gè)層面面試官能感受到你不只是背概念而是真的理解線程運(yùn)行狀態(tài)和GC的交互。3.3 三色標(biāo)記算法與漏標(biāo)問題三色標(biāo)記是并發(fā)標(biāo)記的理論模型很多候選人栽在這里。把對(duì)象分成三種顏色白色未被訪問、灰色自身被訪問但引用子對(duì)象未被全部訪問、黑色自身和引用子對(duì)象都被訪問。從GC Roots出發(fā)初始時(shí)根對(duì)象為灰色不斷把灰色對(duì)象標(biāo)記為黑色、把白色引用對(duì)象標(biāo)記為灰色直到?jīng)]有灰色對(duì)象剩下的白色就是可回收對(duì)象。并發(fā)標(biāo)記的難點(diǎn)不是標(biāo)記本身而是“一邊標(biāo)記、一邊改引用”會(huì)漏標(biāo)。經(jīng)典的漏標(biāo)場(chǎng)景黑色對(duì)象A原本沒有引用C標(biāo)記過程中A新增了引用指向C而C還沒被訪問到此時(shí)如果標(biāo)記線程已經(jīng)處理完A就不會(huì)再去掃描A的新引用C就會(huì)被誤判為垃圾。漏標(biāo)必須滿足兩個(gè)條件黑色對(duì)象新增了指向白色對(duì)象的引用該白色對(duì)象的所有灰色引用被刪除。只要打破任一條件就不會(huì)漏。因此兩種解決方案誕生了增量更新Incremental Update記錄黑色對(duì)象新增的引用最終標(biāo)記階段重新掃描這些引用。CMS采用。原始快照Snapshot At The BeginningSATB記錄被刪除的灰色引用并發(fā)標(biāo)記期間把這些引用指向的對(duì)象當(dāng)作存活對(duì)象處理。G1采用。面試回答這個(gè)點(diǎn)最好畫一個(gè)簡(jiǎn)單示意口頭描述A、B、C三個(gè)對(duì)象的引用變化然后分別解釋增量更新和SATB如何打破漏標(biāo)條件。能說清楚“增量更新是記錄新增、SATB是記錄刪除”已經(jīng)能區(qū)分絕大多數(shù)候選人。4. 收集器選型從Serial聊到ZGC4.1 經(jīng)典收集器組合速查收集器矩陣是面試經(jīng)典環(huán)節(jié)。HotSpot JDK8默認(rèn)是Parallel Scavenge Parallel OldJDK9以后默認(rèn)G1JDK11起ZGC成為可選項(xiàng)JDK21正式GA了ZGC的分代模式。下面這張表建議記牢收集器適用代算法線程特點(diǎn)Serial新生代復(fù)制單線程簡(jiǎn)單Client模式默認(rèn)單核友好ParNew新生代復(fù)制多線程CMS的默認(rèn)搭檔追求低延遲Parallel Scavenge新生代復(fù)制多線程關(guān)注吞吐量可自適應(yīng)調(diào)節(jié)Serial Old老年代標(biāo)記-整理單線程CMS失敗的后備方案Parallel Old老年代標(biāo)記-整理多線程與Parallel Scavenge配套CMS老年代標(biāo)記-清除并發(fā)低延遲有碎片和浮動(dòng)垃圾問題G1全堆Region復(fù)制整理多線程可預(yù)測(cè)停頓JDK9默認(rèn)ZGC全堆Region并發(fā)整理多線程超低停頓停頓不隨堆增長(zhǎng)面試官大概率問“你項(xiàng)目用的什么收集器”。千萬不要報(bào)個(gè)“G1默認(rèn)”就完了要能說出為什么選它。常見答法業(yè)務(wù)對(duì)延遲敏感G1支持指定最大停頓時(shí)間且能做到并發(fā)收集適合大堆如果追求吞吐量且對(duì)停頓不敏感Parallel更合適。4.2 CMS的運(yùn)行流程與經(jīng)典坑點(diǎn)CMSConcurrent Mark Sweep是低延遲時(shí)代的代表四個(gè)階段初始標(biāo)記STW標(biāo)記GC Roots直接關(guān)聯(lián)的對(duì)象很快并發(fā)標(biāo)記從GC Roots開始遍歷對(duì)象圖和用戶線程并發(fā)重新標(biāo)記STW修正并發(fā)期間引用變化導(dǎo)致的漏標(biāo)用增量更新并發(fā)清除和用戶線程并發(fā)清理垃圾對(duì)象兩個(gè)經(jīng)典坑必須掌握。第一個(gè)是“并發(fā)模式失敗”并發(fā)標(biāo)記或清除期間老年代空間被用戶線程的新對(duì)象耗盡CMS無法繼續(xù)并發(fā)收集只能退化為Serial Old單線程Full GC停頓時(shí)間瞬間飆升。解決方法預(yù)留足夠空間-XX:CMSInitiatingOccupancyFraction例如設(shè)68%讓CMS在老年代用到68%時(shí)就開始收集避免撐滿或調(diào)大堆內(nèi)存。第二個(gè)坑是內(nèi)存碎片。CMS用標(biāo)記-清除算法不整理內(nèi)存運(yùn)行久了老年代碎片化嚴(yán)重大對(duì)象分配困難提前觸發(fā)Full GC。解決方案是開-XX:UseCMSCompactAtFullCollection在Full GC時(shí)進(jìn)行碎片整理但會(huì)增加停頓?,F(xiàn)代JDK8u和JDK9已經(jīng)逐步廢棄CMS面試只需說明原理和缺點(diǎn)不要推薦生產(chǎn)環(huán)境再用CMS。4.3 G1的核心設(shè)計(jì)Region、RSet與回收集G1Garbage First是目前工作的主力面試地位極高。它的核心創(chuàng)新是把堆劃分為大小相等的Region默認(rèn)2048個(gè)每個(gè)Region獨(dú)立扮演Eden、Survivor、Old或Humongous大對(duì)象區(qū)。Region的引入讓G1不必對(duì)整個(gè)堆做GC每次只回收一部分Region實(shí)現(xiàn)“可預(yù)測(cè)的停頓”。G1的另一個(gè)核心是RSetRemembered Set用來記錄哪些Region中的對(duì)象引用了當(dāng)前Region。說白了就是跨Region引用的“反向指針?biāo)饕薄.?dāng)GC需要掃描某個(gè)Region的存活對(duì)象時(shí)不用全堆掃描只要查RSet就能知道誰引用了它。RSet的建立和維護(hù)依賴寫屏障這也是“每條引用賦值指令都需要額外開銷”的原因。G1的回收過程可以概括為初始標(biāo)記STW標(biāo)記GC Roots直接關(guān)聯(lián)對(duì)象在Minor GC順帶完成并發(fā)標(biāo)記三色標(biāo)記用SATB記錄引用刪除避免漏標(biāo)最終標(biāo)記STW處理SATB隊(duì)列中的引用記錄篩選回收STW根據(jù)RSet統(tǒng)計(jì)各Region的回收價(jià)值和成本計(jì)算性價(jià)比優(yōu)先回收垃圾最多的Region“Mixed GC”概念常被問到G1的回收不只是老年代而是從所有Region中挑選回收價(jià)值最高的若干Region?;卮饡r(shí)提一句“篩選階段會(huì)計(jì)算每個(gè)Region的回收收益和耗時(shí)選擇垃圾占比高、回收成本低的Region”就能體現(xiàn)你理解了G1為什么叫Garbage First。G1的停頓時(shí)間由-XX:MaxGCPauseMillis指定目標(biāo)但注意這不是硬性保證只是一個(gè)預(yù)測(cè)模型。G1會(huì)基于歷史數(shù)據(jù)估算每個(gè)Region的回收耗時(shí)盡量讓停頓控制在目標(biāo)以內(nèi)。如果堆非常大比如上百GBG1的RSet維護(hù)成本會(huì)明顯上升這就是為什么ZGC在超大堆上更占優(yōu)勢(shì)。4.4 ZGC顏色指針與讀屏障ZGC是追求極致低延遲的收集器。最吸引人的特性是GC停頓時(shí)間不隨堆大小線性增長(zhǎng)JDK16以后ZGC支持在16TB堆上以幾十毫秒內(nèi)完成GC。ZGC的關(guān)鍵技術(shù)是染色指針Colored Pointer。在64位Linux上ZGC把對(duì)象指針的前幾位用作標(biāo)記位用來記錄對(duì)象狀態(tài)Marked0、Marked1、Remapped、Finalizable。GC標(biāo)記時(shí)不需要訪問對(duì)象本身直接在指針上操作狀態(tài)位即可。ZGC還引入了讀屏障每次從堆中加載引用時(shí)讀屏障會(huì)檢查指針狀態(tài)如果指針指向的對(duì)象需要重定位就立即修正指針。這個(gè)設(shè)計(jì)讓“對(duì)象移動(dòng)”和“用戶線程訪問對(duì)象”可以并發(fā)進(jìn)行這就是ZGC能實(shí)現(xiàn)極短STW的根本原因。面試中只要把“ZGC把GC信息放進(jìn)指針本身通過讀屏障并發(fā)修正引用”講清楚面試官就會(huì)比較滿意。如果追問面向?qū)ο髢?nèi)存,可以說ZGC初始化時(shí)會(huì)保留一段虛擬地址空間用于映射多個(gè)視圖這也是加載指針能實(shí)現(xiàn)單次解引用的前提。5. 調(diào)優(yōu)與排查面試加分項(xiàng)與實(shí)戰(zhàn)技巧5.1 常用GC參數(shù)速查面試不僅問原理還問“你平時(shí)怎么調(diào)優(yōu)”。常用參數(shù)要能隨口報(bào)出參數(shù)作用-Xms / -Xmx初始堆/最大堆-Xmn新生代大小-XX:SurvivorRatioEden/Survivor比例默認(rèn)8-XX:MaxTenuringThreshold晉升閾值默認(rèn)15-XX:PrintGCDetails打印GC日志JDK9前-Xlog:gc*JDK9統(tǒng)一日志-XX:HeapDumpOnOutOfMemoryErrorOOM時(shí)自動(dòng)導(dǎo)出堆快照-XX:MaxGCPauseMillisG1目標(biāo)停頓時(shí)間-XX:ConcGCThreads并發(fā)GC線程數(shù)注意一個(gè)細(xì)節(jié)-Xlog:gc*是JDK9以后統(tǒng)一日志體系的寫法老參數(shù)-XX:PrintGCDetails在JDK9雖然還能生效但推薦遷移到新語法。面試時(shí)能主動(dòng)提這一點(diǎn)說明你用過新版本。5.2 GC日志分析實(shí)戰(zhàn)翻GC日志是定位問題的入口。舉個(gè)例子JDK8下用-XX:PrintGCDetails日志片段長(zhǎng)這樣[GC (Allocation Failure) [PSYoungGen: 61440K-8191K(71680K)] 122880K-65407K(190464K), 0.0234567 secs] [Full GC (Ergonomics) [PSYoungGen: 32767K-0K(71680K)] [ParOldGen: 131512K-147456K(182272K)] 163471K-147456K(254080K), 1.2345678 secs]解析要點(diǎn)Allocation Failure本次Minor GC因Eden區(qū)分配失敗觸發(fā)箭頭前是GC前占用箭頭后是GC后占用括號(hào)里是總量0.0234 secs是本次GC耗時(shí)PSYoungGen和ParOldGen說明用的是Parallel收集器Full GC的耗時(shí)往往數(shù)倍于Minor GC連續(xù)多個(gè)Full GC日志就是內(nèi)存泄漏的典型信號(hào)確認(rèn)堆內(nèi)存不足時(shí)排查順序應(yīng)該是先看OS物理內(nèi)存和容器限制再看進(jìn)程啟動(dòng)參數(shù)最后才是業(yè)務(wù)代碼。很多“頻繁Full GC”其實(shí)是-Xms和-Xmx不一致導(dǎo)致堆反復(fù)伸縮GC觸發(fā)頻繁。生產(chǎn)經(jīng)驗(yàn)是-Xms和-Xmx設(shè)置相同避免運(yùn)行時(shí)擴(kuò)容帶來的額外負(fù)擔(dān)。5.3 常見性能問題排查思路面試常問“線上頻繁Full GC你怎么排查”。我給一個(gè)實(shí)戰(zhàn)套路jstat -gcutil pid 1000連續(xù)觀察看到FGC次數(shù)不斷增長(zhǎng)、FCTFull GC時(shí)間持續(xù)增加基本確認(rèn)Full GC頻繁jmap -dump:formatb,fileheap.bin pid導(dǎo)出堆快照再用MAT或VisualVM分析找大對(duì)象和“類加載器泄漏”MAT中看Dominator Tree挑Retained Heap最大的對(duì)象追到業(yè)務(wù)代碼如果是System.gc()觸發(fā)的查代碼里有沒有顯示調(diào)用如果是CMS觸發(fā)查CMSInitiatingOccupancyFraction是否設(shè)置過小用jstat -gcutil看Eden、Survivor、Old各自的占用趨勢(shì)定位是對(duì)象分配過快還是回收不了一個(gè)容易被忽略但很實(shí)際的技巧是排查GC之前先確認(rèn)是不是CPU限制導(dǎo)致GC線程搶不到CPU很多“GC停頓異常長(zhǎng)”其實(shí)是CPU配額不足這時(shí)調(diào)整GC參數(shù)是沒用的得先解決資源問題。6. 高頻問答速查表面試前10分鐘必背清單最后把高頻問題濃縮成表格適合面試前一天快速過一遍問題核心答法判斷對(duì)象存活的方法可達(dá)性分析GC Roots起點(diǎn)GC Roots有哪些棧引用、靜態(tài)變量、常量、JNI、鎖、JVM內(nèi)部四種引用區(qū)別強(qiáng)引用不會(huì)回收軟引用內(nèi)存不足回收弱引用每次GC回收虛引用回收通知復(fù)制算法適合哪代新生代對(duì)象存活率低復(fù)制開銷小CMS為什么有碎片標(biāo)記-清除不整理內(nèi)存解決需Compact三色標(biāo)記漏標(biāo)的原理黑色對(duì)象新增白色引用同時(shí)該白色引用被刪除增量更新或SATB修復(fù)G1和CMS的區(qū)別G1按Region回收可預(yù)測(cè)停頓CMS標(biāo)記-清除低延遲但有碎片F(xiàn)ull GC觸發(fā)條件老年代滿、元空間滿、擔(dān)保失敗、System.gc生產(chǎn)環(huán)境怎么選收集器延遲敏感選G1/ZGC吞吐優(yōu)先選Parallel調(diào)優(yōu)基本步驟確認(rèn)目標(biāo)資源配置日志監(jiān)控定位瓶頸這個(gè)表可以作為沖刺記憶卡但請(qǐng)記住面試官不會(huì)照著表問他們會(huì)把一個(gè)表中的點(diǎn)橫向展開問到你怎么排查、為什么這樣選、遇到過什么問題。只有把前面的原理真正理解了這張表才是加分項(xiàng)否則就是背題模板。7. 我聊幾個(gè)真實(shí)面試中的感受做面試官這些年我最深的感觸是候選人背題和真懂的區(qū)別非常明顯。問“什么是可達(dá)性分析”幾乎人人會(huì)答但追問“為什么可達(dá)性分析必須STW”很多人開始支支吾吾。再追問“如果有一種收集器不需要STW做標(biāo)記應(yīng)該怎么做”能聊出三色標(biāo)記和SATB的人就明顯少了一大截。如果你準(zhǔn)備面試建議不要只盯著收集器對(duì)比表背而是把“對(duì)象分配→Minor GC→晉升→Full GC→并發(fā)標(biāo)記→三色標(biāo)記→各種收集器設(shè)計(jì)取舍”當(dāng)成一條流水線去理解。順著這條線走一遍從原理到實(shí)戰(zhàn)會(huì)非常順暢。這個(gè)思路不僅對(duì)面試有效日常排查線上問題也會(huì)少走很多彎路。最后分享一個(gè)我在實(shí)際調(diào)優(yōu)中反復(fù)驗(yàn)證過的經(jīng)驗(yàn)所有GC參數(shù)都要結(jié)合監(jiān)控?cái)?shù)據(jù)調(diào)整不要上來就抄別人的參數(shù)組合。我見過太多“面試背了一堆參數(shù)生產(chǎn)直接套用然后出問題”的例子。-Xms、-Xmx設(shè)置相等、根據(jù)對(duì)象分配速率調(diào)-Xmn、根據(jù)停頓目標(biāo)選G1還是Parallel這些基礎(chǔ)決策做好比追求冷門參數(shù)有意義得多。真正的JVM調(diào)優(yōu)永遠(yuǎn)是先有監(jiān)控?cái)?shù)據(jù)再有針對(duì)性調(diào)整最后用數(shù)據(jù)驗(yàn)證效果而不是靠參數(shù)堆砌。