優(yōu)實(shí)戰(zhàn))
Java 高頻面試題JVM 深度解析與性能調(diào)優(yōu)實(shí)戰(zhàn)附詳細(xì)答案 目錄JVM 內(nèi)存模型詳解垃圾回收算法垃圾收集器對(duì)比GC 日志分析實(shí)戰(zhàn)OOM 問(wèn)題排查JVM 參數(shù)調(diào)優(yōu)實(shí)戰(zhàn)常用工具使用指南經(jīng)典面試真題一、JVM 內(nèi)存模型詳解 ?????1.1 JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)┌─────────────────────────────────────┐ │ CPU Registers │ ├─────────────────────────────────────┤ │ Stack (棧) │ │ ┌──────────────────────────┐ │ │ │ Frame 3: methodC() │ │ │ ├──────────────────────────┤ │ │ │ Frame 2: methodB() │ │ │ ├──────────────────────────┤ │ │ │ Frame 1: methodA() │ │ │ └──────────────────────────┘ │ ├─────────────────────────────────────┤ │ Native Method Stack │ ├─────────────────────────────────────┤ │ Heap (堆) │ │ ┌──────────────────────────────┐ │ │ │ Metaspace / PermGen │ │ │ ├── 類信息、方法字節(jié)碼等 │ │ │ ├── │ │ │ ├── 新生代 (Young Gen) │ │ │ │ ├── Eden Space │ │ │ │ └── Survivor From/To │ │ │ ├── │ │ │ └── 老年代 (Old Gen) │ │ │ └──────────────────────────────┘ │ ├─────────────────────────────────────┤ │ Program Counter Register │ │ │ │ ├─────────────────────────────────────┤ │ Method Area (方法區(qū)) │ │ │ └── 靜態(tài)變量、常量池等 │ │ └─────────────────────────────────────┘1.2 各區(qū)域詳細(xì)說(shuō)明區(qū)域線程共享性存儲(chǔ)內(nèi)容溢出異常程序計(jì)數(shù)器線程私有當(dāng)前執(zhí)行字節(jié)碼行號(hào)無(wú)Java 虛擬機(jī)棧線程私有局部變量表、操作數(shù)棧、幀StackOverflowError / OutOfMemoryError本地方法棧線程私有Native 方法執(zhí)行環(huán)境StackOverflowError / OutOfMemoryError堆全局共享對(duì)象實(shí)例、數(shù)組OutOfMemoryError方法區(qū)全局共享類信息、常量、靜態(tài)變量OutOfMemoryError運(yùn)行時(shí)常量池全局共享編譯期生成的各種字面量和符號(hào)引用OutOfMemoryError1.3 堆內(nèi)存結(jié)構(gòu)Eden 區(qū) → Minor GC 頻繁發(fā)生 ┌──────────────────────────────────────┐ │ Survivor From │ Survivor To │ │ (from sgc) │ (to sgc) │ └──────────────────────────────────────┘ ↓ Old Gen (老年代) ┌─────────────────────────────────────┐ │ │ │ CMS / G1 大對(duì)象區(qū) │ │ │ └─────────────────────────────────────┘ ↓ Permanent/Metaspace (元空間)1.4 對(duì)象優(yōu)先在 Eden 分配// 默認(rèn)情況下新對(duì)象分配在 Eden 區(qū)ObjectobjnewObject();// Exception: 大對(duì)象直接分配老年代byte[]largeDatanewbyte[10*1024*1024];// 10MB -XX:PretenureSizeThreshold// Exception: 長(zhǎng)期存活對(duì)象晉升老年代for(inti0;i100;i){objects.add(newObject());// 經(jīng)歷多次 GC 仍存活}// Exception: 動(dòng)態(tài)年齡判斷// 如果 Survivor 中相同年齡的對(duì)象總和 50%則年齡最大的對(duì)象直接進(jìn)入老年代二、垃圾回收算法 ?????2.1 判斷對(duì)象可回收方法一引用計(jì)數(shù)法優(yōu)點(diǎn)實(shí)現(xiàn)簡(jiǎn)單判定效率高 缺點(diǎn)無(wú)法解決循環(huán)引用問(wèn)題// 引用計(jì)數(shù)方案示意classA{Brefnull;}classB{Arefnull;}publicvoidtest(){AanewA();BbnewB();a.refb;// a-ref 引用 bb.refa;// b-ref 引用 a// 此時(shí) a、b 的引用計(jì)數(shù)都 1anull;bnull;// 引用計(jì)數(shù)都是 1GC 無(wú)法回收(形成循環(huán)引用)}方法二根搜索算法根集合 (GC Roots): ├── 虛擬機(jī)棧中的引用 ├── 方法區(qū)靜態(tài)屬性引用 ├── 方法區(qū)常量引用 └── native 方法引用的對(duì)象 GC Roots → 對(duì)象圖可達(dá) → 對(duì)象存活 GC Roots → 對(duì)象圖不可達(dá) → 對(duì)象可回收2.2 四種標(biāo)記 - 清除算法Mark-Sweep (標(biāo)記 - 清除)步驟 1. 從 GC Roots 開始遍歷 2. 標(biāo)記所有可達(dá)對(duì)象 3. 掃描堆回收未標(biāo)記對(duì)象 優(yōu)點(diǎn)簡(jiǎn)單直觀 缺點(diǎn) - 效率低兩次掃描 - 空間碎片化圖示Mark-Sweep: [●●○●][●●●○][○○●●][●○○○] ↑標(biāo)記 ↑保留 ?清除 ?清除 → 結(jié)果[●●][●●●] [碎片][碎片]Copying (復(fù)制算法)步驟 1. 將堆分為兩半From To 2. GC 時(shí)只保留 From 存活對(duì)象 3. 復(fù)制到 To 中清理 From 優(yōu)點(diǎn) - 無(wú)碎片產(chǎn)生 - 速度快 缺點(diǎn) - 內(nèi)存利用率只有 50%應(yīng)用新生代Eden SurvivorBefore: [Live][Dead][Live][Dead][Dead] From: ██████ ████ ████ ████ ████ To: ?????? ???? ???? ???? ???? Copy After: From: ?????? ???? ???? ???? ???? ← 清空 To: ???? ???? ???? ???? ???? ↑ ↑ Eden SurvivorMark-Clean (標(biāo)記 - 整理)步驟 1. 標(biāo)記存活對(duì)象 2. 向一端移動(dòng)存活對(duì)象 3. 清理邊界外的內(nèi)存 優(yōu)點(diǎn) - 無(wú)碎片 - 內(nèi)存利用率高 缺點(diǎn) - 移動(dòng)開銷大應(yīng)用老年代2.3 三種算法組合策略新生代Copying 算法 (效率高) ├── Eden: 70% ├── Survivor From: 15% └── Survivor To: 15% 老年代Mark-Clean 算法 (考慮碎片) └── 適合大量對(duì)象存活場(chǎng)景 混合策略 CMS Mark-Sweep Parallel G1 Region 劃分 Mark-Clean三、垃圾收集器對(duì)比 ?????3.1 JDK 主流收集器總覽Serial Collector ├── Single Thread ├── Stop-The-World └── 適用于客戶端模式 Parallel Scavenge ├── Multi Thread ├── 吞吐量?jī)?yōu)先 └── 適用于后臺(tái)計(jì)算 CMS (Java 9 廢棄) ├── Low Latency ├── Concurrent GC └── 適用于交互式應(yīng)用 G1 (Java 9 默認(rèn)) ├── Region 劃分 ├── Predictable Pause Time └── 適用于多核 大內(nèi)存 ZGC (Java 11) ├── Sub-millisecond pause times ├── Colossal heap support └── 超大規(guī)模數(shù)據(jù)集3.2 CMS 收集器原理CMS Concurrent Mark Sweep (并發(fā)標(biāo)記清除) 四個(gè)主要步驟 1. Initial Mark (STW) └── 標(biāo)記 GC Roots 直接關(guān)聯(lián)的對(duì)象 2. Concurrent Mark └── 追蹤引用關(guān)系并發(fā)標(biāo)記 3. Reset Mark (STW) └── 處理用戶線程產(chǎn)生的修改 4. Concurrent Sweep └── 并發(fā)回收未標(biāo)記對(duì)象 額外階段 - Remark (STW) - Re-scan (優(yōu)化) 觸發(fā)條件 - Concurrent Mode Failure → 降級(jí)為 Serial Old3.3 G1 收集器原理G1 Garbage First (分代收集的進(jìn)化版) 核心特性 1. 將堆劃分為多個(gè) Region (大小固定最大 32MB) └── Region 不固定屬于新生代或老年代 2. Region 角色 - 初始為空 - 可以是 Eden、From、To - 也可以是 Old 3. Remembered Set (記錄跨 Region 引用) └── 避免全圖掃描 GC 過(guò)程 1. Young GC └── 類似 Parallel Scavenge 2. Mixed GC └── 回收部分老年代 新生代 3. Full GC (很少觸發(fā)) └── CMS 降級(jí)后的兜底方案3.4 收集器對(duì)比一覽表收集器新生代老年代線程停頓時(shí)間吞吐量適用場(chǎng)景SerialYesYesSingle長(zhǎng)低客戶端/小內(nèi)存ParNewYesCMSMulti中等中等開發(fā)測(cè)試Parallel ScavengeYesOldMulti短高后臺(tái)計(jì)算CMSParNewCMSConcurrent短中高響應(yīng)要求高G1YesYesConcurrent可控高現(xiàn)代應(yīng)用推薦ZGCYesYesConcurrent1ms高超大內(nèi)存四、GC 日志分析實(shí)戰(zhàn) 4.1 開啟 GC 日志配置# JDK 8-Xloggc:/tmp/gc.log-verbose:gc-XX:PrintGCDetails-XX:PrintGCDateStamps-XX:PrintGCCause-XX:PrintTenuringDistribution-XX:PrintFLSStatistics# JDK 9-Xlog:gc*:file/tmp/gc.log:time,uptime,tags:filecount5,filesize10M4.2 完整 GC 日志示例[GC (Allocation Failure) [PSYoungGen: 113199K-1350K(123392K)] 385515K-237028K(385152K), 0.0218788 secs] [Full GC (Ergonomics) [PSYoungGen: 1536K-0K(123392K)] [ParOldGen: 236979K-233746K(261760K)] 238515K-233746K(385152K), [Metaspace: 34112K-34112K(1068952K)], 0.0921234 secs]4.3 關(guān)鍵字段解讀[GC (Allocation Failure) ↑ └── 原因Eden 區(qū)滿Minor GC [PSYoungGen: 113199K-1350K(123392K)] └── 新生代狀態(tài) 前113MB →后1.3MB 總120MB 385515K-237028K(385152K) └── 堆整體 前385MB →后237MB 總385MB 0.0218788 secs] └── GC 耗時(shí)21.8ms4.4 識(shí)別 GC 類型# Minor GC 特征[GC (Allocation Failure)...[GC (Metadata GC Threshold)...[GC (Ergonomics)...# Full GC 特征[Full GC (原因)...[Full GC (Heap Dump)...[Full GC (System.gc() called)...# CMS GC 特征[CMS-initial-mark:0.0035s...[CMS-concurrent-mark:0.0450s[CMS-sweep:0.0025s...# G1 GC 特征[G1 Pause (G1 Humongous Region)...[G1 Evacuation Failure because of Allocation Fails...4.5 GC 頻率分析實(shí)戰(zhàn)正常情況1 秒 1 次 Minor GC 10 分鐘 1 次 Full GC ?異常情況1 分鐘 3 次 Full GC ? → 可能是內(nèi)存泄漏 每秒 1 次 Minor GC ? → 對(duì)象創(chuàng)建過(guò)快4.6 常見 GC 問(wèn)題分析現(xiàn)象可能原因解決方案頻繁 Full GC老年代快滿了增大老年代、檢查內(nèi)存泄漏Long STWG1 暫停過(guò)長(zhǎng)調(diào)整-XX:MaxGCPauseMillisPromotion Failed晉升失敗增大新生代、提前晉升閾值CMS 并發(fā)失敗內(nèi)存不足增加堆、降低啟動(dòng)參數(shù)GC 時(shí)間占比高回收率低優(yōu)化代碼、減少對(duì)象創(chuàng)建五、OOM 問(wèn)題排查流程 5.1 OOM 常見類型// 1. Heap Space OOM// 原因內(nèi)存不足newbyte[100*1024*1024];// 100MB// 2. Metaspace OOM// 原因類加載過(guò)多// 動(dòng)態(tài)代理生成大量類 → MetaSpace 耗盡// 3. GC Overhead Limit Exceeded// 原因GC 時(shí)間超過(guò) 98%回收內(nèi)存少于 2%// JVM 認(rèn)為程序正在OOM邊緣 → 主動(dòng)退出// 4. Request stack size// 原因遞歸過(guò)深voiddeepRecursive(){deepRecursive();// StackOverflowError}// 5. Unable to create new native thread// 原因線程數(shù)超限5.2 OOM 排查流程圖發(fā)現(xiàn) OOM ↓ 檢查錯(cuò)誤日志 ↓ 確定 OOM 類型 ↓ 現(xiàn)場(chǎng)采樣分析 ├── jmap -histo:live ├── jmap -dump:formatb,filexxx.hprof └── MAT/VisualVM 分析 ↓ 定位問(wèn)題根因 ├── 內(nèi)存泄漏 ├── 內(nèi)存溢出 └── GC 設(shè)置不合理 ↓ 修復(fù)驗(yàn)證5.3 內(nèi)存泄漏 vs 內(nèi)存溢出內(nèi)存泄漏 (Memory Leak):ListStringlistnewArrayList();while(true){StringdatafetchData();list.add(data);// 只增不減}// 結(jié)果不斷增長(zhǎng)最終 OOM內(nèi)存溢出 (OutOfMemory):// 需求 可用資源newbyte[1GB];// 堆只有 512MB// 直接 OOM5.4 Dump 文件分析實(shí)戰(zhàn)# JDK 8 導(dǎo)出 dumpjmap-dump:formatb,file/tmp/dump.hprofpid# JDK 9 導(dǎo)出 dumpjcmdpidGC.heap_dump /tmp/dump.hprof分析工具Eclipse MAT (Memory Analyzer Tool) ? 推薦VisualVMJProfilerYourKitMAT 分析重點(diǎn)Dominator Tree - 查看占用內(nèi)存最多的對(duì)象Histogram - 按類統(tǒng)計(jì)對(duì)象數(shù)量Path to GC Roots - 查找泄露鏈路5.5 實(shí)際案例線上服務(wù) OOM問(wèn)題現(xiàn)象每天下午 3 點(diǎn)定時(shí)崩潰 GC 日志顯示 Full GC 后內(nèi)存依然很高排查步驟# 1. 獲取 dump 文件jmap -dump:live,formatb,fileoom.hprof25918# 2. MAT 分析- Top Largest Objects:10萬(wàn)條訂單數(shù)據(jù) - Dominator Tree: QueryDAO.queryAll()返回對(duì)象未釋放 - 結(jié)論查詢?nèi)繑?shù)據(jù)導(dǎo)致內(nèi)存不足根本原因// ? 錯(cuò)誤代碼OverridepublicListOrderfindAllOrders(){returnorderMapper.selectAll();// 一次性查 10 萬(wàn)條}// ? 正確代碼OverridepublicListOrderfindAllOrders(intpage,intsize){returnorderMapper.selectAllPage(page,size);// 分頁(yè)查詢}解決方案改為分頁(yè)查詢添加索引優(yōu)化 SQL增加堆內(nèi)存到 4GB六、JVM 參數(shù)調(diào)優(yōu)實(shí)戰(zhàn) ????6.1 堆空間調(diào)整# 基礎(chǔ)參數(shù)-Xms# 初始堆大小-Xmx# 最大堆大小-Xss# 線程棧大小 (默認(rèn) 1MB)# 建議設(shè)置-Xms8g-Xmx8g# 讓初始值最大值避免擴(kuò)容# Xss 根據(jù)線程數(shù)調(diào)整-Xss256k# 線程密集型應(yīng)用減少每線程占用6.2 新生代調(diào)整# 新生代比例-Xmn4g# 強(qiáng)制指定新生代大小-XX:NewRatio2# 老年代/新生代2:1# Survivor 區(qū)比例-XX:SurvivorRatio8# Eden/Survivor8:1# 默認(rèn) Edn:SurF8:1:1即 SurF占 10%# 對(duì)象晉升閾值-XX:PretenureSizeThreshold10m# 大對(duì)象直接在老年代-XX:MaxTenuringThreshold15# 最大晉升年齡6.3 CMS 調(diào)優(yōu)# CMS 參數(shù)集合-XX:UseConcMarkSweepGC# 啟用 CMS-XX:UseCMSCompactAtEnd# 結(jié)束時(shí)空整理-XX:CMSInitiatingOccupancyRatio70# 觸發(fā)閾值 70%-XX:ExplicitGCInvokesConcurrent# System.gc() 并發(fā)回收# 并發(fā)級(jí)別-XX:ConcGCThreads4# 并發(fā)線程數(shù)6.4 G1 調(diào)優(yōu)# G1 參數(shù)集合-XX:UseG1GC# 啟用 G1-XX:MaxGCPauseMillis200# 目標(biāo)停頓時(shí)間-XX:G1HeapRegionSize16m# Region 大小# 堆限制-XX:G1ReservePercent10# 預(yù)留空間 10%-XX:InitiatingHeapOccupancyPercent45# TSAT 閾值 45%# 大對(duì)象-XX:G1NewSizePercent30# 新生代最小占比-XX:G1MaxNewSizePercent60# 新生代最大占比6.5 調(diào)優(yōu)策略高吞吐場(chǎng)景 (大數(shù)據(jù)處理): ├── 選擇Parallel Scavenge Parallel Old ├── 策略最大化吞吐量 ├── 參數(shù)-XX:MaxGCPauseMillis不限制 └── 優(yōu)勢(shì)CPU 利用率高 低延遲場(chǎng)景 (Web 服務(wù)): ├── 選擇CMS or G1 ├── 策略控制停頓時(shí)間 ├── 參數(shù)-XX:MaxGCPauseMillis200ms └── 優(yōu)勢(shì)響應(yīng)穩(wěn)定 大內(nèi)存場(chǎng)景 (10GB): ├── 必須選G1 or ZGC ├── 禁用Serial/Paralle/New └── 優(yōu)勢(shì)預(yù)測(cè)性停頓時(shí)間七、常用工具使用指南7.1 jps - JVM 進(jìn)程狀態(tài)jps-l# 顯示主類名jps-v# 顯示 JVM 參數(shù)jps-m# 顯示主方法參數(shù)7.2 jstat - JVM 統(tǒng)計(jì)監(jiān)控jstat-gcutilpid1000# 每 1 秒打印 GC 統(tǒng)計(jì)jstat-gccapacitypid# 打印堆容量jstat-gcnewpid# 新生代 GCjstat-gcoldpid# 老年代 GCjstat-classpid# 類加載統(tǒng)計(jì)7.3 jmap - 內(nèi)存映像# 生成 dumpjmap-dump:formatb,fileheap.hprofpid# 對(duì)象直方圖jmap-histo:livepid# 查看特定對(duì)象jmap-histo:live-clpid7.4 jstack - 線程堆棧# 打印線程快照jstackpid# 指定文件保存jstackpidthread_dump.txt# 死鎖檢測(cè)jstack-lpid|grep-A10found deadlock7.5 jhat - HTTP 查看器jhat -J-Xmx4g heap.hprof# 分析 dump 文件啟動(dòng) HTTP 服務(wù)器# 訪問(wèn) http://localhost:70007.6 VisualVM - 可視化監(jiān)控功能CPU 監(jiān)控內(nèi)存使用曲線線程視圖堆轉(zhuǎn)儲(chǔ)分析性能火焰圖安裝方式# JDK 6-11 自帶$JAVA_HOME/bin/jvisualvm# JDK 12 需單獨(dú)下載https://visualvm.github.io/八、經(jīng)典面試真題 Q1: 談?wù)剬?duì) JVM 內(nèi)存模型的理解A:JVM 內(nèi)存模型分為 5 大部分 1. 程序計(jì)數(shù)器 - 線程私有記錄當(dāng)前執(zhí)行的字節(jié)碼行號(hào) 2. Java 虛擬機(jī)棧 - 線程私有存儲(chǔ)棧幀局部變量表、操作數(shù)棧等 3. 本地方法棧 - 線程私有支持 Native 方法 4. 堆 - 線程共享存放對(duì)象實(shí)例是 GC 管理的核心區(qū)域 5. 方法區(qū) - 線程共享存儲(chǔ)類信息、常量、靜態(tài)變量 堆內(nèi)存再細(xì)分 - 新生代Eden Survivor(From/To) - 老年代存放長(zhǎng)期存活對(duì)象 - 元空間 (JDK 8): 替代永久代存放在本地內(nèi)存Q2: 哪些對(duì)象可以作為 GC Roots?A:可以作為 GC Roots 的包括 1. 虛擬機(jī)棧中引用的對(duì)象 2. 方法區(qū)中類靜態(tài)屬性引用的對(duì)象 3. 方法區(qū)中常量引用的對(duì)象 4. 本地方法棧中 JNI 引用的對(duì)象 此外還有兩個(gè)特殊 GC Roots: - ClassLoader - Monitor(用于 synchronized 實(shí)現(xiàn))Q3: CMS 和 G1 的區(qū)別A:相同點(diǎn) - 都是基于標(biāo)記 - 清除算法 - 都支持并發(fā)執(zhí)行 - 都追求低停頓時(shí)間 不同點(diǎn) 1. 空間管理 - CMS: 連續(xù)堆空間 - G1: 劃分為多個(gè)獨(dú)立的 Region 2. 垃圾回收方式 - CMS: 分代收集新生代用復(fù)制老年代用標(biāo)記清除 - G1: 整體看做標(biāo)記整理局部可以復(fù)制 3. 可控性 - CMS: 只能盡量縮短停頓 - G1: 可設(shè)定目標(biāo)停頓時(shí)間 (-XX:MaxGCPauseMillis) 4. 內(nèi)存碎片 - CMS: 容易產(chǎn)生碎片 - G1: 通過(guò)整理避免碎片 推薦生產(chǎn)環(huán)境推薦使用 G1CMS 已經(jīng)過(guò)時(shí)Q4: 如何判斷一個(gè)對(duì)象是否可以被回收A:兩種主要方法 1. 引用計(jì)數(shù)法 - 每個(gè)對(duì)象維護(hù)一個(gè)引用計(jì)數(shù)器 - 有引用則計(jì)數(shù) 1斷開引用則計(jì)數(shù) -1 - 計(jì)數(shù)為 0 則可回收 - 問(wèn)題無(wú)法解決循環(huán)引用 2. 可達(dá)性分析 (GC Roots) - 從 GC Roots 向下搜索 - 路徑下的對(duì)象均為存活對(duì)象 - 不可達(dá)的對(duì)象可回收 - JDK 統(tǒng)一采用此方法 第二次篩選 - 對(duì)象初判死亡但 equalsHashCode() 可拯救 - Soft Reference 軟引用可挽留Q5: 如何定位 OOM 問(wèn)題A:標(biāo)準(zhǔn)排查流程 1. 分析錯(cuò)誤日志確定 OOM 類型 2. 現(xiàn)場(chǎng)保存 - jmap -dump:formatb,filexxx.hprof pid - jstack pid thread_dump.txt 3. 使用工具分析 - MAT (推薦) - VisualVM - JProfiler 4. 關(guān)鍵分析項(xiàng) - Dominator Tree - 內(nèi)存占用 Top 對(duì)象 - Path to GC Roots - 尋找泄露鏈路 - Histogram - 對(duì)象數(shù)量統(tǒng)計(jì) 5. 常見原因 - 內(nèi)存泄漏對(duì)象生命周期過(guò)長(zhǎng) - 內(nèi)存溢出超出堆大小 - GC 閾值設(shè)置不合理 - 大對(duì)象占用過(guò)多 解決優(yōu)化代碼、調(diào)整參數(shù)、增加內(nèi)存 參考資料《深入理解 Java 虛擬機(jī)》第 3 版 - 周志明Oracle 官方文檔Alibaba Java Coding Guidelines各類開源項(xiàng)目最佳實(shí)踐 更多技術(shù)文章持續(xù)更新中…關(guān)注作者獲取更多 Java 技術(shù)干貨如果覺(jué)得有用歡迎點(diǎn)贊收藏轉(zhuǎn)發(fā)有任何問(wèn)題歡迎評(píng)論區(qū)交流~