天天看點

JVM調優JVM調優

JVM調優

JVM記憶體

JVM調優JVM調優

JVM腦圖

JVM調優JVM調優

垃圾回收算法

  1. 标記-複制算法

為了解決效率問題,“複制”收集算法出現了。它可以将記憶體分為大小相同的兩塊,每次使用其中的一塊。當這一塊的記憶體使用完後,就将還存活的對象複制到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的記憶體回收都是對記憶體區間的一半進行回收。

  1. 标記-清除算法

算法分為“标記”和“清除”階段:标記存活的對象, 統一回收所有未被标記的對象(一般選擇這種);也可以反過來,标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象 。但是會帶來兩個問題:

  • 效率問題:如果需要标記的對象太多,效率不高
  • 空間問題:标記清除後會産生大量不連續的碎片
  1. 标記-整理算法

根據老年代的特點特出的一種标記算法,标記過程仍然與“标記-清除”算法一樣,但後續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一端移動,然後直接清理掉端邊界以外的記憶體。

  1. 指針壓縮

通過指針壓縮來減小對記憶體的消耗,來提高記憶體的使用率。

  1. 對象記憶體配置設定-棧上配置設定

棧上記憶體的特點為線程所有,不存在共享資料,是以就可以做到用完就銷毀,就不需要垃圾回收,基于這個特點,JVM會對對象進行是否可以在棧上配置設定的判斷-“逃逸分析”和 “标量替換”。

堆(Heap)

JVM調優基本上都是圍繞着堆的調整來優化垃圾回收時機。首先我們來看一下垃圾回收算法都有哪些。

堆的劃分

分帶收集理論

目前虛拟機的垃圾收集都采用分代收集算法,這種算法沒有什麼新的思想,隻是根據對象存活周期的不同将記憶體分為幾塊。一般将java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合适的垃圾收集算法。

比如在新生代中,每次收集都會有大量對象(近99%)死去,是以可以選擇複制算法,隻需要付出少量對象的複制成本就可 以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,而且沒有額外的空間對它進行配置設定擔保,是以我們必須選擇“标記-清除”或“标記-整理”算法進行垃圾收集。注意,“标記-清除”或“标記-整理”算法會比複制算法慢10倍以上。

虛拟機一般把堆分為兩部分 年輕代 和老年代。

年輕代

大多數情況下,對象在新生代中 Eden 區配置設定。當 Eden 區沒有足夠空間進行配置設定時,虛拟機将發起一次Minor GC。年輕代采用的垃圾回收算法一般都是标記-複制算法,是以需要把年輕代再次劃分為多個區域。

JVM調優JVM調優

Eden與Survivor區預設8:1:1

大量的對象被配置設定在eden區,eden區滿了後會觸發yang gc,可能會有99%以上的對象成為垃圾被回收掉,剩餘存活的對象會被挪到為空的那塊survivor區,下一次eden區滿了後又會觸發minor gc,把eden區和survivor區垃圾對象回收,把剩餘存活的對象一次性挪動到另外一塊為空的survivor區,因為新生代的對象都是朝生夕死的,存活時間很短,是以JVM預設的8:1:1的比例是很合适的,讓eden區盡量的大,survivor區夠用即可,JVM預設有這個參數-XX:+UseAdaptiveSizePolicy(預設開啟),會導緻這個8:1:1比例自動變化,如果不想這個比例有變化可以設定參數-XX:-UseAdaptiveSizePolicy。

老年代

進入老年代的對象隻能被Full GC收集,是以我們要控制進入老年代的對象,盡量減少Full GC的次數。對象進入老年代的途徑:

  1. 大對象直接進入老年代

大對象就是需要大量連續記憶體空間的對象(比如:字元串、數組)。JVM參數 -XX:PretenureSizeThreshold 可以設定大對象的大小,如果對象超過設定大小會直接進入老年代,不會進入年輕代,這個參數隻在 Serial 和ParNew兩個收集器下有效。

  1. 長期存活的對象晉升到老年代

既然虛拟機采用了分代收集的思想來管理記憶體,那麼記憶體回收時就必須能識别哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這一點,虛拟機給每個對象一個對象年齡(Age)計數器。如果對象在 Eden 出生并經過第一次 Minor GC 後仍然能夠存活,并且能被 Survivor 容納的話,将被移動到 Survivor空間中,并将對象年齡設為1。對象在 Survivor 中每熬過一次 MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15歲,CMS收集器預設6歲,不同的垃圾收集器會略微有點不同),就會被晉升到老年代中。對象晉升到老年代的年齡門檻值,可以通過參數 -XX:MaxTenuringThreshold 來設定。

  1. 對象動态年齡判斷

目前放對象的Survivor區域裡(其中一塊區域,放對象的那塊s區),一批對象的總大小大于這塊Survivor區域記憶體大小的50%(-XX:TargetSurvivorRatio可以指定),那麼此時大于等于這批對象年齡最大值的對象,就可以直接進入老年代了,例如Survivor區域裡現在有一批對象,年齡1+年齡2+年齡n的多個年齡對象總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的對象都放入老年代。這個規則其實是希望那些可能是長期存活的對象,盡早進入老年代。對象動态年齡判斷機制一般是在yang gc之後觸發的。

觸發Full GC的時機

1. System.gc()方法的調用

在代碼中調用System.gc()方法會建議JVM進行Full GC,但是注意這隻是建議,JVM執行不執行是另外一回事兒,不過在大多數情況下會增加Full GC的次數,導緻系統性能下降,一般建議不要手動進行此方法的調用,可以通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

2. 老年代(Tenured Gen)空間不足

在Survivor區域的對象滿足晉升到老年代的條件時,晉升進入老年代的對象大小大于老年代的可用記憶體,這個時候會觸發Full GC。

3. Metaspace區記憶體達到門檻值

Metaspace使用的是本地記憶體,而不是堆記憶體,也就是說在預設情況下Metaspace的大小隻與本地記憶體大小有關。-XX:MetaspaceSize=21810376B(約為20.8MB)超過這個值就會引發Full GC,這個值不是固定的,是會随着JVM的運作進行動态調整的。

4. 老年代空間擔保配置設定機制

年輕代每次minor gc之前JVM都會計算下老年代剩餘可用空間,如果這個可用空間小于年輕代裡現有的所有對象大小之和(包括垃圾對象) 就會看一個“-XX:-HandlePromotionFailure”(jdk1.8預設就設定了)的參數是否設定了如果有這個參數,就會看看老年代的可用記憶體大小,是否大于之前每一次yang gc後進入老年代的對象的**平均大小。**如果上一步結果是小于或者之前說的參數沒有設定,那麼就會觸發一次Full gc,對老年代和年輕代一起回收一次垃圾,

如果回收完還是沒有足夠空間存放新的對象就會發生"OOM"當然,如果yang gc之後剩餘存活的需要挪動到老年代的對象大小還是大于老年代可用空間,那麼也會觸發full gc,full gc完之後如果還是沒有空間放yang gc之後的存活對象,則也會發生“OOM”。

JVM調優

相關指令

前置啟動程式 jps

事先啟動一個web應用程式,用jps檢視其程序id,接着用各種jdk自帶指令優化應用

Jmap

此指令可以用來檢視記憶體資訊,執行個體個數以及占用記憶體大小

jmap -histo 17296 > ./test.txt

JVM調優JVM調優

num:序号 instances:執行個體數量 bytes:占用空間大小

class name:類名稱,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]

堆資訊

jmap -heap 18564

E:\gitee\vastmoon\sparrow>jmap -heap 18564
Attaching to process ID 18564, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.231-b11

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4263510016 (4066.0MB)
   NewSize                  = 89128960 (85.0MB)
   MaxNewSize               = 1420820480 (1355.0MB)
   OldSize                  = 179306496 (171.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 246939648 (235.5MB)
   used     = 139866816 (133.38739013671875MB)
   free     = 107072832 (102.11260986328125MB)
   56.64008073746019% used
From Space:
   capacity = 12582912 (12.0MB)
   used     = 0 (0.0MB)
   free     = 12582912 (12.0MB)
   0.0% used
To Space:
   capacity = 14680064 (14.0MB)
   used     = 0 (0.0MB)
   free     = 14680064 (14.0MB)
   0.0% used
PS Old Generation
   capacity = 117440512 (112.0MB)
   used     = 18069168 (17.232101440429688MB)
   free     = 99371344 (94.76789855957031MB)
   15.385804857526507% used

18745 interned Strings occupying 1746872 bytes.

           

堆記憶體dump

jmap -dump:format=b,file=bootstrapApplication.hprof 18564

也可以設定記憶體溢出自動導出dump檔案(記憶體很大的時候,可能會導不出來)

  • -XX:+HeapDumpOnOutOfMemoryError
  • -XX:HeapDumpPath=./ (路徑)

Jstack

用jstack加程序id查找死鎖

Jinfo

檢視正在運作的Java應用程式的擴充參數

jinfo -flags 18564

檢視jvm的參數

jinfo -sysprops 18564

檢視java系統參數

Jstat

jstat指令可以檢視堆記憶體各部分的使用量,以及加載類的數量。指令的格式如下:

jstat [-指令選項] [vmid] [間隔時間(毫秒)] [查詢次數]

垃圾回收統計

jstat -gc pid

最常用,可以評估程式記憶體使用及GC壓力整體情況

JVM調優JVM調優

S0C:第一個幸存區的大小,機關KB, S1C:第二個幸存區的大小 S0U:第一個幸存區的使用大小S1U:第二個幸存區的使用大小 EC:伊甸園區的大小 ,EU:伊甸園區的使用大小 OC:老年代大小 OU:老年代使用大小

MC:方法區大小(元空間) MU:方法區使用大小 CCSC:壓縮類空間大小 CCSU:壓縮類空間使用大小

YGC:年輕代垃圾回收次數 YGCT:年輕代垃圾回收消耗時間,機關s FGC:老年代垃圾回收次數 FGCT:老年代垃圾回收消耗時間,機關s GCT:垃圾回收消耗總時間,機關s

jstat -gccapacity 18564

堆記憶體統計

JVM調優JVM調優

NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:目前新生代容量 S0C:第一個幸存區大小

S1C:第二個幸存區的大小 EC:伊甸園區的大小 OGCMN:老年代最小容量 OGCMX:老年代最大容量

OGC:目前老年代大小 OC:目前老年代大小 MCMN:最小中繼資料容量 MCMX:最大中繼資料容量 MC:目前中繼資料空間大小 CCSMN:最小壓縮類空間大小 CCSMX:最大壓縮類空間大小 CCSC:目前壓縮類空間大小 YGC:年輕代gc次數 FGC:老年代GC次數

jstat -gcnew 18564

新生代垃圾回收統計

JVM調優JVM調優

S0C:第一個幸存區的大小 S1C:第二個幸存區的大小 S0U:第一個幸存區的使用大小 S1U:第二個幸存區的使用大小 TT:對象在新生代存活的次數 MTT:對象在新生代存活的最大次數 DSS:期望的幸存區大小 EC:伊甸園區的大小 EU:伊甸園區的使用大小 YGC:年輕代垃圾回收次數 YGCT:年輕代垃圾回收消耗時間

jstat -gcnewcapacity 18564

新生代記憶體統計

JVM調優JVM調優

NGCMN:新生代最小容量 NGCMX:新生代最大容量 NGC:目前新生代容量 S0CMX:最大幸存1區大小

S0C:目前幸存1區大小 S1CMX:最大幸存2區大小 S1C:目前幸存2區大小 ECMX:最大伊甸園區大小

EC:目前伊甸園區大小 YGC:年輕代垃圾回收次數 FGC:老年代回收次數

jstat -gcold 18564

老年代垃圾回收統計

JVM調優JVM調優

MC:方法區大小 MU:方法區使用大小 CCSC:壓縮類空間大小 CCSU:壓縮類空間使用大小 OC:老年代大小

OU:老年代使用大小 YGC:年輕代垃圾回收次數 FGC:老年代垃圾回收次數 FGCT:老年代垃圾回收消耗時間

GCT:垃圾回收消耗總時間

jstat -gcoldcapacity 18564

老年代記憶體統計

JVM調優JVM調優

OGCMN:老年代最小容量 OGCMX:老年代最大容量 OGC:目前老年代大小 OC:老年代大小 YGC:年輕代垃圾回收次數 FGC:老年代垃圾回收次數 FGCT:老年代垃圾回收消耗時間 GCT:垃圾回收消耗總時間

jstat -gcmetacapacity 18564

中繼資料空間統計

JVM調優JVM調優

MCMN:最小中繼資料容量 MCMX:最大中繼資料容量 MC:目前中繼資料空間大小 CCSMN:最小壓縮類空間大小

CCSMX:最大壓縮類空間大小 CCSC:目前壓縮類空間大小 YGC:年輕代垃圾回收次數 FGC:老年代垃圾回收次數 FGCT:老年代垃圾回收消耗時間 GCT:垃圾回收消耗總時間

jstat -gcutil 18564

輔助資訊

JVM調優JVM調優

S0:幸存1區目前使用比例 S1:幸存2區目前使用比例 E:伊甸園區使用比例 O:老年代使用比例 M:中繼資料區使用比例 CCS:壓縮使用比例 YGC:年輕代垃圾回收次數 FGC:老年代垃圾回收次數 FGCT:老年代垃圾回收消耗時間 GCT:垃圾回收消耗總時間

JVM運作情況預估

用 jstat gc -pid 指令可以計算出如下一些關鍵資料,有了這些資料就可以采用之前介紹過的優化思路,先給自己的系統設定一些初始性的JVM參數,比如堆記憶體大小,年輕代大小,Eden和Survivor的比例,老年代的大小,大對象的門檻值,大齡對象進入老年代的門檻值等。

年輕代對象增長的速率

可以執行指令 jstat -gc pid 1000 10 (每隔1秒執行1次指令,共執行10次),通過觀察EU(eden區的使用)來估算每秒eden大概新增多少對象,如果系統負載不高,可以把頻率1秒換成1分鐘,甚至10分鐘來觀察整體情況。注意,一般系統可能有高峰期和日常期,是以需要在不同的時間分别估算不同情況下對象增長速率。

Young GC的觸發頻率和每次耗時

知道年輕代對象增長速率我們就能推根據eden區的大小推算出Young GC大概多久觸發一次,Young GC的平均耗時可以通過 YGCT/YGC 公式算出,根據結果我們大概就能知道系統大概多久會因為Young GC的執行而卡頓多久。

每次Young GC後有多少對象存活和進入老年代

這個因為之前已經大概知道Young GC的頻率,假設是每5分鐘一次,那麼可以執行指令 jstat -gc pid 300000 10 ,觀察每次結果eden,survivor和老年代使用的變化情況,在每次gc後eden區使用一般會大幅減少,survivor和老年代都有可能增長,這些增長的對象就是每次Young GC後存活的對象,同時還可以看出每次Young GC後進去老年代大概多少對象,進而可以推算出老年代對象增長速率。

Full GC的觸發頻率和每次耗時

知道了老年代對象的增長速率就可以推算出Full GC的觸發頻率了,Full GC的每次耗時可以用公式 FGCT/FGC 計算得出。

優化思路

其實簡單來說就是盡量讓每次Young GC後的存活對象小于Survivor區域的50%,都留存在年輕代裡。盡量别讓對象進入老年代。盡量減少Full GC的頻率,避免頻繁Full GC對JVM性能的影響。