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