梳理:從基礎(chǔ)到框架一次說清)
面試官第一句話往往不是“自我介紹”而是“講講你理解的Java平臺(tái)無關(guān)性”。很多候選人脫口而出“一次編譯到處運(yùn)行”然后就沒有然后了。這恰恰暴露了應(yīng)試者只背了結(jié)論、沒有構(gòu)建知識(shí)體系的問題。真正的平臺(tái)無關(guān)性是一套從源碼到字節(jié)碼、再到JVM的完整鏈路.java文件編譯成.class字節(jié)碼字節(jié)碼不面向任何具體操作系統(tǒng)而是面向JVM規(guī)范。不同的操作系統(tǒng)安裝對(duì)應(yīng)的JVM由JVM將字節(jié)碼解釋或JIT編譯為本地機(jī)器碼。面試官追問“Java為什么慢”如果你能提到HotSpot的JIT會(huì)熱點(diǎn)代碼編譯為機(jī)器碼而C2編譯器甚至能做激進(jìn)優(yōu)化這就說明你理解的是“運(yùn)行時(shí)適應(yīng)”而非教科書上的死話。繼續(xù)深挖基礎(chǔ)String是高頻考點(diǎn)中的釘子戶。別只背“不可變”三個(gè)字要理解不可變帶來的安全性和字符串常量池的復(fù)用機(jī)制。面試官喜歡問String a a b和new String(ab)的區(qū)別更進(jìn)階的是問StringBuilder在循環(huán)拼接中的必要性。真正拉開差距的答案是說出“字符串拼接的優(yōu)化是javac編譯期的常量折疊而循環(huán)內(nèi)的拼接會(huì)在字節(jié)碼層反復(fù)創(chuàng)建StringBuilder”。如果還能補(bǔ)一句“在JDK9之后String內(nèi)部存儲(chǔ)從char[]換成了byte[]用coder字段區(qū)分Latin1和UTF-16”面試官的眼神會(huì)不一樣。集合框架HashMap是半個(gè)Java面試的縮影HashMap這一關(guān)過不了后面對(duì)話基本沒法進(jìn)行。先問自己能默寫出put方法的完整流程嗎從計(jì)算hash、定位桶、插入到紅黑樹轉(zhuǎn)化、擴(kuò)容每一步都有對(duì)應(yīng)的問題。例如hash函數(shù)為什么是(h key.hashCode()) ^ (h 16)因?yàn)楦?6位與低16位異或讓高位信息參與低位運(yùn)算減少哈希碰撞。為什么加載因子是0.75這是空間和時(shí)間成本的折中泊松分布下桶內(nèi)鏈表長度達(dá)到8的概率極低所以閾值定為8。HashMap的最核心思想不是“數(shù)組鏈表”而是“根據(jù)hash散列盡可能讓數(shù)據(jù)均勻分布以空間換時(shí)間”。如果記不住所有源碼至少要把“為什么樹化閾值是8”和“為什么容量總是2的冪”講清楚這兩個(gè)點(diǎn)是判斷是否真讀過源碼的試金石。ConcurrentHashMap是進(jìn)階版。JDK7的Segment分段鎖與JDK8的CASsynchronized你都應(yīng)該能畫圖。JDK8拋棄了Segments直接用Node數(shù)組put時(shí)通過CAS嘗試插入空桶失敗則synchronized鎖住桶頭節(jié)點(diǎn)。這里有一個(gè)值得思考的點(diǎn)為什么JDK8在并發(fā)度上比JDK7更高因?yàn)殒i粒度從Segment每段包含多個(gè)桶細(xì)化為單個(gè)鏈表/紅黑樹的頭節(jié)點(diǎn)。你還可以引申到擴(kuò)容時(shí)的遷移機(jī)制sizeCtl負(fù)值表示擴(kuò)容狀態(tài)多線程協(xié)助遷移數(shù)據(jù)。說出“擴(kuò)容時(shí)通過ForwardingNode標(biāo)記已遷移的桶”就足以證明你研究過并發(fā)擴(kuò)容的實(shí)現(xiàn)細(xì)節(jié)。并發(fā)編程從volatile到AQS的思維躍遷并發(fā)是Java面試的分水嶺。volatile兩個(gè)字背后是JMMJava內(nèi)存模型、可見性、有序性和內(nèi)存屏障。面試官愛問“volatile能保證原子性嗎”答案是不能但更精彩的回答是解釋為什么不能i是讀、加、寫三步volatile只保證每次讀都從主內(nèi)存取最新值但三步中間可能有其他線程穿插。真正理解volatile的人會(huì)知道JMM的happens-before規(guī)則中有一條“對(duì)volatile變量的寫先行發(fā)生于之后對(duì)它的讀”由此推導(dǎo)出它適用于一寫多讀的狀態(tài)標(biāo)志。如果還能說出“JSR-133規(guī)范重新定義了volatile禁止了與普通變量之間的重排序”那就是加分級(jí)表現(xiàn)。synchronized是另一個(gè)核心。鎖升級(jí)過程無鎖→偏向鎖→輕量級(jí)鎖→重量級(jí)鎖幾乎是必問。但很多候選人只背答案說不清為什么需要偏向鎖因?yàn)榇蠖鄶?shù)對(duì)象的鎖在生命周期內(nèi)只被一個(gè)線程訪問偏向鎖可以減少無競(jìng)爭(zhēng)時(shí)的CAS開銷。輕量級(jí)鎖則是在無競(jìng)爭(zhēng)情況下用CAS替代互斥。在JDK15之后偏向鎖被廢棄如果你能提到這一點(diǎn)會(huì)顯得知識(shí)更新及時(shí)。并發(fā)編程的終極口訣是鎖不是性能問題的根源競(jìng)爭(zhēng)的頻率才是。所以讀多寫少的場(chǎng)景用ReadWriteLock高并發(fā)短操作用synchronizedJVM已經(jīng)優(yōu)化得很好了高吞吐場(chǎng)景考慮LongAdder這種分段累加器。AQS幾乎是Java并發(fā)包的地基。ReentrantLock、Semaphore、CountDownLatch、ThreadPoolExecutor的Worker都基于AQS。你需要能說出AQS的核心一個(gè)volatile int state 一個(gè)CLH變體隊(duì)列。獲取鎖失敗時(shí)線程被封裝成Node入隊(duì)通過LockSupport.park掛起釋放鎖時(shí)喚醒后繼節(jié)點(diǎn)。“公平鎖和非公平鎖的區(qū)別在于非公平鎖在獲取時(shí)先嘗試一次CAS搶鎖搶不到才排隊(duì)。”如果你能畫出AQS的隊(duì)列結(jié)構(gòu)并解釋為什么用雙向隊(duì)列——因?yàn)樾枰奖愕厝∠却蛦拘押罄^那就已經(jīng)從使用層面上升到設(shè)計(jì)層面。線程池是實(shí)戰(zhàn)高頻問題。ThreadPoolExecutor的七大參數(shù)都知道但面試官真正想聽的是你如何配置。核心線程數(shù)怎么定CPU密集型設(shè)置為CPU核數(shù)1IO密集型通常設(shè)置為2 CPU核數(shù)但更科學(xué)的說法是“線程等待時(shí)間與計(jì)算時(shí)間的比值決定了最佳線程數(shù)”。拒絕策略不是隨便選的CallerRunsPolicy用調(diào)用者線程執(zhí)行任務(wù)能起到背壓效果AbortPolicy會(huì)拋出異常適合及時(shí)暴露問題。還要能解釋為什么不能用Executors.newFixedThreadPool因?yàn)長inkedBlockingQueue無界任務(wù)堆積可能導(dǎo)致OOM。JVM內(nèi)存與垃圾回收是檢驗(yàn)真知的標(biāo)尺JVM的問題逃不掉但很多人只會(huì)背幾塊內(nèi)存區(qū)域。要分層理解線程私有程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧和線程共享堆、方法區(qū)。JDK8把方法區(qū)移出變?yōu)樵臻gMetaspace并直接使用本地內(nèi)存。這一改動(dòng)的深層原因是永久代經(jīng)常OOM而元空間默認(rèn)大小受本地內(nèi)存限制且字符串常量池和類元數(shù)據(jù)的存儲(chǔ)分離。“棧管運(yùn)行堆管存儲(chǔ)”這種話太膚淺更準(zhǔn)確的是“棧幀記錄方法調(diào)用的狀態(tài)堆存對(duì)象的實(shí)例數(shù)據(jù)而對(duì)象類型信息放在元空間”。垃圾回收算法中復(fù)制、標(biāo)記清除、標(biāo)記整理要會(huì)對(duì)比。分代收集理論怎么用新生代用復(fù)制對(duì)象存活率低老年代用標(biāo)記-清除或標(biāo)記-整理對(duì)象存活率高。GC Roots有哪些虛擬機(jī)棧中引用的對(duì)象、靜態(tài)屬性引用的對(duì)象、常量引用的對(duì)象、JNI引用等。要理解“可達(dá)性分析”不是掃描所有對(duì)象而是從GC Roots出發(fā)做圖遍歷因此判斷對(duì)象是否可以回收只看它是否被活動(dòng)對(duì)象引用。接下來是垃圾收集器。從Serial到G1到ZGC要能說出適用場(chǎng)景。G1現(xiàn)在仍是主流它把堆分成Region維護(hù)可預(yù)測(cè)的停頓時(shí)間模型。G1最革命的設(shè)計(jì)是“標(biāo)記清理”可以和用戶線程并發(fā)執(zhí)行并且用Remembered Set來追蹤跨Region引用。如果你想展示深度可以提一下ZGC通過染色指針和讀屏障實(shí)現(xiàn)幾乎零停頓突破了幾十TB堆大小的限制。JVM調(diào)優(yōu)問題不要只背參數(shù)要結(jié)合案例-Xms和-Xmx設(shè)為相同值避免擴(kuò)容抖動(dòng)-XX:HeapDumpOnOutOfMemoryError導(dǎo)出堆快照然后用MAT分析。調(diào)優(yōu)的本質(zhì)不是把參數(shù)調(diào)得花里胡哨而是減少Full GC的頻率和單次時(shí)間。類加載機(jī)制里“雙親委派”必考。為什么需要雙親委派為了安全——防止自定義類覆蓋java.lang.String。破壞雙親委派的例子Tomcat為了加載不同應(yīng)用的同名類JDBC的SPI通過Thread.currentThread().getContextClassLoader()加載驅(qū)動(dòng)實(shí)現(xiàn)。自己能不能寫一個(gè)和String同包名的類不能因?yàn)橐龑?dǎo)類加載器優(yōu)先加載rt.jar里的類。類加載的“可見性”規(guī)則是父加載器看不到子加載器加載的類而子加載器可以看到父加載器加載的類。Spring框架IOC和AOP只是起手式Spring面試題已經(jīng)從“IOC是什么”變成了“Spring是如何解決循環(huán)依賴的”。這個(gè)問題幾乎等于送分題但拿滿分的少。標(biāo)準(zhǔn)答案是單例的setter注入循環(huán)依賴通過三級(jí)緩存解決——一級(jí)緩存放成品對(duì)象二級(jí)緩存放半成品對(duì)象提前暴露引用三級(jí)緩存放ObjectFactory生成代理對(duì)象的工廠。關(guān)鍵點(diǎn)在于三級(jí)緩存的存在是為了支持AOP代理——如果對(duì)象被代理那么其他依賴它的bean拿到的是代理對(duì)象而不是原始對(duì)象。如果面試官問“構(gòu)造函數(shù)注入的循環(huán)依賴能解決嗎”不能因?yàn)闃?gòu)造函數(shù)執(zhí)行時(shí)對(duì)象還沒實(shí)例化沒地方提前暴露。AOP的考點(diǎn)則集中在動(dòng)態(tài)代理JDK動(dòng)態(tài)代理要求接口CGLIB通過繼承實(shí)現(xiàn)。Spring默認(rèn)如果目標(biāo)類有接口就用JDK代理否則用CGLIB。更深刻的理解是AOP的底層不是反射那么簡(jiǎn)單而是代理鏈的組裝通過Advisor匹配切點(diǎn)把多個(gè)增強(qiáng)器包裝成攔截器鏈遞歸調(diào)用時(shí)依次執(zhí)行前置、后置、環(huán)繞通知。事務(wù)管理也是AOP的實(shí)戰(zhàn)應(yīng)用Transactional失效的幾種情況必須牢記方法內(nèi)部自調(diào)用、private方法、異常被catch、傳播行為設(shè)置錯(cuò)誤等。Spring事務(wù)的傳播行為七種中最重要的REQUIRED和REQUIRES_NEW。前者支持當(dāng)前事務(wù)沒有則新建通常默認(rèn)后者掛起當(dāng)前事務(wù)開啟新事務(wù)用于日志記錄或異步操作。事務(wù)失效的本質(zhì)是“事務(wù)管理依賴代理而代理只能在通過bean引用調(diào)用時(shí)生效”。所以自調(diào)用this.method()不走代理事務(wù)自然就沒效果。如果面試官問“一個(gè)大事務(wù)里調(diào)用了另一個(gè)方法想讓這個(gè)方法回滾不影響外層”答案就是REQUIRES_NEW開一個(gè)新事務(wù)讓異常被捕獲后外層按需提交。MySQL索引和事務(wù)是數(shù)據(jù)庫的兩座大山MySQL面試索引模型是首選。InnoDB的BTree為什么好因?yàn)闃涞母叨劝龑泳湍艽媲f級(jí)數(shù)據(jù)葉子節(jié)點(diǎn)用雙向鏈表串聯(lián)范圍查詢時(shí)不需要回溯樹結(jié)構(gòu)。至于為什么不選B-TreeB-Tree的節(jié)點(diǎn)存數(shù)據(jù)導(dǎo)致單節(jié)點(diǎn)能存的鍵值少樹變高且范圍查詢需要中序遍歷。BTree的所有數(shù)據(jù)都在葉子節(jié)點(diǎn)非葉子節(jié)點(diǎn)只存鍵這樣磁盤I/O次數(shù)少且天然支持排序和范圍查詢。還要會(huì)看執(zhí)行計(jì)劃explain中的key、rows、typetype從ref到index到all性能遞減。索引失效的常見場(chǎng)景對(duì)索引列使用函數(shù)、隱式類型轉(zhuǎn)換、最左前綴不滿足、%開頭的模糊查詢。但最深的坑是“優(yōu)化器可能因?yàn)閿?shù)據(jù)分布而選擇全表掃描即使有索引”。索引不是越多越好每個(gè)索引都是一棵BTree寫入時(shí)就要維護(hù)多棵樹犧牲寫性能。聯(lián)合索引的建立原則是“區(qū)分度高、查詢頻繁、最左匹配”。如果面試官問“為什么order by慢”可以提到Using filesort如果排序字段正好是聯(lián)合索引的后續(xù)列可以避免排序。事務(wù)隔離級(jí)別是必考。四個(gè)級(jí)別讀未提交、讀已提交、可重復(fù)讀、串行化。MySQL默認(rèn)是可重復(fù)讀為什么默認(rèn)這個(gè)因?yàn)樵缙诨赽inlog的復(fù)制在read committed下會(huì)出現(xiàn)主從數(shù)據(jù)不一致不對(duì)MySQL在read committed下也可以正常工作。更合理的解釋是MySQL的binlog格式是statement時(shí)在read committed下無法正確復(fù)制因?yàn)镽EAD COMMITTED允許“間隙鎖”釋放而statement日志回放時(shí)會(huì)出現(xiàn)問題。但到了row格式后可重復(fù)讀的默認(rèn)選擇更多是歷史慣性。MVCC是實(shí)現(xiàn)“快照讀”的核心每行記錄隱藏了兩個(gè)隱藏列——事務(wù)ID和回滾指針通過undo log構(gòu)建回滾鏈實(shí)現(xiàn)不同事務(wù)看到不同版本。可重復(fù)讀在第一次讀時(shí)生成快照之后一直讀這個(gè)快照讀已提交是每次讀都生成新快照。至于當(dāng)前讀需要用lock in share mode或for update走的是鎖機(jī)制不是MVCC。鎖問題表鎖、行鎖、間隙鎖、臨鍵鎖。Next-Key Lock是間隙鎖和記錄鎖的結(jié)合前開后閉區(qū)間用于解決幻讀。在可重復(fù)讀級(jí)別下MySQL通過Next-Key Lock鎖住查詢范圍防止其他事務(wù)插入新記錄從而保證當(dāng)前讀的一致性。如果范圍查詢的記錄不存在會(huì)退化為純間隙鎖。還有死鎖的排查show engine innodb status查看最近死鎖信息分析兩個(gè)事務(wù)持有哪些鎖、等待哪些鎖通常通過調(diào)整事務(wù)順序或縮小鎖范圍解決。Redis緩存三兄弟和分布式鎖Redis在面試中幾乎和Java一樣重要。首先是緩存穿透、擊穿、雪崩三兄弟。穿透請(qǐng)求的數(shù)據(jù)在緩存和數(shù)據(jù)源中都不存在用“布隆過濾器”或“緩存空對(duì)象”解決。擊穿某個(gè)熱點(diǎn)key過期瞬間大量請(qǐng)求打到DB用“互斥鎖”或“熱點(diǎn)key永不過期邏輯過期”解決。雪崩多個(gè)key同時(shí)過期或Redis宕機(jī)用“過期時(shí)間加隨機(jī)值”、“集群高可用”、“限流降級(jí)”解決。這三件事拷問的不是你記住了多少方案而是你是否理解“緩存是擋在DB前面的第一道防線防不住就要保護(hù)DB”。Redis持久化RDB和AOF。RDB是某一時(shí)刻的內(nèi)存快照適合備份和重啟加載AOF記錄寫命令文件大、恢復(fù)慢但安全性高。混合持久化AOF重寫時(shí)把RDB內(nèi)容作為AOF文件開頭這樣加載快且丟失最少。面試官喜歡問“RDB持久化時(shí)還能處理寫命令嗎”答案是能的因?yàn)镽edis通過fork子進(jìn)程生成快照父進(jìn)程繼續(xù)處理命令寫時(shí)復(fù)制COW保證數(shù)據(jù)一致性。如果還能提到appendfsync的everysec是默認(rèn)配置每秒刷盤最多丟一秒數(shù)據(jù)就很完善了。分布式鎖是應(yīng)用層高頻題。簡(jiǎn)單的setnx命令容易踩坑要設(shè)置過期時(shí)間要用set key value nx ex 3000原子命令。釋放鎖時(shí)要先驗(yàn)證value是否是自己設(shè)的用Lua腳本保證“檢查刪除”的原子性。更健壯的方案是Redisson的看門狗機(jī)制自動(dòng)續(xù)期。真正的資深回答會(huì)指出Redis分布式鎖不是銀彈在主從切換下可能丟失鎖如果必須強(qiáng)一致要用Redlock或ZooKeeper鎖。但Redlock本身也有爭(zhēng)議馬丁·克萊普曼專門批判過它。能說出這個(gè)爭(zhēng)議說明你讀過分布式系統(tǒng)的經(jīng)典討論。面試不是背誦而是用體系串聯(lián)碎片把上面這些知識(shí)點(diǎn)串起來看Java面試其實(shí)考察的是“三條線”第一條線是語言基礎(chǔ)從集合到并發(fā)到JVM反映你寫代碼時(shí)知不知道底層在發(fā)生什么第二條線是框架從Spring到數(shù)據(jù)庫到緩存反映你構(gòu)建系統(tǒng)時(shí)能不能理清組件之間的協(xié)作第三條線是“為什么”每個(gè)高頻問題背后都有設(shè)計(jì)權(quán)衡比如HashMap為什么用紅黑樹而不是AVL樹因?yàn)榧t黑樹插入刪除的旋轉(zhuǎn)次數(shù)更少。Spring為什么用三級(jí)緩存因?yàn)橐患?jí)放不下代理對(duì)象二級(jí)解決不了循環(huán)引用。MySQL為什么默認(rèn)可重復(fù)讀因?yàn)樵缙谥鲝膹?fù)制要保證一致性。面試官想看到的不是你背誦的答案而是你在腦海里構(gòu)建的一張知識(shí)圖譜每個(gè)節(jié)點(diǎn)都有連接。所以準(zhǔn)備面試時(shí)不要一條一條背八股文。試著從一道題切入比如“HashMap為什么線程不安全”你會(huì)引出數(shù)據(jù)覆蓋、死循環(huán)、ConcurrentHashMap然后到CAS到鎖到AQS到線程池到JUC工具類再到Spring的單例池循環(huán)依賴……一路追問下去你會(huì)把所有高頻考點(diǎn)串成一張網(wǎng)。把知識(shí)織成網(wǎng)而不是堆成山這才是應(yīng)對(duì)Java面試的最優(yōu)策略。祝你拿下心儀offer。