控到調(diào)優(yōu):構(gòu)建JVM性能保障體系的實(shí)戰(zhàn)指南)
1. 項(xiàng)目概述從“救火”到“治未病”的JVM調(diào)優(yōu)之路干了這么多年Java后端最怕半夜被電話叫醒一看監(jiān)控告警“服務(wù)響應(yīng)時(shí)間飆升”、“Full GC頻繁”。這種場(chǎng)景相信不少同行都深有體會(huì)。JVM問(wèn)題分析調(diào)優(yōu)聽(tīng)起來(lái)是個(gè)老生常談的話題但真正能把它做透、做到“治未病”的團(tuán)隊(duì)并不多。很多時(shí)候我們都是在問(wèn)題爆發(fā)后才手忙腳亂地登錄服務(wù)器拉取堆棧分析日志整個(gè)過(guò)程充滿了被動(dòng)和不確定性。這個(gè)項(xiàng)目或者說(shuō)這份經(jīng)驗(yàn)總結(jié)就是想把這種被動(dòng)的“救火”狀態(tài)轉(zhuǎn)變?yōu)橹鲃?dòng)的、系統(tǒng)化的性能保障體系。它不僅僅是解決一兩個(gè)OutOfMemoryError更是要建立起一套從監(jiān)控、分析、定位到優(yōu)化驗(yàn)證的完整方法論讓JVM這個(gè)黑盒變得可控、可觀測(cè)、可優(yōu)化。核心要解決的就是三個(gè)層面的問(wèn)題首先是“看得見(jiàn)”即我們需要什么樣的監(jiān)控指標(biāo)來(lái)第一時(shí)間發(fā)現(xiàn)問(wèn)題其次是“看得懂”即拿到一堆GC日志、線程Dump、堆Dump后如何快速定位到根因最后是“治得好”即針對(duì)不同的根因如內(nèi)存泄漏、不合理的內(nèi)存分配、鎖競(jìng)爭(zhēng)、不恰當(dāng)?shù)腉C參數(shù)等我們有哪些經(jīng)過(guò)驗(yàn)證的優(yōu)化手段可以實(shí)施并且如何評(píng)估優(yōu)化效果。這個(gè)過(guò)程會(huì)涉及到JVM內(nèi)存模型、垃圾回收機(jī)制、線程模型等核心原理的理解以及像jstack、jmap、jstat、MAT、Arthas等一系列工具的組合運(yùn)用。接下來(lái)我就結(jié)合多次“踩坑”和“填坑”的經(jīng)歷把這套經(jīng)驗(yàn)系統(tǒng)地拆解一遍。2. 核心監(jiān)控體系搭建問(wèn)題發(fā)現(xiàn)的“眼睛”沒(méi)有監(jiān)控調(diào)優(yōu)就是盲人摸象。一套好的監(jiān)控體系能讓我們?cè)谟脩舾兄絾?wèn)題之前就發(fā)現(xiàn)異常這是主動(dòng)調(diào)優(yōu)的前提。2.1 必須監(jiān)控的黃金指標(biāo)監(jiān)控指標(biāo)不在多而在精。以下這幾類指標(biāo)是必須持續(xù)關(guān)注的GC相關(guān)指標(biāo)這是JVM健康度的最直接反映。Young GC / Full GC 頻率與耗時(shí)通過(guò)jstat -gcutil或JMX暴露的指標(biāo)獲取。重點(diǎn)關(guān)注Full GC的頻率和單次耗時(shí)。如果Full GC頻繁如幾分鐘一次或單次耗時(shí)過(guò)長(zhǎng)超過(guò)1秒就是明確的告警信號(hào)。Young GC的頻率和平均耗時(shí)則反映了新生代對(duì)象的分配和回收速度。各內(nèi)存區(qū)域使用率Eden區(qū)、Survivor區(qū)、老年代、元空間Metaspace的使用率。老年代使用率持續(xù)緩慢增長(zhǎng)最后觸發(fā)Full GC是典型的內(nèi)存泄漏跡象。元空間使用率異常增長(zhǎng)則可能跟動(dòng)態(tài)類加載、反射濫用有關(guān)。內(nèi)存與線程指標(biāo)堆內(nèi)存使用趨勢(shì)觀察總堆內(nèi)存的使用量是否呈階梯式上升內(nèi)存泄漏還是穩(wěn)定在一個(gè)水位線。非堆內(nèi)存Off-Heap如果使用了Netty、DirectByteBuffer等必須監(jiān)控堆外內(nèi)存的使用情況防止OutOfDirectMemoryError。線程狀態(tài)監(jiān)控總線程數(shù)、處于BLOCKED、WAITING、TIMED_WAITING狀態(tài)的線程數(shù)。大量線程阻塞往往是慢SQL、鎖競(jìng)爭(zhēng)或外部服務(wù)調(diào)用超時(shí)的表現(xiàn)。系統(tǒng)資源指標(biāo)CPU使用率區(qū)分系統(tǒng)CPU和用戶CPU。如果用戶CPU持續(xù)很高且主要消耗在GC線程上GCT時(shí)間占比高說(shuō)明GC壓力巨大。如果消耗在應(yīng)用線程上則可能是計(jì)算密集型任務(wù)或出現(xiàn)了死循環(huán)。系統(tǒng)負(fù)載Load Average結(jié)合CPU核心數(shù)看如果長(zhǎng)期高于核心數(shù)說(shuō)明系統(tǒng)資源緊張。磁盤I/O與網(wǎng)絡(luò)I/O對(duì)于頻繁讀寫日志、做文件操作的應(yīng)用磁盤I/O可能成為瓶頸。實(shí)操心得不要只盯著平均值。很多問(wèn)題在平均值上表現(xiàn)正常但在P95、P99分位數(shù)上已經(jīng)惡化。務(wù)必配置分位數(shù)監(jiān)控和告警。例如應(yīng)用TP99響應(yīng)時(shí)間突然從50ms上升到200ms但平均響應(yīng)時(shí)間可能變化不大此時(shí)就需要結(jié)合GC日志看是否在TP99時(shí)間點(diǎn)附近發(fā)生了STWStop-The-World的GC。2.2 日志收集與關(guān)鍵信息抓取監(jiān)控指標(biāo)告訴我們“不對(duì)勁”但具體哪里不對(duì)勁需要更詳細(xì)的日志來(lái)定位。開(kāi)啟并歸檔詳細(xì)的GC日志這是分析GC問(wèn)題的基石。JVM啟動(dòng)參數(shù)中必須加上-Xlog:gc*,gcheapdebug,gcagetrace:filegc.log:time,uptime,level,tags:filecount10,filesize100M或者對(duì)于較老的JDK 8-XX:PrintGCDetails -XX:PrintGCDateStamps -XX:PrintGCTimeStamps -Xloggc:/path/to/gc.log關(guān)鍵是要配置日志滾動(dòng)防止打滿磁盤。這些日志記錄了每一次GC的起因、類型、前后內(nèi)存變化、耗時(shí)等詳細(xì)信息。配置OOM時(shí)的自動(dòng)Dump當(dāng)發(fā)生OutOfMemoryError時(shí)自動(dòng)生成堆轉(zhuǎn)儲(chǔ)文件Heap Dump這是分析內(nèi)存泄漏的“現(xiàn)場(chǎng)快照”。-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/path/to/heapdump.hprof準(zhǔn)備線上診斷工具在容器化環(huán)境中提前將診斷工具打入基礎(chǔ)鏡像或確保有權(quán)限快速安裝。Arthas是首選它可以在不重啟應(yīng)用的情況下進(jìn)行方法調(diào)用追蹤、查看實(shí)時(shí)線程堆棧、監(jiān)控方法耗時(shí)等是線上問(wèn)題定位的神器。3. 問(wèn)題根因定位從現(xiàn)象到本質(zhì)的“偵探”過(guò)程當(dāng)告警響起我們需要像偵探一樣根據(jù)線索監(jiān)控指標(biāo)找到兇手根因。下面是一個(gè)典型的排查流程。3.1 性能下降/延遲毛刺排查流程確認(rèn)現(xiàn)象查看監(jiān)控是全局性能下降還是局部毛刺響應(yīng)時(shí)間變長(zhǎng)是否伴隨錯(cuò)誤率上升關(guān)聯(lián)資源檢查對(duì)應(yīng)時(shí)間點(diǎn)的CPU、GC、線程狀態(tài)。如果CPU飆升且GC時(shí)間同步飆升問(wèn)題很可能在GC。分析GC日志使用gceasy.io在線或GCViewer本地工具分析GC日志文件。重點(diǎn)關(guān)注Full GC原因是“Metadata GC Threshold”元空間不足、“Ergonomics”自適應(yīng)調(diào)整還是“System.gc()”調(diào)用GC前后內(nèi)存回收效果如果Full GC后老年代內(nèi)存回收很少說(shuō)明大部分對(duì)象是存活的可能是內(nèi)存泄漏也可能是堆大小設(shè)置不合理。暫停時(shí)間觀察所有STW事件的持續(xù)時(shí)間是否與應(yīng)用延遲毛刺時(shí)間吻合。檢查線程如果CPU高但GC正常使用jstack -l多次如間隔5秒抓取線程堆棧然后用fastthread.io這類工具分析。尋找相同的堆棧軌跡大量線程卡在同一個(gè)方法或鎖上可能是熱點(diǎn)方法或鎖競(jìng)爭(zhēng)。線程狀態(tài)大量BLOCKED線程指向同一個(gè)鎖對(duì)象是典型的鎖競(jìng)爭(zhēng)大量WAITINGonjava.util.concurrent.FutureTask可能是線程池任務(wù)堆積。追蹤慢請(qǐng)求結(jié)合分布式鏈路追蹤如SkyWalking, Zipkin定位到具體是哪個(gè)服務(wù)、哪個(gè)接口變慢并查看該接口的調(diào)用鏈定位到具體的數(shù)據(jù)庫(kù)查詢或外部服務(wù)調(diào)用。3.2 內(nèi)存泄漏排查實(shí)戰(zhàn)內(nèi)存泄漏是最常見(jiàn)的問(wèn)題之一表現(xiàn)為老年代使用率隨時(shí)間推移而穩(wěn)步上升最終觸發(fā)Full GC且Full GC后內(nèi)存回收率很低。獲取堆轉(zhuǎn)儲(chǔ)文件通過(guò)OOM自動(dòng)Dump或使用jmap -dump:live,formatb,fileheap.hprof手動(dòng)Dump線上謹(jǐn)慎使用會(huì)觸發(fā)Full GC。使用MATMemory Analyzer Tool分析Leak Suspects ReportMAT會(huì)自動(dòng)生成泄漏嫌疑報(bào)告這是一個(gè)很好的起點(diǎn)。Histogram查看對(duì)象數(shù)量的直方圖按Retained Heap排序找到占用內(nèi)存最大的對(duì)象類型。Dominator Tree支配樹(shù)視圖可以清晰地看到哪些對(duì)象持有大量?jī)?nèi)存以及它們的引用鏈。Path to GC Roots對(duì)疑似泄漏的對(duì)象查看其到GC Roots的引用路徑。這是定位泄漏源的關(guān)鍵。重點(diǎn)關(guān)注被靜態(tài)集合如static Map、線程局部變量ThreadLocal、第三方框架緩存如本地緩存長(zhǎng)期持有的對(duì)象。常見(jiàn)泄漏模式靜態(tài)集合類生長(zhǎng)例如一個(gè)static ConcurrentHashMap不斷被放入業(yè)務(wù)對(duì)象且沒(méi)有移除邏輯。未正確關(guān)閉的資源數(shù)據(jù)庫(kù)連接、文件流、網(wǎng)絡(luò)連接等。監(jiān)聽(tīng)器/回調(diào)未注銷向消息總線、事件系統(tǒng)注冊(cè)了監(jiān)聽(tīng)器但在對(duì)象銷毀時(shí)未注銷。ThreadLocal濫用使用完ThreadLocal后未調(diào)用remove()在線程池場(chǎng)景下線程復(fù)用會(huì)導(dǎo)致之前線程的數(shù)據(jù)殘留。踩坑記錄有一次遇到老年代緩慢增長(zhǎng)用MAT分析發(fā)現(xiàn)是大量char[]對(duì)象被持有。通過(guò)支配樹(shù)和引用鏈最終定位到一個(gè)JSON序列化框架在內(nèi)部使用了線程局部變量ThreadLocal來(lái)緩存char[]以提高性能但這個(gè)緩存池的大小沒(méi)有上限在高并發(fā)下不斷增長(zhǎng)。解決方案是為該緩存池設(shè)置一個(gè)合理的上限大小。4. GC調(diào)優(yōu)策略與垃圾回收器的“對(duì)話”GC調(diào)優(yōu)不是簡(jiǎn)單地調(diào)整幾個(gè)參數(shù)而是理解應(yīng)用的對(duì)象分配行為并為之選擇合適的垃圾回收器及參數(shù)。4.1 選擇適合的垃圾回收器JDK 8以后G1已成為默認(rèn)回收器但在特定場(chǎng)景下其他回收器可能更優(yōu)。Parallel Scavenge Parallel OldPSPOJDK 8默認(rèn)。追求高吞吐量適合后臺(tái)計(jì)算型應(yīng)用對(duì)延遲不敏感。調(diào)優(yōu)相對(duì)簡(jiǎn)單。Garbage FirstG1JDK 9默認(rèn)。目標(biāo)是可控的停頓時(shí)間MaxGCPauseMillis。適合堆內(nèi)存較大6GB、要求響應(yīng)延遲穩(wěn)定的應(yīng)用如Web服務(wù)。它采用分區(qū)Region思想能更精確地控制回收范圍。Z Garbage CollectorZGCJDK 15后生產(chǎn)可用。主打超低停頓亞毫秒級(jí)停頓時(shí)間不隨堆大小增長(zhǎng)而增長(zhǎng)。適用于超大堆內(nèi)存TB級(jí)別和對(duì)延遲極其敏感的應(yīng)用如金融交易。但吞吐量可能略低于G1。Shenandoah與ZGC目標(biāo)類似低停頓。由Red Hat主導(dǎo)在非Oracle的JDK發(fā)行版中可能更早可用。選型建議對(duì)于大多數(shù)Web應(yīng)用從G1開(kāi)始。如果堆內(nèi)存非常大超過(guò)32GB且對(duì)延遲有極致要求可以評(píng)估ZGC。對(duì)于傳統(tǒng)的批處理應(yīng)用PSPO可能仍然是個(gè)穩(wěn)妥的選擇。4.2 G1調(diào)優(yōu)核心參數(shù)實(shí)戰(zhàn)假設(shè)我們?yōu)橐粋€(gè)堆內(nèi)存為8G的Web服務(wù)進(jìn)行G1調(diào)優(yōu)。設(shè)定核心目標(biāo)-XX:MaxGCPauseMillis200。這是期望的最大停頓時(shí)間目標(biāo)G1會(huì)盡力達(dá)成但不是保證。設(shè)置一個(gè)不切實(shí)際的小值如50ms會(huì)導(dǎo)致GC頻繁發(fā)生反而降低吞吐量。設(shè)置堆大小-Xms8g -Xmx8g。生產(chǎn)環(huán)境務(wù)必設(shè)置初始堆和最大堆一致避免運(yùn)行期堆擴(kuò)容收縮帶來(lái)的性能波動(dòng)。設(shè)置Region大小-XX:G1HeapRegionSize。一般不用手動(dòng)設(shè)置G1會(huì)根據(jù)堆大小自動(dòng)計(jì)算1MB到32MB。如果堆特別大可以考慮設(shè)置為更大如16M以減少Region數(shù)量降低管理開(kāi)銷。關(guān)注并發(fā)階段-XX:ConcGCThreads并發(fā)標(biāo)記階段的線程數(shù)。默認(rèn)值大致是-XX:ParallelGCThreads的1/4。如果并發(fā)標(biāo)記階段耗時(shí)過(guò)長(zhǎng)可以適當(dāng)增加此值。-XX:InitiatingHeapOccupancyPercentIHOP觸發(fā)并發(fā)標(biāo)記周期的堆占用閾值默認(rèn)45%。如果老年代增長(zhǎng)很快可以適當(dāng)降低此值如40%讓G1更早開(kāi)始標(biāo)記避免在標(biāo)記完成前就不得不進(jìn)行Full GC。年輕代調(diào)優(yōu)G1的年輕代大小是自適應(yīng)的。但我們可以通過(guò)-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent來(lái)設(shè)定其最小和最大占比默認(rèn)分別是5%和60%。如果應(yīng)用產(chǎn)生大量短期對(duì)象可以適當(dāng)提高最大新生代比例。調(diào)優(yōu)是一個(gè)迭代過(guò)程每次調(diào)整參數(shù)后必須在預(yù)發(fā)環(huán)境進(jìn)行壓測(cè)對(duì)比GC日志和性能指標(biāo)吞吐量、延遲。使用jstat -gc觀察實(shí)時(shí)GC情況或通過(guò)JMX監(jiān)控。5. 內(nèi)存分配優(yōu)化減少GC壓力的“源頭治理”調(diào)優(yōu)GC是“治標(biāo)”優(yōu)化內(nèi)存分配才是“治本”。對(duì)象分配得越少、存活時(shí)間越短GC的壓力就越小。5.1 對(duì)象分配速率優(yōu)化使用jstat -gc觀察YGC頻率和GCT時(shí)間。如果Young GC非常頻繁如幾秒一次說(shuō)明對(duì)象分配速率過(guò)高。避免在循環(huán)中創(chuàng)建對(duì)象特別是創(chuàng)建大對(duì)象或大量小對(duì)象。例如日志拼接、字符串操作。// 壞味道 for (Item item : list) { String log Processing: item.getId() with name: item.getName(); // 每次循環(huán)都new StringBuilder和String logger.info(log); } // 優(yōu)化使用參數(shù)化日志或提前判斷日志級(jí)別 if (logger.isInfoEnabled()) { // ... 使用StringBuilder手動(dòng)拼接 }重用對(duì)象對(duì)于可變對(duì)象考慮通過(guò)對(duì)象池如Apache Commons Pool或線程局部變量ThreadLocal進(jìn)行重用。但要注意池化帶來(lái)的復(fù)雜性和內(nèi)存常駐開(kāi)銷權(quán)衡利弊。選擇合適的數(shù)據(jù)結(jié)構(gòu)ArrayListvsLinkedListHashMapvsTreeMap。ArrayList的隨機(jī)訪問(wèn)效率高但中間插入刪除慢HashMap默認(rèn)負(fù)載因子0.75如果知道大致容量構(gòu)造時(shí)指定初始大小new HashMap(expectedSize)避免多次擴(kuò)容重建。5.2 大對(duì)象與內(nèi)存布局優(yōu)化警惕大對(duì)象直接進(jìn)入老年代G1和Parallel收集器都有-XX:PretenureSizeThreshold參數(shù)默認(rèn)0即不生效可以設(shè)置對(duì)象超過(guò)多大時(shí)直接在老年代分配。但通常不建議修改因?yàn)榇髮?duì)象在新生代分配能更快被回收如果它很快變垃圾。問(wèn)題在于大對(duì)象會(huì)占用整個(gè)RegionG1或?qū)е滦律臻g不足引發(fā)提前GC。優(yōu)化數(shù)據(jù)結(jié)構(gòu)的內(nèi)存占用使用基本類型數(shù)組int[]代替包裝類列表List。對(duì)于數(shù)量巨大的小對(duì)象考慮使用sun.misc.Unsafe謹(jǐn)慎或第三方庫(kù)如Chronicle Map進(jìn)行堆外存儲(chǔ)或壓縮。檢查實(shí)體類避免不必要的字段使用byte、short代替int如果范圍允許。6. 線程與鎖優(yōu)化消除系統(tǒng)內(nèi)部的“交通堵塞”高并發(fā)下鎖競(jìng)爭(zhēng)和線程狀態(tài)異常是導(dǎo)致CPU資源浪費(fèi)、響應(yīng)延遲的常見(jiàn)原因。6.1 鎖競(jìng)爭(zhēng)分析與優(yōu)化識(shí)別熱點(diǎn)鎖通過(guò)jstack抓取線程Dump分析大量BLOCKED線程等待的鎖對(duì)象。也可以使用Arthas的monitor命令統(tǒng)計(jì)方法級(jí)別的競(jìng)爭(zhēng)情況。優(yōu)化策略縮小鎖粒度將一個(gè)大鎖拆分成多個(gè)小鎖。例如一個(gè)全局的CacheManager鎖可以拆分為按緩存Key分區(qū)的多個(gè)鎖。使用讀寫鎖對(duì)于讀多寫少的場(chǎng)景ReentrantReadWriteLock比synchronized能大幅提升并發(fā)讀性能。嘗試無(wú)鎖編程對(duì)于簡(jiǎn)單的計(jì)數(shù)器、狀態(tài)標(biāo)志優(yōu)先考慮java.util.concurrent.atomic包下的原子類。使用并發(fā)集合用ConcurrentHashMap代替synchronized Map。避免在持鎖時(shí)進(jìn)行耗時(shí)操作如IO操作、遠(yuǎn)程調(diào)用。6.2 線程池配置不當(dāng)問(wèn)題線程池配置是另一個(gè)性能黑洞。ThreadPoolExecutor的核心參數(shù)需要根據(jù)任務(wù)類型仔細(xì)設(shè)置。任務(wù)隊(duì)列堆積如果使用無(wú)界隊(duì)列如LinkedBlockingQueue當(dāng)任務(wù)生產(chǎn)速度持續(xù)超過(guò)消費(fèi)速度時(shí)隊(duì)列會(huì)無(wú)限增長(zhǎng)最終導(dǎo)致內(nèi)存溢出。務(wù)必使用有界隊(duì)列并配合合理的拒絕策略RejectedExecutionHandler如記錄日志、降級(jí)或臨時(shí)擴(kuò)容。核心/最大線程數(shù)設(shè)置CPU密集型任務(wù)線程數(shù) ≈ CPU核心數(shù) 1。設(shè)置過(guò)多會(huì)導(dǎo)致頻繁的線程上下文切換。IO密集型任務(wù)線程數(shù)可以更多因?yàn)榫€程大部分時(shí)間在等待。一個(gè)參考公式線程數(shù) CPU核心數(shù) * (1 平均等待時(shí)間 / 平均計(jì)算時(shí)間)。需要通過(guò)壓測(cè)找到最佳值。監(jiān)控線程池狀態(tài)通過(guò)ThreadPoolExecutor自身暴露的getQueue().size()、getActiveCount()等方法或通過(guò)Micrometer等監(jiān)控框架將隊(duì)列大小、活躍線程數(shù)等指標(biāo)暴露出來(lái)設(shè)置告警。7. 實(shí)戰(zhàn)案例匯編典型問(wèn)題與解決方案速查這里將一些常見(jiàn)的問(wèn)題現(xiàn)象、可能原因和排查動(dòng)作整理成表方便快速對(duì)照。問(wèn)題現(xiàn)象可能原因排查方向與工具潛在解決方案CPU使用率持續(xù)100%1. 無(wú)限循環(huán)/死循環(huán)2. 頻繁的Young GC (G1的并發(fā)標(biāo)記階段也會(huì)占CPU)3. 激烈的鎖競(jìng)爭(zhēng)大量線程自旋1.top -Hp找到占用CPU高的線程IDjstack轉(zhuǎn)換后查看堆棧。2.jstat -gcutil查看GC時(shí)間占比。3.jstack查看大量RUNNABLE且堆棧相同的線程。1. 修復(fù)代碼邏輯。2. 優(yōu)化對(duì)象分配降低GC頻率。3. 優(yōu)化鎖策略減少鎖粒度或使用無(wú)鎖結(jié)構(gòu)。服務(wù)響應(yīng)時(shí)間周期性毛刺1. 定時(shí)觸發(fā)的Full GC2. 后臺(tái)定時(shí)任務(wù)如日志滾動(dòng)、數(shù)據(jù)歸檔3. 外部依賴的周期性波動(dòng)1. 核對(duì)GC日志時(shí)間點(diǎn)與毛刺時(shí)間點(diǎn)。2. 檢查應(yīng)用日志和系統(tǒng)crontab。3. 查看鏈路追蹤和外部服務(wù)監(jiān)控。1. 優(yōu)化GC參數(shù)減少Full GC或降低其停頓時(shí)間。2. 將重型后臺(tái)任務(wù)移至獨(dú)立服務(wù)或低峰期執(zhí)行。3. 對(duì)外部依賴增加緩存、降級(jí)或熔斷。老年代使用率緩慢增長(zhǎng)直至Full GC內(nèi)存泄漏1. 監(jiān)控老年代趨勢(shì)圖。2. 在增長(zhǎng)期使用jmap -histo:live觀察對(duì)象類型變化謹(jǐn)慎會(huì)觸發(fā)Full GC。3. 使用jmap -dump獲取堆轉(zhuǎn)儲(chǔ)用MAT分析。1. 修復(fù)泄漏點(diǎn)如未關(guān)閉的資源、靜態(tài)集合無(wú)清理。2. 檢查第三方庫(kù)/框架的緩存配置。Metaspace (元空間) 持續(xù)增長(zhǎng)1. 大量動(dòng)態(tài)類生成如CGLib代理、Groovy腳本2. 應(yīng)用重啟未清理舊加載器1. 監(jiān)控Metaspace使用量。2. 使用jcmd VM.metaspace查看詳情。1. 限制動(dòng)態(tài)代理的使用或緩存代理類。2. 增加-XX:MaxMetaspaceSize限制并監(jiān)控其Full GC。Young GC頻率極高幾秒一次對(duì)象分配速率過(guò)快1.jstat -gc觀察YGC計(jì)數(shù)增長(zhǎng)速度和GCT時(shí)間。2. 使用JMC或Async Profiler采樣查看分配熱點(diǎn)方法。1. 優(yōu)化代碼減少不必要的對(duì)象創(chuàng)建如日志、字符串拼接。2. 考慮適當(dāng)調(diào)大新生代大小G1中調(diào)整-XX:G1MaxNewSizePercent。大量線程處于BLOCKED狀態(tài)鎖競(jìng)爭(zhēng)激烈1.jstack抓取線程Dump分析BLOCKED線程的鎖持有者和等待者。2. 使用Arthas的thread -b找出死鎖。1. 拆分鎖粒度。2. 使用讀寫鎖、并發(fā)集合。3. 檢查業(yè)務(wù)邏輯避免長(zhǎng)時(shí)間持鎖。8. 建立長(zhǎng)效性能保障機(jī)制一次調(diào)優(yōu)的結(jié)束正是常態(tài)化性能管理的開(kāi)始。要避免問(wèn)題反復(fù)需要建立機(jī)制。性能基準(zhǔn)測(cè)試Baseline在每次重大發(fā)布前對(duì)核心接口進(jìn)行基準(zhǔn)壓測(cè)建立性能基線吞吐量、延遲、資源使用率。后續(xù)版本與之對(duì)比防止代碼變更引入性能衰退。持續(xù)性能監(jiān)控與告警將第2部分提到的黃金指標(biāo)納入統(tǒng)一的監(jiān)控平臺(tái)如Prometheus Grafana并設(shè)置智能告警規(guī)則如Full GC頻率超過(guò)閾值、P99延遲突增。容量規(guī)劃與預(yù)案根據(jù)業(yè)務(wù)增長(zhǎng)預(yù)測(cè)定期進(jìn)行容量評(píng)估。明確當(dāng)流量增長(zhǎng)X倍時(shí)需要增加多少資源CPU、內(nèi)存、實(shí)例數(shù)。并制定降級(jí)、擴(kuò)容預(yù)案。代碼層面的性能意識(shí)在Code Review中加入性能視角警惕常見(jiàn)反模式如大對(duì)象創(chuàng)建、循環(huán)內(nèi)數(shù)據(jù)庫(kù)查詢、不當(dāng)?shù)木€程池使用等。JVM調(diào)優(yōu)不是一個(gè)一勞永逸的開(kāi)關(guān)而是一個(gè)結(jié)合監(jiān)控、分析、實(shí)驗(yàn)和迭代的持續(xù)過(guò)程。它要求我們既要有扎實(shí)的JVM原理功底也要有豐富的實(shí)戰(zhàn)排查經(jīng)驗(yàn)更要有將經(jīng)驗(yàn)沉淀為工具和流程的系統(tǒng)化思維。從被動(dòng)的“救火隊(duì)員”轉(zhuǎn)變?yōu)橹鲃?dòng)的“系統(tǒng)醫(yī)生”這條路很長(zhǎng)但每解決一個(gè)棘手問(wèn)題對(duì)系統(tǒng)的理解就更深一層這份經(jīng)驗(yàn)才是工程師最寶貴的財(cái)富。最后分享一個(gè)習(xí)慣任何重要的JVM參數(shù)變更一定要在預(yù)發(fā)環(huán)境進(jìn)行至少24小時(shí)的穩(wěn)定性觀察和壓測(cè)并且做好一鍵回滾的準(zhǔn)備這是線上調(diào)優(yōu)的底線。