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