一文學(xué)會JVM性能優(yōu)化

實戰(zhàn)性能優(yōu)化

重新認知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(棧幀)。所以當遞歸調(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)存被使用了之后,難免會有不夠用或者達到設(shè)定值的時候,就需要對內(nèi)存空間進行垃圾回收。

9.6.1 垃圾收集發(fā)生的時機

GC是由JVM自動完成的,根據(jù)JVM系統(tǒng)環(huán)境而定,所以時機是不確定的。

當然,我們可以手動進行垃圾回收,比如調(diào)用System.gc()方法通知JVM進行一次垃圾回收,但是具體什么時刻運行也無法控制。也就是說System.gc()只是通知要回收,什么時候回收由JVM決定。

但是不建議手動調(diào)用該方法,因為消耗的資源比較大。

一般以下幾種情況會發(fā)生垃圾回收

(1)當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歲的時候,爸爸說我成人了,該去社會上闖闖了。

于是我就去了年老代那邊,年老代里,人很多,并且年齡都挺大的,我在這里也認識了很多人。在年老代里,我生活了20年(每次GC加一歲),然后被回收。

9.6.2 實驗環(huá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的時候就能夠在當前目錄下拿到gc.log文件

可以看到默認使用的是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停頓時間指標

比如設(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))'. 默認值為 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)整對象晉升的速度和年齡,從而達到為收集器設(shè)置的暫停時間目標

如果手動設(shè)置了大小就意味著放棄了G1的自動調(diào)優(yōu)

(2)不斷調(diào)優(yōu)暫停時間目標

一般情況下這個值設(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)。暫停時間只是一個目標,并不能總是得到滿足。

(3)使用-XX:ConcGCThreads=n來增加標記線程的數(shù)量

IHOP如果閥值設(shè)置過高,可能會遇到轉(zhuǎn)移失敗的風(fēng)險,比如對象進行轉(zhuǎn)移時空間不足。如果閥值設(shè)置過低,就會使標記周期運行過于頻繁,并且有可能混合收集期回收不到空間。

> IHOP值如果設(shè)置合理,但是在并發(fā)周期時間過長時,可以嘗試增加并發(fā)線程數(shù),調(diào)高ConcGCThreads。

(4)MixedGC調(diào)優(yōu)

-XX:InitiatingHeapOccupancyPercent

-XX:G1MixedGCLiveThresholdPercent

-XX:G1MixedGCCountTarger

-XX:G1OldCSetRegionThresholdPercent

(5)適當增加堆內(nèi)存大小

9.7 一張圖總結(jié)JVM性能優(yōu)化


全文完!thanks for watching

歡迎關(guān)注“Java架構(gòu)師學(xué)習(xí)”

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容