與電子商務(wù)網(wǎng)站建設(shè)有關(guān)實訓(xùn)報告濰坊建設(shè)銀行招聘網(wǎng)站
鶴壁市浩天電氣有限公司
2026/01/24 17:41:53
與電子商務(wù)網(wǎng)站建設(shè)有關(guān)實訓(xùn)報告,濰坊建設(shè)銀行招聘網(wǎng)站,如何利用dw建設(shè)網(wǎng)站,佛山網(wǎng)站優(yōu)化質(zhì)量好JVM性能調(diào)優(yōu)與監(jiān)控實戰(zhàn)完整指南
一、JVM內(nèi)存模型深度解析
1.1 JVM內(nèi)存結(jié)構(gòu)概述
Java虛擬機#xff08;JVM#xff09;作為Java程序的運行環(huán)境#xff0c;承擔(dān)著內(nèi)存管理、垃圾回收、字節(jié)碼執(zhí)行等核心職責(zé)。在JVM的眾多職責(zé)中#xff0c;內(nèi)存管理無疑是最重要的一環(huán)。合理的…JVM性能調(diào)優(yōu)與監(jiān)控實戰(zhàn)完整指南一、JVM內(nèi)存模型深度解析1.1 JVM內(nèi)存結(jié)構(gòu)概述Java虛擬機JVM作為Java程序的運行環(huán)境承擔(dān)著內(nèi)存管理、垃圾回收、字節(jié)碼執(zhí)行等核心職責(zé)。在JVM的眾多職責(zé)中內(nèi)存管理無疑是最重要的一環(huán)。合理的內(nèi)存劃分和管理直接影響到應(yīng)用程序的性能表現(xiàn)。當(dāng)我們談?wù)揓VM性能調(diào)優(yōu)時首先需要深入理解JVM是如何組織和管理內(nèi)存的。JVM在執(zhí)行Java程序時會將系統(tǒng)分配給它的內(nèi)存劃分為多個不同的數(shù)據(jù)區(qū)域每個區(qū)域有著明確的用途、創(chuàng)建時間和銷毀時間。這種設(shè)計不是隨意為之而是經(jīng)過精心考慮的結(jié)果目的是為了更高效地管理內(nèi)存、更快速地分配對象、更安全地執(zhí)行垃圾回收。理解這些內(nèi)存區(qū)域的劃分方式、各自的職責(zé)、以及它們之間的協(xié)作關(guān)系是進行JVM性能調(diào)優(yōu)的基礎(chǔ)。只有深入了解了內(nèi)存的組織結(jié)構(gòu)我們才能在遇到內(nèi)存問題時快速定位原因才能在進行參數(shù)調(diào)優(yōu)時做到有的放矢。從宏觀角度來看JVM內(nèi)存可以分為兩大類線程共享區(qū)域和線程私有區(qū)域。線程共享區(qū)域包括堆內(nèi)存和方法區(qū)這些區(qū)域在JVM啟動時創(chuàng)建所有線程都可以訪問這些區(qū)域的數(shù)據(jù)。線程私有區(qū)域包括程序計數(shù)器、虛擬機棧和本地方法棧這些區(qū)域的生命周期與線程相同隨線程的創(chuàng)建而創(chuàng)建隨線程的結(jié)束而銷毀。根據(jù)Java虛擬機規(guī)范JVM內(nèi)存主要分為以下幾個區(qū)域JVM內(nèi)存結(jié)構(gòu) ├── 堆內(nèi)存Heap- 線程共享 │ ├── 年輕代Young Generation │ │ ├── Eden區(qū) │ │ ├── Survivor0區(qū)From │ │ └── Survivor1區(qū)To │ └── 老年代Old Generation ├── 方法區(qū)Method Area- 線程共享 │ ├── 運行時常量池 │ ├── 類型信息 │ └── 字段和方法信息 ├── 虛擬機棧VM Stack- 線程私有 ├── 本地方法棧Native Method Stack- 線程私有 ├── 程序計數(shù)器Program Counter Register- 線程私有 └── 直接內(nèi)存Direct Memory- 不屬于JVM規(guī)范1.2 堆內(nèi)存詳解堆內(nèi)存是JVM內(nèi)存管理中最核心、也是最復(fù)雜的一塊區(qū)域。從大小上來說堆通常是JVM管理的最大一塊內(nèi)存空間在現(xiàn)代應(yīng)用中堆內(nèi)存的大小通常從幾百兆到幾十GB不等。從重要性上來說堆是垃圾回收器工作的主戰(zhàn)場幾乎所有的對象實例以及數(shù)組都在堆上分配內(nèi)存。可以說堆內(nèi)存的設(shè)計和管理水平直接決定了JVM的性能表現(xiàn)。為什么堆內(nèi)存如此重要這要從Java的內(nèi)存分配機制說起。在Java中我們通過new關(guān)鍵字創(chuàng)建對象時這些對象的內(nèi)存主要分配在堆上。與棧內(nèi)存不同堆內(nèi)存不會隨著方法的結(jié)束而自動回收這些對象會一直存在于內(nèi)存中直到垃圾回收器判斷它們不再被使用時才會被回收。這種特性使得堆內(nèi)存的管理變得復(fù)雜需要專門的垃圾回收機制來處理。堆內(nèi)存的一個核心設(shè)計理念是分代管理。這個設(shè)計源于一個被大量實際應(yīng)用驗證的經(jīng)驗規(guī)律絕大多數(shù)對象的生命周期都很短它們被創(chuàng)建后很快就會變得不可達可以被回收只有很少一部分對象會長期存活。這個規(guī)律被稱為弱分代假說Weak Generational Hypothesis?;谶@個假說JVM將堆內(nèi)存劃分為不同的代對不同年齡的對象采用不同的回收策略從而大大提高了垃圾回收的效率。1.2.1 年輕代Young Generation年輕代是所有新創(chuàng)建對象的出生地。當(dāng)我們在代碼中創(chuàng)建一個對象時這個對象通常會首先被分配到年輕代的Eden區(qū)。年輕代的設(shè)計體現(xiàn)了朝生夕死的對象特點——大部分對象在這里被創(chuàng)建也在這里被回收。從容量規(guī)劃角度來看年輕代的大小通常占整個堆內(nèi)存的1/3左右。這個比例不是固定的而是可以根據(jù)應(yīng)用的特點進行調(diào)整。如果你的應(yīng)用創(chuàng)建了大量生命周期很短的對象比如Web應(yīng)用中的Request、Response對象那么可以適當(dāng)增大年輕代的比例反之如果應(yīng)用中對象的生命周期普遍較長則可以減小年輕代的比例。年輕代內(nèi)部又進一步細分為三個區(qū)域Eden區(qū)和兩個Survivor區(qū)。這種設(shè)計看似復(fù)雜實際上是為了實現(xiàn)高效的垃圾回收算法。讓我們詳細了解這三個區(qū)域Eden區(qū)伊甸園區(qū)Eden區(qū)是年輕代中最大的一塊區(qū)域默認占據(jù)年輕代的80%空間。之所以叫Eden伊甸園寓意是所有對象的出生地。幾乎所有新創(chuàng)建的對象都會首先被分配到這里這是對象生命周期的起點。Eden區(qū)采用的是一種非常高效的內(nèi)存分配策略叫做指針碰撞Bump the Pointer。簡單來說JVM維護一個指針指向Eden區(qū)已使用內(nèi)存和未使用內(nèi)存的分界點。當(dāng)需要分配新對象時只需要檢查剩余空間是否足夠如果足夠就將指針向前移動相應(yīng)的大小即可。這種分配方式非??焖賻缀跖c在棧上分配內(nèi)存的速度相當(dāng)。當(dāng)Eden區(qū)的空間被用完時就會觸發(fā)一次Minor GC也叫Young GC。這時候JVM會暫停應(yīng)用程序的運行Stop The World檢查Eden區(qū)中的所有對象找出那些仍然被引用的存活對象將它們復(fù)制到Survivor區(qū)然后清空整個Eden區(qū)。整個過程通常非??煲驗镋den區(qū)中的大部分對象都已經(jīng)死亡需要復(fù)制的對象很少。Survivor區(qū)幸存者區(qū)Survivor區(qū)的設(shè)計是年輕代回收機制中最巧妙的部分。它由兩個大小完全相等的區(qū)域組成通常稱為S0和S1或者From區(qū)和To區(qū)。每個Survivor區(qū)默認占年輕代的10%空間。為什么需要兩個Survivor區(qū)這個設(shè)計源于一個重要的考慮如何避免內(nèi)存碎片。如果只有一個Survivor區(qū)那么在多次GC后這個區(qū)域會充滿各種大小不一的對象它們之間會產(chǎn)生很多不連續(xù)的空閑空間。這些碎片化的空間很難被有效利用可能導(dǎo)致明明有足夠的總空閑空間卻無法分配一個稍大的對象。兩個Survivor區(qū)的工作機制是這樣的在任何時刻兩個Survivor區(qū)中只有一個是活躍的From區(qū)另一個保持完全空閑To區(qū)。當(dāng)發(fā)生Minor GC時Eden區(qū)和From區(qū)中的存活對象會被一起復(fù)制到To區(qū)。復(fù)制完成后Eden區(qū)和From區(qū)被完全清空然后From區(qū)和To區(qū)的角色互換——原來的To區(qū)變成新的From區(qū)原來的From區(qū)變成新的To區(qū)。這種乒乓式的切換機制確保了使用中的Survivor區(qū)始終是緊湊的、沒有碎片的。這種復(fù)制算法有個額外的好處它天然地實現(xiàn)了內(nèi)存整理。每次GC后所有存活對象都被整齊地排列在To區(qū)的前端沒有任何碎片。這使得后續(xù)的對象分配仍然可以使用快速的指針碰撞方式。對象在年輕代的生命周期一個對象的成長之路理解對象在年輕代的完整生命周期對于理解JVM的內(nèi)存管理至關(guān)重要。讓我們跟隨一個對象從創(chuàng)建到晉升的整個過程首先當(dāng)應(yīng)用程序創(chuàng)建一個新對象時這個對象會被分配到Eden區(qū)。此時這個對象的年齡Age被標(biāo)記為0。對象的年齡是JVM用來跟蹤對象經(jīng)歷了多少次GC的一個計數(shù)器。隨著程序的運行越來越多的對象被創(chuàng)建Eden區(qū)逐漸被填滿。當(dāng)Eden區(qū)無法再分配新對象時JVM觸發(fā)第一次Minor GC。垃圾回收器會掃描Eden區(qū)的所有對象識別哪些對象仍然被程序引用存活對象哪些對象已經(jīng)沒有被引用垃圾對象。存活對象會被復(fù)制到Survivor0區(qū)同時它們的年齡增加到1。垃圾對象則被清除Eden區(qū)恢復(fù)為空。程序繼續(xù)運行新的對象繼續(xù)在Eden區(qū)分配。當(dāng)Eden區(qū)再次填滿時觸發(fā)第二次Minor GC。這次不僅要掃描Eden區(qū)還要掃描Survivor0區(qū)。Eden區(qū)和Survivor0區(qū)中的存活對象會一起被復(fù)制到Survivor1區(qū)它們的年齡再次加1對于Eden區(qū)的新對象年齡變?yōu)?對于Survivor0區(qū)的對象年齡變?yōu)?。然后Eden區(qū)和Survivor0區(qū)被清空。這個過程會反復(fù)進行。每次Minor GC時Eden區(qū)和使用中的Survivor區(qū)From區(qū)的存活對象都會被復(fù)制到空閑的Survivor區(qū)To區(qū)對象年齡加1然后兩個Survivor區(qū)角色互換。那么對象什么時候會離開年輕代進入老年代呢默認情況下當(dāng)對象的年齡達到15時就會被晉升Promotion到老年代。為什么是15因為對象頭中用于存儲年齡的位段只有4位最大只能表示15。當(dāng)然這個閾值是可以通過參數(shù)調(diào)整的。值得注意的是對象并不一定要等到年齡達到15才能晉升。如果Survivor區(qū)空間不足裝不下所有存活對象那么一些對象會提前晉升到老年代即使它們的年齡還很小。這被稱為過早晉升Premature Promotion是一種不理想的情況因為這些對象可能很快就會死亡但卻進入了老年代增加了老年代GC的負擔(dān)。1.2.2 老年代Old Generation老年代是堆內(nèi)存中用于存放長者的區(qū)域這里的對象都是經(jīng)過多次GC考驗、依然存活的老對象。從空間分配來看老年代通常占據(jù)堆內(nèi)存的2/3左右這個比例反映了一個事實雖然大部分對象都很短命但那些長壽對象所占用的總內(nèi)存量卻不小。老年代的管理策略與年輕代有著本質(zhì)的不同。在年輕代對象密度較低大部分是垃圾適合用復(fù)制算法快速清理。但在老年代對象密度很高大部分都存活如果還用復(fù)制算法就需要復(fù)制大量對象效率很低。因此老年代通常采用標(biāo)記-清除或標(biāo)記-整理算法這些算法不需要大量復(fù)制對象但執(zhí)行時間較長。老年代的GC事件通常稱為Major GC或Full GC嚴格來說兩者有細微差別但在實際中常被混用其特點是頻率低、耗時長、影響大。一次Full GC可能需要幾秒甚至更長時間在此期間應(yīng)用程序會完全停頓。因此性能調(diào)優(yōu)的一個重要目標(biāo)就是減少Full GC的頻率。對象進入老年代的條件不只是年齡很多人以為對象進入老年代只有一個條件年齡達到閾值。實際上JVM設(shè)計了多種機制來決定對象何時晉升這些機制共同作用確保內(nèi)存的高效利用。讓我們逐一分析1. 年齡達到晉升閾值A(chǔ)ge Threshold這是最常見的晉升方式。對象每經(jīng)歷一次Minor GC年齡就加1。當(dāng)年齡達到設(shè)定的閾值默認15可通過-XX:MaxTenuringThreshold調(diào)整對象就會晉升到老年代。這個機制的邏輯很簡單一個對象如果經(jīng)歷了這么多次GC還沒死那它很可能是個長壽對象應(yīng)該放到老年代去。2. 大對象直接分配Large Object Direct Allocation某些特別大的對象通常是大數(shù)組會直接繞過年輕代在創(chuàng)建時就被分配到老年代。這個設(shè)計的考慮是大對象在年輕代會占用大量空間而年輕代的復(fù)制算法需要復(fù)制存活對象復(fù)制大對象的成本很高。與其在年輕代折騰不如直接放到老年代。這個特別大的閾值可以通過-XX:PretenureSizeThreshold參數(shù)設(shè)置。不過需要注意這個參數(shù)只對Serial和ParNew收集器有效對Parallel Scavenge無效。在實際應(yīng)用中應(yīng)該盡量避免創(chuàng)建大對象如果必須創(chuàng)建也要考慮對象池等復(fù)用機制。3. 動態(tài)年齡判定Dynamic Age Determination這是一個很聰明的機制。JVM并不會死板地等待對象年齡達到15才晉升。如果在Survivor區(qū)中相同年齡的所有對象大小的總和大于Survivor空間的一半那么年齡大于或等于該年齡的對象就可以直接晉升到老年代無需等到MaxTenuringThreshold設(shè)定的年齡。為什么要這樣設(shè)計假設(shè)有一批對象都是在同一時刻創(chuàng)建的比如處理一批請求它們會一起在Survivor區(qū)中停留。如果這批對象的數(shù)量很大占據(jù)了Survivor區(qū)的大半空間那么繼續(xù)讓它們留在Survivor區(qū)就沒有意義了還不如早點讓它們晉升騰出Survivor區(qū)的空間給更年輕的對象。4. 空間分配擔(dān)保Allocation Guarantee這是一種應(yīng)急晉升機制。當(dāng)發(fā)生Minor GC時如果Survivor區(qū)空間不足以容納所有存活對象那些放不下的對象就會直接晉升到老年代不管它們的年齡是多少。這種情況通常說明Survivor區(qū)設(shè)置得太小了是一個需要關(guān)注的調(diào)優(yōu)點。老年代GC的特點慢而重老年代的垃圾回收與年輕代有著本質(zhì)的不同主要體現(xiàn)在以下幾個方面首先是觸發(fā)時機。老年代GC通常在老年代空間不足時觸發(fā)這可能是因為對象晉升導(dǎo)致的也可能是直接在老年代分配大對象導(dǎo)致的。還有一種情況是在Minor GC之前JVM會做一個檢查如果預(yù)測這次Minor GC后需要晉升的對象大小大于老年代剩余空間就會先觸發(fā)一次Full GC。其次是回收算法。老年代不能使用年輕代的復(fù)制算法因為老年代中大部分對象都是存活的復(fù)制的成本太高。老年代通常使用標(biāo)記-清除或標(biāo)記-整理算法這些算法需要標(biāo)記所有存活對象然后清除或整理內(nèi)存過程比年輕代的GC復(fù)雜得多。最后是性能影響。一次Full GC可能需要幾百毫秒到幾秒的時間在此期間應(yīng)用程序完全停頓Stop The World。對于在線服務(wù)來說幾秒的停頓意味著成百上千個請求超時這是無法接受的。因此性能調(diào)優(yōu)的一個核心目標(biāo)就是減少Full GC的頻率或者選用能夠并發(fā)執(zhí)行、停頓時間短的垃圾回收器。1.3 方法區(qū)元空間類信息的存儲庫方法區(qū)是JVM規(guī)范中定義的一個邏輯概念用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等數(shù)據(jù)。雖然規(guī)范稱其為方法區(qū)但它存儲的內(nèi)容遠不止方法更準確地說它是類的元數(shù)據(jù)的存儲區(qū)域。方法區(qū)的重要性常常被忽視但它在JVM中扮演著至關(guān)重要的角色。當(dāng)我們編寫一個Java類時包含了類的結(jié)構(gòu)信息有哪些字段、哪些方法、類的繼承關(guān)系、實現(xiàn)了哪些接口等等。這些信息在類加載時會被解析并存儲到方法區(qū)中??梢哉f方法區(qū)存儲的是Java程序的骨架而堆中存儲的則是這個骨架的血肉對象實例。從永久代到元空間一次重要的演進方法區(qū)的實現(xiàn)在JVM的演進過程中經(jīng)歷了一次重大變革理解這次變革有助于我們更好地理解和調(diào)優(yōu)JVM。在JDK 7及以前的版本中HotSpot虛擬機使用永久代Permanent Generation簡稱PermGen來實現(xiàn)方法區(qū)。永久代使用的是JVM堆內(nèi)存這意味著永久代的大小受到堆內(nèi)存的限制。這種實現(xiàn)帶來了一些問題首先永久代的大小很難估算。不同的應(yīng)用加載的類的數(shù)量差異很大框架多、使用反射多、動態(tài)代理多的應(yīng)用可能需要很大的永久代空間。如果永久代設(shè)置得太小容易發(fā)生java.lang.OutOfMemoryError: PermGen space錯誤如果設(shè)置得太大又會擠占堆內(nèi)存空間。其次永久代的垃圾回收效率低。類的卸載條件非常苛刻需要滿足類的所有實例都被回收、類加載器被回收、Class對象沒有被引用等條件。在實際應(yīng)用中類的卸載很少發(fā)生這意味著永久代的空間基本上是只增不減的。為了解決這些問題從JDK 8開始HotSpot虛擬機完全移除了永久代改用元空間Metaspace來實現(xiàn)方法區(qū)。這是一次革命性的變化主要體現(xiàn)在元空間使用的是本地內(nèi)存Native Memory而不是JVM堆內(nèi)存。這意味著元空間的大小不再受到-Xmx參數(shù)的限制而是受到機器物理內(nèi)存的限制。默認情況下元空間可以動態(tài)擴展理論上可以使用所有可用的系統(tǒng)內(nèi)存當(dāng)然這通常不是好事所以還是應(yīng)該設(shè)置上限。這個變化帶來了幾個好處首先不再需要精確估算方法區(qū)的大小減少了OOM的風(fēng)險。其次堆內(nèi)存的規(guī)劃變得更簡單不需要在堆內(nèi)存和永久代之間權(quán)衡。最后元空間的垃圾回收更加高效因為它與堆的GC獨立進行。但這個變化也帶來了新的挑戰(zhàn)如果不設(shè)置元空間的上限類加載過多或者發(fā)生類加載器泄漏時可能會耗盡系統(tǒng)內(nèi)存影響整個機器的穩(wěn)定性。因此在生產(chǎn)環(huán)境中通常建議顯式設(shè)置-XX:MaxMetaspaceSize參數(shù)。方法區(qū)存儲內(nèi)容方法區(qū) ├── 類型信息類名、父類、接口、修飾符等 ├── 方法信息方法名、返回類型、參數(shù)、字節(jié)碼等 ├── 字段信息字段名、類型、修飾符 ├── 運行時常量池 │ ├── 字面量字符串常量、final常量等 │ └── 符號引用類、方法、字段的符號引用 └── 靜態(tài)變量1.4 虛擬機棧方法執(zhí)行的舞臺虛擬機棧是線程私有的內(nèi)存區(qū)域它的生命周期與線程相同。當(dāng)創(chuàng)建一個新線程時JVM會為這個線程分配一個虛擬機棧當(dāng)線程結(jié)束時它的虛擬機棧也隨之銷毀。虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型每個方法在執(zhí)行時都會創(chuàng)建一個棧幀Stack Frame用于存儲該方法運行時需要的各種信息。理解虛擬機棧對于理解Java程序的執(zhí)行機制非常重要。當(dāng)我們調(diào)用一個方法時實際上是將一個新的棧幀壓入棧頂當(dāng)方法執(zhí)行完畢無論是正常返回還是拋出異常對應(yīng)的棧幀就會從棧頂彈出。這種后進先出LIFO的結(jié)構(gòu)天然地支持了方法調(diào)用的嵌套關(guān)系。棧幀中存儲了什么呢主要包括局部變量表、操作數(shù)棧、動態(tài)鏈接和方法返回地址等信息。局部變量表存儲了方法的參數(shù)和方法內(nèi)定義的局部變量操作數(shù)棧用于執(zhí)行字節(jié)碼指令時的操作數(shù)臨時存儲動態(tài)鏈接用于將符號引用轉(zhuǎn)換為直接引用方法返回地址則記錄了方法執(zhí)行完成后要返回到哪里繼續(xù)執(zhí)行。虛擬機棧的大小是有限的如果線程請求的棧深度大于虛擬機所允許的深度就會拋出StackOverflowError。這最常見于遞歸調(diào)用沒有正確設(shè)置終止條件的情況。虛擬機棧也可以動態(tài)擴展但如果擴展時無法申請到足夠的內(nèi)存就會拋出OutOfMemoryError。在性能調(diào)優(yōu)時虛擬機棧的大小通常不是關(guān)注的重點除非應(yīng)用程序有以下特點使用了大量的遞歸、方法調(diào)用層次很深、或者創(chuàng)建了大量的線程。這時候就需要通過-Xss參數(shù)來調(diào)整每個線程的棧大小。棧幀結(jié)構(gòu)棧幀Stack Frame ├── 局部變量表 │ └── 存儲方法參數(shù)和局部變量 ├── 操作數(shù)棧 │ └── 用于存放方法執(zhí)行過程中產(chǎn)生的中間結(jié)果 ├── 動態(tài)鏈接 │ └── 指向運行時常量池中該棧幀所屬方法的引用 ├── 方法返回地址 │ └── 方法正常退出或異常退出的定義 └── 附加信息相關(guān)異常StackOverflowError線程請求的棧深度大于虛擬機所允許的深度如遞歸調(diào)用過深OutOfMemoryError虛擬機棧動態(tài)擴展時無法申請到足夠的內(nèi)存棧內(nèi)存大小設(shè)置-Xss256k# 設(shè)置每個線程的棧大小為256KB1.5 直接內(nèi)存直接內(nèi)存不是JVM運行時數(shù)據(jù)區(qū)的一部分但在NIO操作中被頻繁使用。特點通過DirectByteBuffer對象分配和管理不受JVM堆內(nèi)存限制但受物理內(nèi)存限制避免了Java堆和Native堆之間的數(shù)據(jù)復(fù)制提高性能不會被垃圾回收直接管理但通過Reference機制回收參數(shù)設(shè)置-XX:MaxDirectMemorySize512M# 設(shè)置直接內(nèi)存最大值1.6 對象的內(nèi)存分配流程理解對象的內(nèi)存分配流程對于性能調(diào)優(yōu)至關(guān)重要對象創(chuàng)建 ↓ 是否為大對象 ├─ 是 → 直接分配到老年代 └─ 否 → 嘗試在Eden區(qū)分配 ↓ Eden區(qū)是否有足夠空間 ├─ 是 → 分配成功 └─ 否 → 觸發(fā)Minor GC ↓ 清理Eden區(qū)和Survivor From區(qū) ↓ 存活對象移到Survivor To區(qū) ↓ 對象年齡1 ↓ 年齡是否達到閾值 ├─ 是 → 晉升到老年代 └─ 否 → 留在Survivor區(qū) ↓ Survivor區(qū)是否放得下 ├─ 是 → 分配成功 └─ 否 → 直接進入老年代 ↓ 老年代是否有空間 ├─ 是 → 分配成功 └─ 否 → 觸發(fā)Full GC ↓ GC后是否有空間 ├─ 是 → 分配成功 └─ 否 → OutOfMemoryError二、垃圾回收器原理與選擇2.1 垃圾回收算法基礎(chǔ)垃圾回收Garbage CollectionGC是JVM自動內(nèi)存管理的核心機制。在Java中程序員不需要手動釋放內(nèi)存不像C/C需要free或delete這項工作由垃圾回收器自動完成。但自動不意味著隨意垃圾回收器遵循著一套精心設(shè)計的算法這些算法決定了如何識別垃圾、何時回收垃圾、如何回收垃圾。在深入了解各種垃圾回收器之前我們需要先理解垃圾回收的基礎(chǔ)算法。這些算法是所有垃圾回收器的理論基礎(chǔ)不同的回收器本質(zhì)上是這些基礎(chǔ)算法的不同組合和優(yōu)化。掌握了這些基礎(chǔ)算法就能理解為什么不同的回收器適用于不同的場景也能在遇到GC問題時更好地分析和解決。2.1.1 標(biāo)記-清除算法Mark-Sweep最基礎(chǔ)的回收算法標(biāo)記-清除算法是最基礎(chǔ)、最早出現(xiàn)的垃圾回收算法由John McCarthy在1960年發(fā)明用于Lisp語言。雖然它有明顯的缺點但這個算法的思想影響深遠后續(xù)的很多算法都是在它的基礎(chǔ)上改進而來。這個算法的名字已經(jīng)很好地描述了它的工作過程分為標(biāo)記和清除兩個階段。標(biāo)記階段的深入理解標(biāo)記階段的核心任務(wù)是找出所有仍然存活的對象。但是JVM如何判斷一個對象是否還存活呢采用的是可達性分析算法。這個算法的基本思路是從一系列稱為GC Roots的對象開始向下搜索形成一個引用鏈。如果一個對象到GC Roots沒有任何引用鏈相連用圖論的話說就是從GC Roots到這個對象不可達那么這個對象就是垃圾可以被回收。那么哪些對象可以作為GC Roots呢主要包括虛擬機棧中引用的對象方法的局部變量、方法區(qū)中類靜態(tài)屬性引用的對象、方法區(qū)中常量引用的對象、本地方法棧中JNI引用的對象、以及JVM內(nèi)部的引用等。這些對象被認為是根對象從它們出發(fā)可以追蹤到所有仍在使用的對象。標(biāo)記過程需要遍歷整個對象圖這是一個相對耗時的過程。而且為了保證標(biāo)記的準確性在標(biāo)記期間必須暫停所有應(yīng)用線程Stop The World否則對象的引用關(guān)系可能會在標(biāo)記過程中發(fā)生變化導(dǎo)致誤判。清除階段的工作方式標(biāo)記完成后所有對象就被分為了兩類被標(biāo)記的存活對象和未被標(biāo)記的垃圾對象。清除階段的任務(wù)就是回收未被標(biāo)記對象占用的內(nèi)存。需要注意的是“清除并不是真的將內(nèi)存清零而是將可回收對象所占用的內(nèi)存加入到空閑列表”Free List中。當(dāng)后續(xù)需要分配內(nèi)存時就從空閑列表中尋找合適大小的空閑塊。算法的優(yōu)缺點分析標(biāo)記-清除算法的優(yōu)點是概念簡單、實現(xiàn)相對容易。它不需要移動對象這在某些場景下是有利的比如對象移動會導(dǎo)致引用關(guān)系更新的開銷。但它的缺點也很明顯主要有兩個第一個缺點是效率問題。標(biāo)記和清除兩個過程的效率都不高特別是當(dāng)堆中對象很多時需要標(biāo)記和清除的對象數(shù)量巨大。而且這兩個階段都需要遍歷整個內(nèi)存空間。第二個缺點也是更致命的是會產(chǎn)生大量的內(nèi)存碎片。清除后內(nèi)存空間中會出現(xiàn)大量不連續(xù)的小塊空閑空間。這些碎片化的空間很難被利用可能會出現(xiàn)明明總的空閑內(nèi)存足夠卻無法分配一個稍大的對象的情況。內(nèi)存碎片會導(dǎo)致不得不提前進行垃圾回收甚至可能觸發(fā)Full GC嚴重影響性能。正因為這些缺點標(biāo)記-清除算法通常不會單獨使用而是與其他算法組合使用。比如CMS收集器就是基于標(biāo)記-清除算法的但它通過并發(fā)標(biāo)記等技術(shù)來減少停頓時間并定期進行內(nèi)存整理來解決碎片問題。示意圖標(biāo)記前 [對象A][對象B][對象C][對象D][對象E] ↓ ↓ ↓ ↓ ↓ 存活 垃圾 存活 垃圾 存活 標(biāo)記后 [對象A][ ][對象C][ ][對象E] ? ? ? 清除后產(chǎn)生碎片可能無法分配大對象2.1.2 標(biāo)記-復(fù)制算法Mark-Copy為年輕代而生的算法標(biāo)記-復(fù)制算法也常被簡稱為復(fù)制算法是1969年由Fenichel提出的它巧妙地解決了標(biāo)記-清除算法的內(nèi)存碎片問題。這個算法的核心思想是將內(nèi)存分為兩塊每次只使用其中一塊GC時將存活對象復(fù)制到另一塊區(qū)域然后清空當(dāng)前區(qū)域。算法的工作機制讓我們詳細看看這個算法是如何工作的。假設(shè)我們將內(nèi)存分為A區(qū)和B區(qū)兩塊大小相等的區(qū)域。開始時所有對象都分配在A區(qū)B區(qū)保持空閑。當(dāng)A區(qū)滿了之后觸發(fā)垃圾回收首先進行標(biāo)記找出A區(qū)中所有存活的對象。然后不是就地清除垃圾而是將這些存活對象按順序復(fù)制到B區(qū)。復(fù)制完成后A區(qū)中的所有內(nèi)存包括存活對象和垃圾對象都可以一次性清空。下一次分配時就在B區(qū)分配。當(dāng)B區(qū)滿了再次GC時就將B區(qū)的存活對象復(fù)制回A區(qū)如此循環(huán)往復(fù)。這個算法有個非常聰明的地方復(fù)制時存活對象是按順序緊密排列的自然就沒有碎片。而且清空內(nèi)存區(qū)域時不需要逐個回收對象而是整塊清空效率極高。內(nèi)存分配的簡化復(fù)制算法還帶來了一個額外的好處簡化了內(nèi)存分配策略。在使用標(biāo)記-清除算法時內(nèi)存中到處都是碎片分配內(nèi)存需要在空閑列表中搜索合適大小的空閑塊這個過程比較復(fù)雜。而在復(fù)制算法中由于所有對象都是緊密排列的內(nèi)存分配就變得非常簡單只需要使用一個指針記錄已使用內(nèi)存的邊界分配新對象時只需要將指針向前移動相應(yīng)的大小即可。這種方式被稱為指針碰撞Bump the Pointer速度非??鞄缀跖c在棧上分配內(nèi)存一樣快。算法的適用場景復(fù)制算法在理論上很完美但它有一個顯而易見的缺點內(nèi)存利用率只有50%。一半的內(nèi)存永遠處于空閑狀態(tài)這在內(nèi)存資源寶貴的情況下是無法接受的。但是如果我們換個角度思考在對象大量死亡、只有少量存活的場景下復(fù)制算法就變得非常高效了。需要復(fù)制的對象很少而且通過復(fù)制就自動解決了碎片問題。而且實際上不需要將內(nèi)存對半分——如果知道對象的存活率很低就可以讓使用區(qū)更大、空閑區(qū)更小。這正是年輕代的特點研究表明年輕代中的對象有98%在第一次GC時就會死亡。既然絕大多數(shù)對象都會死亡那么復(fù)制算法就是最合適的選擇。現(xiàn)代JVM正是基于這個觀察在年輕代采用了改進的復(fù)制算法。不是簡單地把內(nèi)存對半分而是分為一個較大的Eden區(qū)和兩個較小的Survivor區(qū)比例為8:1:1。每次使用Eden區(qū)和其中一個Survivor區(qū)GC時將存活對象復(fù)制到另一個Survivor區(qū)。這樣內(nèi)存利用率就提高到了90%只有10%的空間是空閑的。而且即使在極端情況下存活對象過多、Survivor區(qū)放不下也有老年代作為擔(dān)保多出來的對象可以直接晉升到老年代。這個機制叫做分配擔(dān)保Handle Promotion確保復(fù)制算法在任何情況下都能正常工作。示意圖GC前使用From區(qū) From區(qū): [A][B][C][D][E] To區(qū): [空閑] GC后復(fù)制存活對象到To區(qū) From區(qū): [已清空] To區(qū): [A][C][E] 下次GC時From和To互換2.1.3 標(biāo)記-整理算法Mark-Compact老年代的最佳選擇標(biāo)記-整理算法也稱標(biāo)記-壓縮算法可以看作是標(biāo)記-清除算法的改進版本。它保留了標(biāo)記階段但將清除階段改為整理階段從而解決了內(nèi)存碎片問題同時又避免了復(fù)制算法的內(nèi)存浪費問題。算法的工作流程標(biāo)記-整理算法的執(zhí)行過程可以分為三個步驟第一步是標(biāo)記階段與標(biāo)記-清除算法完全相同從GC Roots開始遍歷對象圖標(biāo)記所有可達的存活對象。第二步是整理階段這是與標(biāo)記-清除算法的關(guān)鍵區(qū)別。整理階段會將所有存活對象向內(nèi)存的一端移動讓它們緊密地排列在一起。這個過程類似于整理書架把所有的書都擠到一邊讓空閑空間集中到另一邊。第三步是清理階段直接清理掉邊界外的所有內(nèi)存。由于所有存活對象都被移到了一端邊界另一端的所有內(nèi)存都是垃圾可以一次性清理掉。為什么需要移動對象你可能會問移動對象不是很麻煩嗎需要更新所有指向這些對象的引用這不是很耗時嗎確實移動對象是有代價的但這個代價是值得的。首先移動對象后內(nèi)存變得緊湊沒有任何碎片。這意味著后續(xù)的內(nèi)存分配可以使用簡單快速的指針碰撞方式不需要維護復(fù)雜的空閑列表也不需要搜索合適大小的空閑塊。其次雖然移動對象需要更新引用但現(xiàn)代JVM有很多技術(shù)來優(yōu)化這個過程比如使用句柄Handle、轉(zhuǎn)發(fā)指針Forwarding Pointer等使得引用更新的開銷可以接受。最重要的是避免內(nèi)存碎片帶來的長期收益遠大于移動對象的一次性開銷。內(nèi)存碎片會導(dǎo)致頻繁GC、降低內(nèi)存利用率、甚至引發(fā)OutOfMemoryError這些問題的代價要比移動對象大得多。老年代為什么選擇標(biāo)記-整理標(biāo)記-整理算法特別適合老年代原因在于老年代的對象特點首先老年代中大部分對象都是長期存活的對象的存活率很高通常超過90%。在這種情況下如果使用復(fù)制算法需要復(fù)制大量對象而且需要預(yù)留同樣大小的空間用于復(fù)制內(nèi)存利用率太低。使用標(biāo)記-整理算法雖然也需要移動對象但不需要額外的空間內(nèi)存利用率高。其次老年代的GC頻率較低。雖然標(biāo)記-整理算法移動對象需要一定時間但由于老年代GC不頻繁這個開銷可以接受。相比之下如果老年代使用標(biāo)記-清除算法產(chǎn)生的內(nèi)存碎片會長期存在持續(xù)影響性能。最后老年代的對象通常比較大。大對象對內(nèi)存碎片更敏感因為需要連續(xù)的大塊空閑空間才能分配。使用標(biāo)記-整理算法確保了總有連續(xù)的大塊空閑空間可用。因此大多數(shù)針對老年代的垃圾回收器如Serial Old、Parallel Old、G1在Mixed GC階段等都采用了標(biāo)記-整理算法或其變種。示意圖標(biāo)記前 [A][B][C][D][E][F][G] ↓ ↓ ↓ ↓ ↓ ↓ ↓ 存活 死 存活 死 存活 死 存活 整理后 [A][C][E][G][ ] ↓ ↓ ↓ ↓ 清空 連續(xù)的存活對象 可用空間2.2 垃圾回收器詳解2.2.1 Serial收集器特點單線程收集器進行GC時必須暫停所有工作線程Stop The World簡單高效適合單CPU環(huán)境Client模式下默認的年輕代收集器適用場景單核CPU或CPU核心數(shù)少的環(huán)境桌面應(yīng)用程序堆內(nèi)存較小的應(yīng)用幾十MB到一兩百MB啟用參數(shù)-XX:UseSerialGC# 年輕代和老年代都使用串行收集器GC日志示例[GC (Allocation Failure) [DefNew: 4416K-512K(4928K), 0.0042640 secs] 4416K-1520K(15872K), 0.0043140 secs]工作流程應(yīng)用線程運行 ↓ 發(fā)生GC ↓ Stop The World所有應(yīng)用線程暫停 ↓ Serial收集器工作單線程 ↓ GC完成 ↓ 恢復(fù)應(yīng)用線程2.2.2 Parallel收集器吞吐量優(yōu)先特點多線程并行收集關(guān)注吞吐量CPU用于運行用戶代碼的時間與CPU總消耗時間的比值JDK 8默認的收集器也稱為吞吐量優(yōu)先收集器Parallel Scavenge年輕代使用復(fù)制算法多線程并行收集可控制吞吐量Parallel Old老年代使用標(biāo)記-整理算法多線程并行收集適用場景后臺計算任務(wù)不需要太多交互的任務(wù)對吞吐量要求高對停頓時間要求不嚴格啟用參數(shù)-XX:UseParallelGC# 年輕代使用Parallel Scavenge-XX:UseParallelOldGC# 老年代使用Parallel Old-XX:ParallelGCThreads4# 設(shè)置并行GC線程數(shù)-XX:MaxGCPauseMillis100# 設(shè)置最大GC停頓時間毫秒-XX:GCTimeRatio99# 設(shè)置吞吐量大小默認99即1%的時間用于GC-XX:UseAdaptiveSizePolicy# 自動調(diào)節(jié)年輕代大小、Eden和Survivor比例等性能對比假設(shè)堆內(nèi)存1GB4核CPU Serial收集器 - GC線程1個 - GC時間100ms - 總停頓100ms Parallel收集器 - GC線程4個 - GC時間30ms理論值實際約40ms - 總停頓40ms - 吞吐量提升約60%2.2.3 CMS收集器Concurrent Mark SweepCMS是一款以獲取最短停頓時間為目標(biāo)的收集器非常適合互聯(lián)網(wǎng)應(yīng)用和B/S架構(gòu)的服務(wù)端應(yīng)用。特點并發(fā)收集、低停頓基于標(biāo)記-清除算法只作用于老年代大部分工作可以與應(yīng)用線程并發(fā)執(zhí)行工作流程四個階段初始標(biāo)記Initial Mark- STW僅標(biāo)記GC Roots直接關(guān)聯(lián)的對象速度很快停頓時間短并發(fā)標(biāo)記Concurrent Mark從GC Roots直接關(guān)聯(lián)對象開始遍歷整個對象圖與應(yīng)用線程并發(fā)執(zhí)行耗時最長但不需要停頓重新標(biāo)記Remark- STW修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運行而導(dǎo)致標(biāo)記變動的對象停頓時間比初始標(biāo)記稍長但遠比并發(fā)標(biāo)記短并發(fā)清除Concurrent Sweep清除標(biāo)記為垃圾的對象與應(yīng)用線程并發(fā)執(zhí)行時間線示意時間 → |---初始標(biāo)記(STW)---|并發(fā)標(biāo)記|---重新標(biāo)記(STW)---|并發(fā)清除| 應(yīng)用停頓 應(yīng)用繼續(xù)運行 應(yīng)用停頓 應(yīng)用繼續(xù)運行 (很短) (最耗時) (較短) (耗時)啟用參數(shù)-XX:UseConcMarkSweepGC# 使用CMS收集器-XX:CMSInitiatingOccupancyFraction70# 老年代使用70%時觸發(fā)CMS默認68%-XX:UseCMSInitiatingOccupancyOnly# 只使用設(shè)定的回收閾值-XX:ConcGCThreads4# 并發(fā)GC線程數(shù)-XX:CMSParallelRemarkEnabled# 降低重新標(biāo)記停頓時間-XX:CMSScavengeBeforeRemark# 重新標(biāo)記前先進行一次年輕代GC-XX:UseCMSCompactAtFullCollection# Full GC后進行碎片整理-XX:CMSFullGCsBeforeCompaction5# 多少次Full GC后進行碎片整理優(yōu)點并發(fā)收集停頓時間短適合對響應(yīng)時間敏感的應(yīng)用用戶體驗好缺點對CPU資源敏感并發(fā)階段會占用部分CPU資源默認啟動的回收線程數(shù)(CPU核心數(shù) 3) / 4在CPU核心少時影響應(yīng)用性能無法處理浮動垃圾并發(fā)標(biāo)記和并發(fā)清除階段用戶線程仍在運行會產(chǎn)生新的垃圾這部分垃圾只能等到下次GC清理需要預(yù)留足夠內(nèi)存給用戶線程使用產(chǎn)生內(nèi)存碎片基于標(biāo)記-清除算法會產(chǎn)生大量碎片可能導(dǎo)致老年代還有很多空間但無法分配大對象不得不提前觸發(fā)Full GC適用場景互聯(lián)網(wǎng)網(wǎng)站、B/S架構(gòu)服務(wù)端對響應(yīng)時間要求高的應(yīng)用堆內(nèi)存較大4GB-20GB多核CPU服務(wù)器2.2.4 G1收集器Garbage First面向未來的垃圾回收器G1Garbage First收集器是垃圾回收技術(shù)的一個里程碑。它從JDK 7開始引入經(jīng)過多年的優(yōu)化和改進在JDK 9中成為默認的垃圾回收器。G1的設(shè)計目標(biāo)雄心勃勃既要保證高吞吐量又要實現(xiàn)可預(yù)測的低延遲還要能夠處理大堆內(nèi)存幾十GB甚至上百GB。這些目標(biāo)在傳統(tǒng)的垃圾回收器中往往是矛盾的但G1通過一系列創(chuàng)新的設(shè)計在很大程度上實現(xiàn)了這些目標(biāo)的平衡。G1的設(shè)計理念全新的內(nèi)存模型G1最大的創(chuàng)新在于徹底改變了堆內(nèi)存的布局方式。傳統(tǒng)的垃圾回收器如CMS將堆內(nèi)存劃分為固定的年輕代和老年代這兩個區(qū)域在物理上是連續(xù)的。而G1引入了全新的Region區(qū)域概念將整個堆內(nèi)存劃分為多個大小相等的獨立區(qū)域。每個Region的大小通常在1MB到32MB之間必須是2的冪次默認情況下G1會將堆劃分為約2048個Region。這些Region在邏輯上可以分為Eden區(qū)、Survivor區(qū)、Old區(qū)和Humongous區(qū)用于存放大對象但在物理上它們是不連續(xù)的可以分散在堆的任何位置。這種設(shè)計有什么好處呢最大的好處是靈活性。在傳統(tǒng)的分代收集器中年輕代和老年代的大小比例是相對固定的調(diào)整起來比較麻煩。而在G1中一個Region可以靈活地在不同角色之間轉(zhuǎn)換。今天它是Eden區(qū)經(jīng)過一次GC后可能變成空閑Region下次可能被用作Old區(qū)。這種動態(tài)分配使得G1能夠根據(jù)應(yīng)用的實際情況自動調(diào)整各個代的大小。可預(yù)測的停頓時間G1的核心優(yōu)勢G1最吸引人的特性是可以設(shè)置期望的GC停頓時間目標(biāo)。通過-XX:MaxGCPauseMillis參數(shù)你可以告訴G1“我希望每次GC的停頓時間不超過200毫秒”。G1會盡力注意不是保證達到這個目標(biāo)。G1是如何做到的呢關(guān)鍵在于它可以選擇性地回收Region。G1會跟蹤每個Region中的垃圾比例并估算回收每個Region所需的時間。在GC時G1不會回收所有的Region而是優(yōu)先選擇收益最高的Region進行回收——即那些垃圾比例高、回收時間短的Region。這就是Garbage First名字的由來優(yōu)先回收垃圾最多的區(qū)域。通過這種機制G1可以在有限的時間內(nèi)停頓時間目標(biāo)回收盡可能多的垃圾。如果停頓時間目標(biāo)設(shè)置得比較緊G1可能只回收幾個Region如果目標(biāo)比較寬松G1就可以回收更多Region獲得更高的回收效率。G1的回收過程年輕代GC與混合GCG1的垃圾回收分為兩種類型年輕代GCYoung GC和混合GCMixed GC。年輕代GC與傳統(tǒng)收集器類似當(dāng)所有Eden Region被占滿時觸發(fā)。G1會回收所有的Eden Region和Survivor Region將存活對象復(fù)制到新的Survivor Region或晉升到Old Region。年輕代GC是完全Stop The World的但由于年輕代對象死亡率高這個過程通常很快?;旌螱C是G1獨有的特性。當(dāng)堆內(nèi)存使用率達到一定閾值時默認45%可通過-XX:InitiatingHeapOccupancyPercent設(shè)置G1會啟動一個并發(fā)標(biāo)記周期標(biāo)記整個堆中的存活對象。標(biāo)記完成后G1就知道了每個Region的垃圾比例。接下來的若干次GC就不只是回收年輕代還會選擇一些垃圾比例高的Old Region一起回收這就是混合GC。混合GC的好處是可以漸進式地回收老年代避免傳統(tǒng)的Full GC那種一次性回收所有老年代的長時間停頓。通過多次混合GC分批回收老年代每次停頓時間都可控。G1堆內(nèi)存布局每個格子代表一個Region [E][E][E][S][O][O][O][H] [E][E][S][O][O][O][H][O] [E][E][E][O][O][O][O][O] [E][S][O][O][O][H][O][O] E Eden區(qū) S Survivor區(qū) O Old區(qū)老年代 H Humongous區(qū)大對象超過Region 50%的對象特點Region化內(nèi)存布局不再區(qū)分年輕代和老年代的物理空間每個Region大小1MB-32MB必須是2的冪次大對象直接分配到Humongous區(qū)可預(yù)測的停頓時間可以設(shè)置期望停頓時間-XX:MaxGCPauseMillisG1會根據(jù)歷史數(shù)據(jù)預(yù)測每個Region的回收價值優(yōu)先回收價值最大的Region并發(fā)與并行并行多個GC線程同時工作停頓期間并發(fā)GC線程與應(yīng)用線程同時工作工作流程年輕代GCYoung GC當(dāng)Eden區(qū)用完時觸發(fā)采用復(fù)制算法完全STW但速度很快存活對象復(fù)制到Survivor或晉升到Old區(qū)混合GCMixed GC當(dāng)堆內(nèi)存使用達到一定閾值時觸發(fā)同時回收年輕代和部分老年代RegionFull GC當(dāng)Mixed GC無法跟上內(nèi)存分配速度時觸發(fā)單線程執(zhí)行停頓時間長應(yīng)盡量避免Full GCG1 GC詳細階段1. 年輕代GCYoung GC- STW - 清空Eden區(qū) - 復(fù)制存活對象到Survivor或Old區(qū) - 暫停時間可控 2. 并發(fā)標(biāo)記周期當(dāng)老年代使用率達到閾值 a. 初始標(biāo)記Initial Mark- STW - 標(biāo)記GC Roots直接關(guān)聯(lián)的對象 - 通常伴隨Young GC一起進行 b. 根區(qū)域掃描Root Region Scan - 掃描Survivor區(qū)對老年代的引用 - 必須在下次Young GC前完成 c. 并發(fā)標(biāo)記Concurrent Mark - 標(biāo)記整個堆的存活對象 - 與應(yīng)用線程并發(fā)執(zhí)行 - 可被Young GC中斷 d. 重新標(biāo)記Remark- STW - 完成標(biāo)記工作 - 使用SATBSnapshot At The Beginning算法 e. 清理Cleanup- 部分STW - 統(tǒng)計每個Region的存活對象 - 回收完全空閑的Region - 重置RSetRemembered Set 3. 混合GCMixed GC - 選擇收益最大的若干Region進行回收 - 包括所有年輕代Region和部分老年代Region啟用參數(shù)# 基礎(chǔ)參數(shù)-XX:UseG1GC# 使用G1收集器-XX:MaxGCPauseMillis200# 設(shè)置期望的最大GC停頓時間毫秒默認200ms-XX:G1HeapRegionSize16m# 設(shè)置Region大小范圍1MB-32MB# 并發(fā)標(biāo)記相關(guān)-XX:InitiatingHeapOccupancyPercent45# 堆使用率達到45%時啟動并發(fā)標(biāo)記默認45-XX:ConcGCThreads4# 并發(fā)GC線程數(shù)# Mixed GC相關(guān)-XX:G1MixedGCCountTarget8# 一次并發(fā)標(biāo)記后最多執(zhí)行8次Mixed GC-XX:G1OldCSetRegionThresholdPercent10# Mixed GC時老年代Region回收的最大比例-XX:G1MixedGCLiveThresholdPercent85# Region中存活對象超過85%不會被選入CSet# 大對象相關(guān)-XX:G1HeapWastePercent5# 允許的浪費堆空間百分比默認5%性能調(diào)優(yōu)建議不要設(shè)置年輕代大小G1會自動調(diào)整年輕代大小以滿足停頓時間目標(biāo)手動設(shè)置會影響G1的自適應(yīng)能力合理設(shè)置停頓時間目標(biāo)不要設(shè)置過小的值如50ms可能導(dǎo)致頻繁GC推薦值200ms-500ms設(shè)置過小可能導(dǎo)致達不到目標(biāo)反而降低吞吐量觀察是否發(fā)生Full GC# 如果頻繁Full GC可以- 增加堆內(nèi)存 - 調(diào)整InitiatingHeapOccupancyPercent提前觸發(fā)并發(fā)標(biāo)記 - 增加并發(fā)標(biāo)記線程數(shù)適用場景堆內(nèi)存較大6GB以上推薦8GB-64GB需要可預(yù)測的停頓時間服務(wù)端應(yīng)用替代CMS的首選方案G1 vs CMS對比特性CMSG1內(nèi)存布局連續(xù)的年輕代/老年代Region化不連續(xù)停頓時間不可預(yù)測可預(yù)測內(nèi)存碎片有碎片問題整理內(nèi)存碎片少大堆支持較差8GB性能下降好可到64GB吞吐量較低較高適用堆大小4GB-8GB6GB-64GB2.2.5 ZGC收集器Z Garbage CollectorZGC是JDK 11引入的一款低延遲垃圾收集器目標(biāo)是讓GC停頓時間不超過10ms。特點停頓時間極短10ms支持TB級別的堆內(nèi)存吞吐量下降不超過15%使用染色指針Colored Pointer和讀屏障Load Barrier技術(shù)核心技術(shù)染色指針Colored Pointer在64位指針中存儲對象的狀態(tài)信息不需要額外的空間存儲標(biāo)記信息讀屏障Load Barrier在對象訪問時插入一小段代碼實現(xiàn)并發(fā)移動對象啟用參數(shù)-XX:UseZGC# 使用ZGC-XX:ZCollectionInterval120# GC間隔時間秒-XX:ZAllocationSpikeTolerance2# 內(nèi)存分配尖峰容忍度適用場景大內(nèi)存服務(wù)器16GB以上對延遲極度敏感的應(yīng)用金融交易系統(tǒng)實時數(shù)據(jù)處理局限性需要JDK 11目前只支持Linux x64平臺JDK 14開始支持Windows和macOS相比G1吞吐量有所下降2.3 如何選擇垃圾回收器選擇合適的垃圾回收器需要考慮多個因素選擇決策樹 應(yīng)用類型 ├─ 單核/桌面應(yīng)用 │ └─ Serial / Serial Old │ ├─ 多核對吞吐量要求高可接受較長停頓 │ └─ Parallel Scavenge Parallel Old │ ├─ 多核對響應(yīng)時間敏感堆內(nèi)存8GB │ └─ CMSParNew CMS │ ├─ 多核需要可預(yù)測停頓堆內(nèi)存6GB-64GB │ └─ G1 │ └─ 大內(nèi)存對延遲極度敏感堆內(nèi)存16GB └─ ZGC具體場景推薦應(yīng)用類型堆內(nèi)存大小推薦收集器理由桌面應(yīng)用200MBSerial簡單高效停頓時間可接受后臺批處理任意Parallel吞吐量優(yōu)先停頓無所謂普通Web應(yīng)用4GBParallel平衡吞吐量和停頓電商/支付4GB-8GBCMS響應(yīng)時間敏感微服務(wù)2GB-8GBG1可預(yù)測停頓易于調(diào)優(yōu)大數(shù)據(jù)/緩存8GBG1大堆支持好交易系統(tǒng)16GBZGC極低延遲要求JDK版本與默認收集器JDK 8Parallel Scavenge Parallel OldJDK 9-13G1JDK 14G1推薦使用ZGC for低延遲場景三、JVM參數(shù)調(diào)優(yōu)實戰(zhàn)3.1 JVM參數(shù)分類JVM參數(shù)主要分為三類JVM參數(shù)分類 ├── 標(biāo)準參數(shù)-開頭 │ ├── -version 查看JVM版本 │ ├── -help 查看幫助 │ ├── -cp/-classpath 設(shè)置類路徑 │ └── 所有JVM都支持穩(wěn)定不變 │ ├── X參數(shù)-X開頭非標(biāo)準參數(shù) │ ├── -Xms 初始堆大小 │ ├── -Xmx 最大堆大小 │ ├── -Xmn 年輕代大小 │ ├── -Xss 線程棧大小 │ └── 所有JVM都支持但可能有差異 │ └── XX參數(shù)-XX:開頭不穩(wěn)定參數(shù) ├── Boolean類型-XX:[參數(shù)名] 啟用 │ -XX:-[參數(shù)名] 禁用 │ 示例-XX:UseG1GC │ └── KV類型-XX:[參數(shù)名][值] 示例-XX:MaxGCPauseMillis2003.2 堆內(nèi)存參數(shù)詳解3.2.1 基礎(chǔ)堆內(nèi)存參數(shù)# 堆內(nèi)存大小設(shè)置-Xms4g# 初始堆大小4GB-Xmx4g# 最大堆大小4GB建議與Xms相同避免動態(tài)擴容-Xmn1g# 年輕代大小1GB一般為堆的1/3到1/4# 為什么Xms和Xmx要設(shè)置相同# 1. 避免運行時堆擴容擴容會導(dǎo)致Full GC# 2. 減少內(nèi)存碎片# 3. 性能更穩(wěn)定可預(yù)測內(nèi)存大小單位k 或 K# KB (kilobytes)m 或 M# MB (megabytes)g 或 G# GB (gigabytes)# 示例-Xms512m# 512MB-Xmx4G# 4GB-Xmn1024m# 1GB3.2.2 年輕代參數(shù)# 方式1直接指定年輕代大小-Xmn2g# 年輕代大小2GB# 方式2通過比例設(shè)置不推薦-XX:NewRatio2# 年輕代與老年代的比例 1:2# 即年輕代占堆的1/3# Eden和Survivor比例-XX:SurvivorRatio8# Eden : Survivor0 : Survivor1 8:1:1# 默認值就是8# 示例堆4GB年輕代1GBSurvivorRatio8# 則內(nèi)存分布為# Eden: 800MB (1GB * 8/10)# S0: 100MB (1GB * 1/10)# S1: 100MB (1GB * 1/10)# Old: 3GB年輕代大小如何設(shè)置# 經(jīng)驗法則# 1. 年輕代一般設(shè)置為堆的1/4到1/3# 2. 年輕代太小Minor GC頻繁對象過早進入老年代# 3. 年輕代太大Minor GC時間長老年代空間不足# 不同應(yīng)用類型推薦# 短生命周期對象多Web應(yīng)用年輕代可適當(dāng)大一些1/3# 長生命周期對象多緩存應(yīng)用年輕代可適當(dāng)小一些1/43.2.3 老年代參數(shù)# 對象晉升年齡閾值-XX:MaxTenuringThreshold15# 對象在Survivor區(qū)經(jīng)過15次GC后晉升到老年代# 默認值15CMS為6# 范圍0-15# 大對象直接進入老年代的閾值-XX:PretenureSizeThreshold3m# 大于3MB的對象直接分配到老年代# 默認為0即不設(shè)限制# 僅對Serial和ParNew有效# 晉升擔(dān)保參數(shù)-XX:HandlePromotionFailure# 允許擔(dān)保失敗JDK 6 Update 24之后默認開啟3.3 垃圾回收參數(shù)詳解3.3.1 通用GC參數(shù)# GC日志參數(shù)JDK 8-XX:PrintGC# 打印GC簡要信息-XX:PrintGCDetails# 打印GC詳細信息-XX:PrintGCTimeStamps# 打印GC時間戳相對JVM啟動時間-XX:PrintGCDateStamps# 打印GC日期時間戳-XX:PrintHeapAtGC# GC前后打印堆信息-Xloggc:/path/to/gc.log# GC日志輸出到文件# GC日志參數(shù)JDK 9統(tǒng)一日志-Xlog:gc# 基本GC日志-Xlog:gc*# 詳細GC日志-Xlog:gc:file/path/to/gc.log# 輸出到文件-Xlog:gc*:file/path/to/gc.log:time,uptime,level,tags# 完整格式# GC日志文件管理-XX:UseGCLogFileRotation# 啟用GC日志滾動-XX:NumberOfGCLogFiles10# GC日志文件數(shù)量-XX:GCLogFileSize100M# 每個GC日志文件大小# 其他有用參數(shù)-XX:PrintGCApplicationStoppedTime# 打印應(yīng)用停頓時間-XX:PrintGCApplicationConcurrentTime# 打印應(yīng)用運行時間-XX:PrintTenuringDistribution# 打印對象年齡分布-XX:PrintReferenceGC# 打印引用處理信息3.3.2 Parallel收集器參數(shù)# 啟用Parallel收集器-XX:UseParallelGC# 年輕代使用Parallel Scavenge-XX:UseParallelOldGC# 老年代使用Parallel Old# JDK 8中設(shè)置其中一個另一個自動啟用# 并行GC線程數(shù)-XX:ParallelGCThreads8# 設(shè)置并行GC線程數(shù)# 默認值CPU核心數(shù)核心數(shù)8# 默認值3 (5 * CPU核心數(shù) / 8)核心數(shù)8# 性能目標(biāo)設(shè)置-XX:MaxGCPauseMillis200# 最大GC停頓時間目標(biāo)毫秒# JVM會嘗試調(diào)整堆大小和其他參數(shù)來達到目標(biāo)# 不是硬性保證-XX:GCTimeRatio99# 設(shè)置吞吐量大小# 公式吞吐量 1 - 1/(1GCTimeRatio)# 99表示1%的時間用于GC99%用于應(yīng)用# 默認值99# 自適應(yīng)調(diào)節(jié)策略-XX:UseAdaptiveSizePolicy# 啟用自適應(yīng)策略默認開啟# JVM自動調(diào)整年輕代大小、Eden/Survivor比例、# 晉升閾值等參數(shù)以達到性能目標(biāo)-XX:-UseAdaptiveSizePolicy# 禁用自適應(yīng)策略# 示例吞吐量優(yōu)先配置后臺批處理-Xms8g -Xmx8g -XX:UseParallelGC -XX:ParallelGCThreads8-XX:GCTimeRatio99-XX:UseAdaptiveSizePolicy3.3.3 CMS收集器參數(shù)# 啟用CMS-XX:UseConcMarkSweepGC# 老年代使用CMS-XX:UseParNewGC# 年輕代使用ParNewCMS自動啟用# 觸發(fā)CMS GC的時機-XX:CMSInitiatingOccupancyFraction70# 老年代使用70%時觸發(fā)CMS# 默認68%JDK 6# 設(shè)置過高可能來不及回收導(dǎo)致Concurrent Mode Failure# 設(shè)置過低GC過于頻繁浪費CPU-XX:UseCMSInitiatingOccupancyOnly# 只使用設(shè)定的閾值觸發(fā)CMS# 不使用JVM的動態(tài)計算# 并發(fā)線程數(shù)-XX:ConcGCThreads4# CMS并發(fā)線程數(shù)# 默認(ParallelGCThreads 3) / 4-XX:ParallelGCThreads8# 并行GC線程數(shù)用于STW階段# 優(yōu)化重新標(biāo)記階段-XX:CMSParallelRemarkEnabled# 啟用并行重新標(biāo)記默認開啟-XX:CMSScavengeBeforeRemark# 重新標(biāo)記前先進行一次Minor GC# 減少年輕代對象對老年代的引用縮短重新標(biāo)記時間# 內(nèi)存碎片處理-XX:UseCMSCompactAtFullCollection# Full GC時進行碎片整理默認開啟# 但整理會STW時間較長-XX:CMSFullGCsBeforeCompaction5# 多少次Full GC后進行一次碎片整理# 默認0每次Full GC都整理# 設(shè)置為5每5次Full GC后整理一次# 類卸載-XX:CMSClassUnloadingEnabled# 允許CMS回收方法區(qū)永久代/元空間# JDK 8默認開啟# 增量模式已廢棄不推薦-XX:CMSIncrementalMode# CMS增量模式JDK 9已移除# 失敗處理# 如果CMS運行期間無法滿足內(nèi)存分配需求會出現(xiàn)Concurrent Mode Failure# 此時會退化為Serial Old進行Full GC停頓時間很長# 解決方案# 1. 降低CMSInitiatingOccupancyFraction提前觸發(fā)CMS# 2. 增加堆內(nèi)存# 3. 優(yōu)化代碼減少對象創(chuàng)建# 示例低延遲配置Web應(yīng)用-Xms6g -Xmx6g -Xmn2g -XX:UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction70-XX:UseCMSInitiatingOccupancyOnly -XX:CMSScavengeBeforeRemark -XX:CMSParallelRemarkEnabled -XX:ParallelGCThreads8-XX:ConcGCThreads23.3.4 G1收集器參數(shù)# 啟用G1-XX:UseG1GC# 使用G1收集器JDK 9默認# Region大小-XX:G1HeapRegionSize16m# 設(shè)置Region大小1MB-32MB必須是2的冪# 默認堆大小 / 2048# 目標(biāo)是有2048個Region# 停頓時間目標(biāo)-XX:MaxGCPauseMillis200# 期望的最大GC停頓時間毫秒# 默認200ms# 這是一個軟目標(biāo)不是硬性保證# 不要設(shè)置過小否則降低吞吐量# 并發(fā)標(biāo)記相關(guān)-XX:InitiatingHeapOccupancyPercent45# 堆使用率達到45%時啟動并發(fā)標(biāo)記周期# 默認45# IHOP越小越早觸發(fā)并發(fā)標(biāo)記越不容易Full GC-XX:ConcGCThreads4# 并發(fā)標(biāo)記的線程數(shù)# 默認ParallelGCThreads / 4-XX:ParallelGCThreads8# 并行GC線程數(shù)STW階段# 默認CPU核心數(shù)核心8# Mixed GC相關(guān)-XX:G1MixedGCCountTarget8# 一次并發(fā)標(biāo)記周期后目標(biāo)執(zhí)行的Mixed GC次數(shù)# 默認8# 增加此值可以減少每次Mixed GC的停頓時間-XX:G1HeapWastePercent5# 允許的堆空間浪費百分比# 默認5# 當(dāng)可回收空間小于這個值時不啟動Mixed GC-XX:G1MixedGCLiveThresholdPercent85# Region中存活對象超過85%不會被選入CSet# 默認85# 避免回收價值不高的Region-XX:G1OldCSetRegionThresholdPercent10# Mixed GC時老年代Region數(shù)量最大占比# 默認10# 大對象相關(guān)-XX:G1ReservePercent10# 保留的堆空間百分比防止晉升失敗# 默認10# 記憶集Remembered Set相關(guān)-XX:G1RSetUpdatingPauseTimePercent10# 允許用于更新RSet的停頓時間百分比# 默認10# StringDeduplication字符串去重-XX:UseStringDeduplication# 啟用字符串去重-XX:StringDeduplicationAgeThreshold3# 字符串達到此年齡后進行去重檢查# 示例1標(biāo)準Web應(yīng)用配置-Xms8g -Xmx8g -XX:UseG1GC -XX:MaxGCPauseMillis200-XX:InitiatingHeapOccupancyPercent45-XX:ParallelGCThreads8-XX:ConcGCThreads2# 示例2大堆內(nèi)存配置16GB-Xms16g -Xmx16g -XX:UseG1GC -XX:G1HeapRegionSize32m -XX:MaxGCPauseMillis200-XX:InitiatingHeapOccupancyPercent40-XX:G1ReservePercent15# 示例3低延遲配置-Xms8g -Xmx8g -XX:UseG1GC -XX:MaxGCPauseMillis100-XX:InitiatingHeapOccupancyPercent35-XX:G1ReservePercent15-XX:ParallelGCThreads8-XX:ConcGCThreads4G1調(diào)優(yōu)建議不要手動設(shè)置年輕代大小-Xmn、-XX:NewRatioG1會自動調(diào)整以滿足停頓時間目標(biāo)不要設(shè)置過于激進的停頓時間目標(biāo)設(shè)置過小會頻繁GC降低吞吐量推薦200ms起步觀察GC日志關(guān)注Full GCFull GC說明調(diào)優(yōu)不當(dāng)可以降低IHOP提前觸發(fā)并發(fā)標(biāo)記大對象優(yōu)化避免創(chuàng)建超過Region 50%的對象考慮拆分大對象3.3.5 ZGC收集器參數(shù)# 啟用ZGC-XX:UseZGC# 使用ZGC需要JDK 11# 并發(fā)線程數(shù)-XX:ConcGCThreads4# 并發(fā)GC線程數(shù)# 默認CPU核心數(shù) / 8# GC觸發(fā)時機-XX:ZCollectionInterval0# GC間隔時間秒# 默認0不基于時間觸發(fā)-XX:ZAllocationSpikeTolerance2# 內(nèi)存分配尖峰容忍度# 默認2# 示例ZGC配置大內(nèi)存、低延遲-Xms32g -Xmx32g -XX:UseZGC -XX:ConcGCThreads8-Xlog:gc*:file/path/to/gc.log3.4 元空間參數(shù)# 元空間大小JDK 8-XX:MetaspaceSize256m# 初始元空間大小# 默認約21MB平臺相關(guān)# 達到此值會觸發(fā)Full GC-XX:MaxMetaspaceSize512m# 最大元空間大小# 默認無限制只受系統(tǒng)內(nèi)存限制# 建議設(shè)置上限防止內(nèi)存泄漏-XX:MinMetaspaceFreeRatio40# 最小空閑比例-XX:MaxMetaspaceFreeRatio70# 最大空閑比例# 用于控制元空間的擴容和縮容# 永久代大小JDK 7及以前-XX:PermSize256m# 初始永久代大小-XX:MaxPermSize512m# 最大永久代大小元空間調(diào)優(yōu)建議# 問題頻繁Full GC日志顯示Metadata GC Threshold# 原因元空間不足頻繁觸發(fā)Full GC# 解決增大MetaspaceSize# 典型配置-XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m# 大型應(yīng)用Spring Boot、微服務(wù)-XX:MetaspaceSize512m -XX:MaxMetaspaceSize1024m# 動態(tài)類加載多的應(yīng)用Groovy、反射多-XX:MetaspaceSize1g -XX:MaxMetaspaceSize2g3.5 線程棧參數(shù)# 線程棧大小-Xss512k# 每個線程的棧大小為512KB# 默認1MBLinux/Windows# 512KBmacOS# 棧大小影響# 1. 棧太小StackOverflowError遞歸調(diào)用深度受限# 2. 棧太大浪費內(nèi)存能創(chuàng)建的線程數(shù)變少# 線程數(shù)計算公式# 最大線程數(shù) ≈ (系統(tǒng)內(nèi)存 - Xmx - MaxMetaspaceSize) / Xss線程棧大小建議應(yīng)用類型推薦值說明普通應(yīng)用512k-1m默認值遞歸深的應(yīng)用2m-4m避免StackOverflowError高并發(fā)線程數(shù)多256k-512k節(jié)省內(nèi)存支持更多線程3.6 性能監(jiān)控與診斷參數(shù)# OOM時自動dump堆-XX:HeapDumpOnOutOfMemoryError# OOM時自動生成堆轉(zhuǎn)儲文件-XX:HeapDumpPath/path/to/dumps/# 堆轉(zhuǎn)儲文件保存路徑# JMX監(jiān)控-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port9999-Dcom.sun.management.jmxremote.authenticatefalse -Dcom.sun.management.jmxremote.sslfalse# 啟用JFRJava Flight Recorder-XX:UnlockCommercialFeatures# JDK 8需要JDK 11不需要-XX:FlightRecorder -XX:StartFlightRecordingduration60s,filename/path/to/recording.jfr# 性能相關(guān)-XX:AlwaysPreTouch# 啟動時預(yù)先分配物理內(nèi)存# 避免運行時因分配內(nèi)存導(dǎo)致延遲# 適合對延遲敏感的應(yīng)用-XX:UseLargePages# 使用大頁內(nèi)存需要系統(tǒng)支持# 減少TLB miss提升性能3.7 實戰(zhàn)場景參數(shù)配置場景1電商Web應(yīng)用4核8GB服務(wù)器# 特點# - 高并發(fā)對響應(yīng)時間敏感# - 對象生命周期短# - 需要低延遲java -jar application.jar-Xms4g-Xmx4g-Xmn1g-Xss512k-XX:MetaspaceSize256m-XX:MaxMetaspaceSize512m-XX:UseG1GC-XX:MaxGCPauseMillis200-XX:ParallelGCThreads4-XX:ConcGCThreads1-XX:InitiatingHeapOccupancyPercent45-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/logs/heapdump/-Xlog:gc*:file/logs/gc.log:time,uptime,level,tags場景2大數(shù)據(jù)處理應(yīng)用16核32GB服務(wù)器# 特點# - 吞吐量優(yōu)先# - 對停頓時間不敏感# - 批處理任務(wù)java -jar batch-processor.jar-Xms28g-Xmx28g-Xmn8g-Xss256k-XX:MetaspaceSize512m-XX:MaxMetaspaceSize1g-XX:UseParallelGC-XX:ParallelGCThreads16-XX:GCTimeRatio99-XX:UseAdaptiveSizePolicy-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/logs/heapdump/-Xlog:gc*:file/logs/gc.log:time,level,tags場景3微服務(wù)應(yīng)用2核4GB容器# 特點# - 資源受限# - 容器化部署# - 需要快速啟動java -jar microservice.jar-Xms2g-Xmx2g-Xss256k-XX:MetaspaceSize128m-XX:MaxMetaspaceSize256m-XX:UseG1GC-XX:MaxGCPauseMillis200-XX:UseContainerSupport-XX:InitialRAMPercentage50.0-XX:MaxRAMPercentage80.0-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/logs/heapdump.hprof-Xlog:gc:file/logs/gc.log場景4金融交易系統(tǒng)32核64GB服務(wù)器# 特點# - 極低延遲要求10ms# - 大內(nèi)存# - 高并發(fā)java -jar trading-system.jar-Xms48g-Xmx48g-Xss512k-XX:MetaspaceSize512m-XX:MaxMetaspaceSize1g-XX:UseZGC-XX:ConcGCThreads8-XX:AlwaysPreTouch-XX:UseLargePages-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/logs/heapdump/-Xlog:gc*:file/logs/gc.log:time,uptime,level,tags四、JVM監(jiān)控工具詳解4.1 命令行工具JVM問題排查的瑞士軍刀JDK自帶了一套功能強大的命令行工具它們是每個Java開發(fā)者和運維人員必須掌握的利器。這些工具雖然看起來不起眼沒有華麗的圖形界面但在實際的生產(chǎn)環(huán)境問題排查中它們往往是最快速、最有效的選擇。特別是在很多生產(chǎn)環(huán)境中出于安全考慮無法使用圖形界面工具這時候這些命令行工具就成了唯一的選擇。這些工具的另一個優(yōu)勢是輕量級。它們不需要在目標(biāo)JVM中安裝任何agent不需要修改應(yīng)用程序只需要連接到目標(biāo)進程就可以獲取信息。這意味著你可以在不影響應(yīng)用運行的情況下進行監(jiān)控和診斷這對生產(chǎn)環(huán)境來說至關(guān)重要。讓我們逐一了解這些工具不僅要知道它們的基本用法更要理解在什么場景下使用它們最合適如何解讀它們的輸出信息以及在實際問題排查中如何組合使用這些工具。4.1.1 jps - 查看Java進程問題排查的第一步j(luò)psJava Virtual Machine Process Status Tool是JVM進程狀態(tài)工具它的作用類似于Linux的ps命令但專門用于列出Java進程。這個工具看似簡單但卻是所有JVM問題排查的第一步——你首先需要知道要排查哪個Java進程。在生產(chǎn)環(huán)境中可能同時運行著多個Java應(yīng)用比如多個微服務(wù)、多個后臺任務(wù)等。jps可以幫助你快速定位到目標(biāo)進程的PID然后才能使用jstat、jmap等工具進行進一步的診斷。# 基本用法jps# 顯示Java進程ID和主類名jps -l# 顯示完整的類名或jar路徑j(luò)ps -m# 顯示傳遞給main方法的參數(shù)jps -v# 顯示JVM參數(shù)# 輸出示例$ jps -l12345com.example.Application12346org.apache.catalina.startup.Bootstrap12347org.elasticsearch.bootstrap.Elasticsearch $ jps -v12345Application -Xms4g -Xmx4g -XX:UseG1GC4.1.2 jstat - 查看JVM統(tǒng)計信息性能監(jiān)控的核心工具jstatJVM Statistics Monitoring Tool是我個人認為JDK自帶工具中最實用、使用頻率最高的一個。它可以實時顯示JVM的各種運行數(shù)據(jù)包括類加載信息、垃圾收集統(tǒng)計、編譯統(tǒng)計等是性能分析和問題排查的核心工具。jstat的強大之處在于它可以持續(xù)監(jiān)控JVM的狀態(tài)變化。與jmap等一次性工具不同jstat可以按指定的時間間隔反復(fù)采集數(shù)據(jù)讓你看到JVM狀態(tài)的動態(tài)變化。比如你可以觀察Eden區(qū)是如何逐漸被填滿的Minor GC的頻率如何老年代的使用率是否在持續(xù)上升等等。這些動態(tài)信息對于理解應(yīng)用的運行特征、發(fā)現(xiàn)潛在問題至關(guān)重要。在實際工作中當(dāng)接到應(yīng)用響應(yīng)變慢、內(nèi)存使用率高等問題報告時我通常第一時間就是用jstat查看GC情況。很多性能問題的根源都可以通過jstat快速定位是頻繁的Minor GC導(dǎo)致的嗎還是發(fā)生了Full GC老年代使用率是否異常這些問題的答案往往能指引后續(xù)的排查方向。# 基本語法jstat -optionpidintervalcount# option選項# -gc 垃圾收集統(tǒng)計# -gcutil 垃圾收集統(tǒng)計百分比# -gccause 垃圾收集統(tǒng)計 最近GC原因# -gcnew 年輕代統(tǒng)計# -gcold 老年代統(tǒng)計# -class 類加載統(tǒng)計# -compiler JIT編譯統(tǒng)計# 查看GC情況每1秒輸出一次共10次jstat -gc12345100010# 輸出示例S0C S1C S0U S1U EC EU OC OU MC MU YGC YGCT FGC FGCT GCT102401024010240819204567820480010240051200480001561.23450.5671.801# 字段說明S0C: Survivor0容量(KB)S1C: Survivor1容量(KB)S0U: Survivor0使用量(KB)S1U: Survivor1使用量(KB)EC: Eden區(qū)容量(KB)EU: Eden區(qū)使用量(KB)OC: 老年代容量(KB)OU: 老年代使用量(KB)MC: 元空間容量(KB)MU: 元空間使用量(KB)YGC: Young GC次數(shù) YGCT: Young GC總耗時(秒)FGC: Full GC次數(shù) FGCT: Full GC總耗時(秒)GCT: 所有GC總耗時(秒)更詳細的統(tǒng)計百分比jstat -gcutil123451000# 輸出示例S0 S1 E O M CCS YGC YGCT FGC FGCT GCT10.00.055.850.093.788.21561.23450.5671.801# 字段說明S0: Survivor0使用率(%)S1: Survivor1使用率(%)E: Eden區(qū)使用率(%)O: 老年代使用率(%)M: 元空間使用率(%)CCS: 壓縮類空間使用率(%)YGC: Young GC次數(shù) YGCT: Young GC總耗時(秒)FGC: Full GC次數(shù) FGCT: Full GC總耗時(秒)GCT: 所有GC總耗時(秒)查看GC原因jstat -gccause123451000# 輸出示例S0 S1 E O M CCS YGC YGCT FGC FGCT GCT LGCC GCC0.010.055.850.093.788.21571.24550.5671.812Allocation Failure No GC# LGCC: 最近一次GC的原因Last GC Cause# GCC: 當(dāng)前GC的原因Current GC Cause實戰(zhàn)技巧# 1. 持續(xù)監(jiān)控輸出到文件jstat -gcutil123451000gc_monitor.log# 2. 快速判斷是否有Full GCjstat -gccause12345100010|grep-ifull# 3. 監(jiān)控老年代增長速度watch-n1jstat -gc 12345 | tail -1# 4. 計算GC頻率和平均時間# Young GC平均時間 YGCT / YGC# Full GC平均時間 FGCT / FGC4.1.3 jmap - 內(nèi)存映像工具jmap用于生成堆轉(zhuǎn)儲快照heap dump和查看內(nèi)存信息。# 1. 生成堆轉(zhuǎn)儲文件jmap -dump:formatb,file/tmp/heap.hprof12345# live選項只dump存活對象jmap -dump:live,formatb,file/tmp/heap_live.hprof12345# 2. 查看堆內(nèi)存使用情況jmap -heap12345# 輸出示例Attaching to process ID12345, please wait... Heap Configuration: MinHeapFreeRatio40MaxHeapFreeRatio70MaxHeapSize4294967296(4096.0MB)NewSize1073741824(1024.0MB)MaxNewSize1073741824(1024.0MB)OldSize3221225472(3072.0MB)NewRatio2SurvivorRatio8MetaspaceSize268435456(256.0MB)MaxMetaspaceSize536870912(512.0MB)G1HeapRegionSize16777216(16.0MB)Heap Usage: G1 Heap: regions256capacity4294967296(4096.0MB)used2147483648(2048.0MB)free2147483648(2048.0MB)50.0% used# 3. 查看對象統(tǒng)計按內(nèi)存占用排序jmap -histo12345|head-20# 輸出示例num#instances #bytes class name----------------------------------------------1:123456987654320[C2:98765456789012java.lang.String3:45678234567890byte[]4:1234598765432java.util.HashMap$Node# 4. 查看存活對象觸發(fā)Full GCjmap -histo:live12345|head-20# 5. 查看類加載器統(tǒng)計jmap -clstats12345實戰(zhàn)場景# 場景1分析內(nèi)存泄漏# 1. 生成兩個heap dump間隔一段時間jmap -dump:live,formatb,file/tmp/heap1.hprof12345# 等待10分鐘jmap -dump:live,formatb,file/tmp/heap2.hprof12345# 2. 使用MAT工具對比兩個文件找出持續(xù)增長的對象# 場景2排查OOM# 當(dāng)應(yīng)用即將OOM時手動dumpjmap -dump:formatb,file/tmp/oom_heap.hprof12345# 場景3快速查看內(nèi)存占用最多的對象jmap -histo:live12345|head-20# 注意事項# 1. jmap -dump會觸發(fā)Full GC生產(chǎn)環(huán)境慎用# 2. dump文件很大確保有足夠磁盤空間# 3. dump過程中應(yīng)用會暫停STW4.1.4 jstack - 線程堆棧工具jstack用于生成線程快照thread dump分析線程狀態(tài)、死鎖等問題。# 1. 生成線程dumpjstack12345/tmp/thread_dump.txt# 2. 檢測死鎖jstack -l12345# 3. 強制dump進程無響應(yīng)時jstack -F12345# 線程dump輸出示例http-nio-8080-exec-10#123 daemon prio5 os_prio0 tid0x00007f8b2c001000 nid0x1a2b waiting on condition [0x00007f8abc123000]java.lang.Thread.State: WAITING(parking)at sun.misc.Unsafe.park(Native Method)at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)at com.example.Service.process(Service.java:123)# 線程狀態(tài)說明RUNNABLE: 運行中 BLOCKED: 阻塞等待鎖 WAITING: 等待wait、park TIMED_WAITING: 超時等待sleep、wait with timeout TERMINATED: 已終止實戰(zhàn)場景# 場景1排查CPU 100%# 1. 找到進程IDjps -l# 2. 找到占用CPU高的線程top-H -p12345# 假設(shè)線程ID為 6827十進制# 3. 轉(zhuǎn)換為十六進制printf%x
6827# 輸出1aab# 4. 在thread dump中查找 nid0x1aab 的線程jstack12345|grep-A20nid0x1aab# 場景2檢測死鎖jstack -l12345|grep-ideadlock-A20# 場景3分析線程狀態(tài)分布jstack12345|grepjava.lang.Thread.State|sort|uniq-c# 輸出示例# 15 RUNNABLE# 120 WAITING# 10 TIMED_WAITING# 5 BLOCKED# 場景4找出長時間等待的線程jstack12345|grep-A5WAITING4.1.5 jinfo - 配置信息工具# 1. 查看所有JVM參數(shù)jinfo12345# 2. 查看系統(tǒng)屬性jinfo -sysprops12345# 3. 查看JVM flagsjinfo -flags12345# 輸出示例Non-default VM flags: -XX:ConcGCThreads2-XX:G1HeapRegionSize16777216-XX:InitialHeapSize4294967296-XX:MaxHeapSize4294967296-XX:UseG1GC# 4. 查看特定參數(shù)值jinfo -flag MaxHeapSize12345# 輸出-XX:MaxHeapSize4294967296# 5. 動態(tài)修改參數(shù)僅支持manageable標(biāo)記的參數(shù)jinfo -flag PrintGC12345# 開啟GC日志jinfo -flag -PrintGC12345# 關(guān)閉GC日志jinfo -flagPrintGCDetailstrue12345# 查看可動態(tài)修改的參數(shù)java -XX:PrintFlagsFinal -version|grepmanageable4.2 可視化工具4.2.1 JConsoleJConsole是JDK自帶的圖形化監(jiān)控工具可以監(jiān)控內(nèi)存、線程、類、CPU等信息。啟動方式# 1. 直接啟動連接本地進程jconsole# 2. 遠程連接需要配置JMXjconsolehostname:9999# JMX配置-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port9999-Dcom.sun.management.jmxremote.authenticatefalse -Dcom.sun.management.jmxremote.sslfalse主要功能概述CPU使用率、堆內(nèi)存、線程數(shù)、類加載數(shù)內(nèi)存堆內(nèi)存、非堆內(nèi)存使用情況可以手動執(zhí)行GC線程線程列表、線程狀態(tài)、死鎖檢測類已加載類數(shù)量、已卸載類數(shù)量VM摘要JVM參數(shù)、系統(tǒng)屬性MBean查看和修改MBean屬性適用場景快速查看JVM運行狀態(tài)開發(fā)測試環(huán)境監(jiān)控簡單的性能分析4.2.2 VisualVMVisualVM是功能更強大的可視化監(jiān)控工具支持插件擴展。啟動方式# JDK 8及以前自帶jvisualvm# JDK 9需要單獨下載# https://visualvm.github.io/主要功能監(jiān)視CPU使用率堆內(nèi)存、元空間使用情況類加載數(shù)量線程數(shù)量線程線程狀態(tài)時間線死鎖檢測線程dump抽樣器CPU抽樣找出占用CPU最多