實戰(zhàn)性能優(yōu)化
重新認(rèn)知JVM
之前我們畫過一張圖,是從Class文件到類裝載器,再到運行時數(shù)據(jù)區(qū)的過程细移,現(xiàn)在咱們把這張圖不妨豐富完善一下欢唾,展示了JVM的大體物理結(jié)構(gòu)圖且警。
執(zhí)行引擎:用于執(zhí)行JVM字節(jié)碼指令
主要由兩種實現(xiàn)方式:
(1)將輸入的字節(jié)碼指令在加載時或執(zhí)行時翻譯成另外一種虛擬機指令;
(2)將輸入的字節(jié)碼指令在加載時或執(zhí)行時翻譯成宿主主機本地CPU的指令集礁遣。這兩種方式對應(yīng)著字節(jié)碼的解釋執(zhí)行和即時編譯振湾。
9.2 堆內(nèi)存溢出
9.2.1 代碼
記得設(shè)置參數(shù)比如-Xmx20M -Xms20M
9.2.2 運行結(jié)果
訪問->http://localhost:8080/heap
Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit exceeded
9.2.3 回顧jps和jinfo
9.2.4 回顧jmap手動導(dǎo)出和參數(shù)自動導(dǎo)出
jmap手動導(dǎo)出:jmap -dump:format=b,file=heap.hprof PID
參數(shù)自動導(dǎo)出:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump.hprof
9.3 方法區(qū)內(nèi)存溢出
比如向方法區(qū)中添加Class的信息
9.3.1 asm依賴和Class代碼
9.3.2 代碼
設(shè)置Metaspace的大小,比如-XX:MetaspaceSize=50M -XX:MaxMetaspaceSize=50M
9.3.3 運行結(jié)果
訪問->http://localhost:8080/nonheap
9.4 虛擬機棧
9.4.1 代碼演示StackOverFlow
9.4.2 運行結(jié)果
9.4.3 說明
Stack Space用來做方法的遞歸調(diào)用時壓入Stack Frame(棧幀)亡脸。所以當(dāng)遞歸調(diào)用太深的時候,就有可能耗盡Stack Space树酪,爆出StackOverflow的錯誤浅碾。
-Xss128k:設(shè)置每個線程的堆棧大小。JDK 5以后每個線程堆棧大小為1M续语,以前每個線程堆棧大小為256K垂谢。根據(jù)應(yīng)用的線程所需內(nèi)存大小進行調(diào)整。在相同物理內(nèi)存下疮茄,減小這個值能生成更多的線程滥朱。但是操作系統(tǒng)對一個進程內(nèi)的線程數(shù)還是有限制的,不能無限生成力试,經(jīng)驗值在3000~5000左右徙邻。
線程棧的大小是個雙刃劍,如果設(shè)置過小畸裳,可能會出現(xiàn)棧溢出缰犁,特別是在該線程內(nèi)有遞歸、大的循環(huán)時出現(xiàn)溢出的可能性更大怖糊,如果該值設(shè)置過大帅容,就有影響到創(chuàng)建棧的數(shù)量,如果是多線程的應(yīng)用伍伤,就會出現(xiàn)內(nèi)存溢出的錯誤并徘。
9.5 線程死鎖
9.5.1 代碼
?9.4.2 運行結(jié)果
9.4.3 jstack分析
把打印信息拉到最后可以發(fā)現(xiàn)
9.4.4 jvisualvm
將線程信息dump出來
9.6 垃圾收集
內(nèi)存被使用了之后,難免會有不夠用或者達(dá)到設(shè)定值的時候扰魂,就需要對內(nèi)存空間進行垃圾回收麦乞。
9.6.1 垃圾收集發(fā)生的時機
GC是由JVM自動完成的,根據(jù)JVM系統(tǒng)環(huán)境而定阅爽,所以時機是不確定的路幸。
當(dāng)然,我們可以手動進行垃圾回收付翁,比如調(diào)用System.gc()方法通知JVM進行一次垃圾回收简肴,但是具體什么時刻運行也無法控制。也就是說System.gc()只是通知要回收百侧,什么時候回收由JVM決定砰识。
但是不建議手動調(diào)用該方法能扒,因為消耗的資源比較大。
一般以下幾種情況會發(fā)生垃圾回收
(1)當(dāng)Eden區(qū)或者S區(qū)不夠用了
(2)老年代空間不夠用了
(3)方法區(qū)空間不夠用了
(4)System.gc()
雖然垃圾回收的時機是不確定的辫狼,但是可以結(jié)合之前一個對象的一輩子案例初斑,文字圖解再次梳理一下堆內(nèi)存回收的流程。
一個對象的一輩子
我是一個普通的Java對象,我出生在Eden區(qū),在Eden區(qū)我還看到和我長的很像的小兄弟,我們在Eden區(qū)中玩了挺長時間膨处。
有一天Eden區(qū)中的人實在是太多了,我就被迫去了Survivor區(qū)的“From”區(qū),自從去了Survivor區(qū),我就開始漂了,有時候在Survivor的“From”區(qū),有時候在Survivor的“To”區(qū),居無定所见秤。直到我18歲的時候,爸爸說我成人了,該去社會上闖闖了。
于是我就去了年老代那邊,年老代里,人很多,并且年齡都挺大的,我在這里也認(rèn)識了很多人真椿。在年老代里,我生活了20年(每次GC加一歲),然后被回收鹃答。
9.6.2 實驗環(huán)境準(zhǔn)備
我的本地機器使用的是jdk1.8和tomcat8.5,大家也可以使用linux上的tomcat突硝,然后把gc日志下載下來即可测摔。
9.6.3 GC日志文件
回顧升華一下垃圾收集器圖
要想分析日志的信息,得先拿到GC日志文件才行解恰,所以得先配置一下锋八,之前也看過這些參數(shù)。
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log
比如打開windows中的catalina.bat护盈,在第一行加上
set JAVA_OPTS=%JAVA_OPTS% -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:gc.log
這樣使用startup.bat啟動tomcat的時候就能夠在當(dāng)前目錄下拿到gc.log文件
可以看到默認(rèn)使用的是ParallelGC
9.6.3.1 Parallel GC日志
【吞吐量優(yōu)先】
2019-06-10T23:21:53.305+0800: 1.303: [GC (Allocation Failure) [PSYoungGen: 65536K[Young區(qū)回收前]->10748K[Young區(qū)回收后](76288K[Young區(qū)總大小])] 65536K[整個堆回收前]->15039K[整個堆回收后](251392K[整個堆總大小]), 0.0113277 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
`注意`如果回收的差值中間有出入挟纱,說明這部分空間是Old區(qū)釋放出來的
9.6.3.2 CMS日志
【停頓時間優(yōu)先】
參數(shù)設(shè)置
-XX:+UseConcMarkSweepGC
重啟tomcat獲取gc日志,這里的日志格式和上面差不多黄琼,不作分析樊销。
9.6.3.3 G1日志
G1日志格式參考鏈接:
https://blogs.oracle.com/poonam/understanding-g1-gc-logs
【停頓時間優(yōu)先】
why?
https://blogs.oracle.com/poonam/increased-heap-usage-with-g1-gc
參數(shù)設(shè)置
-XX:+UseG1GC
9.6.4 GC日志文件分析工具
9.6.4.1 gceasy
可以比較不同的垃圾收集器的吞吐量和停頓時間
9.6.4.2 GCViewer
9.6.5 G1調(diào)優(yōu)
是否選用G1垃圾收集器的判斷依據(jù)
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases
(1)50%以上的堆被存活對象占用
(2)對象分配和晉升的速度變化非常大
(3)垃圾回收時間比較長
(1)使用G1GC垃圾收集器: -XX:+UseG1GC
修改配置參數(shù),獲取到gc日志脏款,使用GCViewer分析吞吐量和響應(yīng)時間
Throughput? ? ?MinPause? ? ?MaxPause? ? ? AvgPause? ?GCcount
? 99.16%? ? ? ? ? 0.00016s? ? ? ? 0.0137s? ? ? ? ?0.00559s? ? ? ? ?12
(2)調(diào)整內(nèi)存大小再獲取gc日志分析
-XX:MetaspaceSize=100M-Xms300M-Xmx300M
比如設(shè)置堆內(nèi)存的大小围苫,獲取到gc日志,使用GCViewer分析吞吐量和響應(yīng)時間
Throughput? ? ?MinPause? ? ?MaxPause? ? ? AvgPause? ?GCcount
98.89%? ? ? ? ? 0.00021s? ? ? ? 0.01531s? ? ? 0.00538s? ? ? ? ? 12
(3)調(diào)整最大停頓時間
-XX:MaxGCPauseMillis=200 設(shè)置最大GC停頓時間指標(biāo)
比如設(shè)置最大停頓時間撤师,獲取到gc日志剂府,使用GCViewer分析吞吐量和響應(yīng)時間
Throughput? ? ?MinPause? ? ?MaxPause? ? ? AvgPause? ?GCcount
?98.96%? ? ? ? ? ? 0.00015s? ? ? ? 0.01737s? ? ? 0.00574s? ? ? ? ? 12
(4)啟動并發(fā)GC時堆內(nèi)存占用百分比
-XX:InitiatingHeapOccupancyPercent=45 G1用它來觸發(fā)并發(fā)GC周期,基于整個堆的使用率,而不只是某一代內(nèi)存的使用比例。值為 0 則表示“一直執(zhí)行GC循環(huán))'. 默認(rèn)值為 45 (例如, 全部的 45% 或者使用了45%).
比如設(shè)置該百分比參數(shù)剃盾,獲取到gc日志腺占,使用GCViewer分析吞吐量和響應(yīng)時間
Throughput? ? ?MinPause? ? ?MaxPause? ? ? AvgPause? ?GCcount
? 98.11%? ? ? ? ? 0.00406s? ? ? ? 0.00532s? ? ? 0.00469s? ? ? ? ? 12
9.6.6 G1調(diào)優(yōu)的最佳實踐
官網(wǎng)建議:
(https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations)
(1)不要手動設(shè)置新生代和老年代的大小,只要設(shè)置整個堆的大小
G1收集器在運行過程中痒谴,會自己調(diào)整新生代和老年代的大小
其實是通過adapt代的大小來調(diào)整對象晉升的速度和年齡衰伯,從而達(dá)到為收集器設(shè)置的暫停時間目標(biāo)
如果手動設(shè)置了大小就意味著放棄了G1的自動調(diào)優(yōu)
(2)不斷調(diào)優(yōu)暫停時間目標(biāo)
一般情況下這個值設(shè)置到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設(shè)置成50ms就不太合理积蔚。暫停時間設(shè)置的太短意鲸,就會導(dǎo)致出現(xiàn)G1跟不上垃圾產(chǎn)生的速度。最終退化成Full GC。所以對這個參數(shù)的調(diào)優(yōu)是一個持續(xù)的過程怎顾,逐步調(diào)整到最佳狀態(tài)读慎。暫停時間只是一個目標(biāo),并不能總是得到滿足槐雾。
(3)使用-XX:ConcGCThreads=n來增加標(biāo)記線程的數(shù)量
IHOP如果閥值設(shè)置過高夭委,可能會遇到轉(zhuǎn)移失敗的風(fēng)險,比如對象進行轉(zhuǎn)移時空間不足募强。如果閥值設(shè)置過低株灸,就會使標(biāo)記周期運行過于頻繁,并且有可能混合收集期回收不到空間擎值。
> IHOP值如果設(shè)置合理蚂且,但是在并發(fā)周期時間過長時,可以嘗試增加并發(fā)線程數(shù)幅恋,調(diào)高ConcGCThreads。
(4)MixedGC調(diào)優(yōu)
-XX:InitiatingHeapOccupancyPercent
-XX:G1MixedGCLiveThresholdPercent
-XX:G1MixedGCCountTarger
-XX:G1OldCSetRegionThresholdPercent
(5)適當(dāng)增加堆內(nèi)存大小
9.7 一張圖總結(jié)JVM性能優(yōu)化
全文完泵肄!thanks for watching
歡迎關(guān)注“Java架構(gòu)師學(xué)習(xí)”