重新認知JVM:
通過前面從Class文件到類裝載器,再到運行時數據區的過程。我們畫張圖展示了JVM的大體物理結構圖。
GC優化:
內存被使用了之后,難免會有不夠用或者達到設定值的時候,就需要對內存空間進行垃圾回收。
GC是由JVM自動完成的,根據JVM系統環境而定,所以時機是不確定的。 當然,我們可以手動進行垃圾回收,比如調用System.gc()方法通知JVM進行一次垃圾回收,但是具體什么時刻運行也無法控制。也就是說System.gc()只是通知要回收,什么時候回收由JVM決定。 但是不建議手動調用該方法,因為消耗的資源比較大。一般以下幾種情況會發生垃圾回收
- 當Eden區或者S區不夠用了
- 老年代空間不夠用了
- 方法區空間不夠用了
- System.gc()
GC日志文件:
首先回顧一下各個垃圾收集器對應的代。
ParallelGC Log:
要想分析日志的信息,得先拿到GC日志文件才行,所以得先配置一下,之前也看過這些參數。然后啟動項目
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:gc.log
到項目根目錄下找到 gc.log 文件打開:可以看到默認使用的是ParallelGC
注意 如果回收的差值中間有出入,說明這部分空間是Old區釋放出來的
CMS GC Log:
停頓時間優先,參數設置:-XX:+UseConcMarkSweepGC -Xloggc:cms-gc.log
G1日志:
停頓時間優先,參數設置:-XX:+UseG1GC -Xloggc:g1-gc.log
理解G1日志格式可以參考:https://blogs.oracle.com/poonam/understanding-g1-gc-logs
-XX:+UseG1GC # 使用了G1垃圾收集器 # 什么時候發生的GC,相對的時間刻,GC發生的區域young,總共花費的時間,0.00478s, # It is a stop-the-world activity and all # the application threads are stopped at a safepoint during this time. 2019-12-18T16:06:46.508+0800: 0.458: [GC pause (G1 Evacuation Pause) (young), 0.0047804 secs] # 多少個垃圾回收線程,並行的時間 [Parallel Time: 3.0 ms, GC Workers: 4] # GC線程開始相對於上面的0.458的時間刻 [GC Worker Start (ms): Min: 458.5, Avg: 458.5, Max: 458.5, Diff: 0.0] # This gives us the time spent by each worker thread scanning the roots # (globals, registers, thread stacks and VM data structures). [Ext Root Scanning (ms): Min: 0.2, Avg: 0.4, Max: 0.7, Diff: 0.5, Sum: 1.7] # Update RS gives us the time each thread spent in updating the Remembered Sets. [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] ...
常見GC日志文件分析工具:
gceasy:
GC Easy是一款在線的可視化工具,易用、功能強大。官網 :https://gceasy.io 。可以比較不同的垃圾收集器的吞吐量和停頓時間,比如打開cms-gc.log和g1-gc.log
GCViewer:
需要下載 gcviewer-1.36-SNAPSHOT.jar ,然后運行這個jar。
G1調優與最佳指南:
調優,是否選用G1垃圾收集器的判斷依據,https://docs.oracle.com/javase/8/docs/technotes/guides/vm/G1.html#use_cases
- 50%以上的堆被存活對象占用
- 對象分配和晉升的速度變化非常大
- 垃圾回收時間比較長
使用G1GC垃圾收集器: -XX:+UseG1GC 。修改配置參數,獲取到gc日志,使用GCViewer分析吞吐量和響應時間
Throughput Min Pause Max Pause Avg Pause GC count 99.16% 0.00016s 0.0137s 0.00559s 12
調整內存大小再獲取gc日志分析
-XX:MetaspaceSize=100M -Xms300M -Xmx300M
比如設置堆內存的大小,獲取到gc日志,使用GCViewer分析吞吐量和響應時間
Throughput Min Pause Max Pause Avg Pause GC count 98.89% 0.00021s 0.01531s 0.00538s 12
啟動並發GC時堆內存占用百分比: -XX:InitiatingHeapOccupancyPercent=45 G1用它來觸發並發GC周期,基於整個堆的使用率,而不只是某一代內存的使用比例。值為 0 則表示“一直執行GC循環)'. 默認值為 45 (例如, 全部的 45% 或者使用了45%).比如設置該百分比參數,獲取到gc日志,使用GCViewer分析吞吐量和響應時間
Throughput Min Pause Max Pause Avg Pause GC count 98.11% 0.00406s 0.00532s 0.00469s 12
最佳指南:
官網建議 :https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations
不要手動設置新生代和老年代的大小,只要設置整個堆的大小:G1收集器在運行過程中,會自己調整新生代和老年代的大小其實是通過adapt代的大小來調整對象晉升的速度和年齡,從而達到為收集器設置的暫停時間目標如果手動設置了大小就意味着放棄了G1的自動調優不斷調優暫停時間目標一般情況下這個值設置到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設置成50ms就不太合理。暫停時間設置的太短,就會導致出現G1跟不上垃圾產生的速度。最終退化成Full GC。所以對這個參數的調優是一個持續的過程,逐步調整到最佳狀態。暫停時間只是一個目標,並不能總是得到滿足。
使用-XX:ConcGCThreads=n來增加標記線程的數量,IHOP(InitiatingHeapOccupancyPercent)如果閥值設置過高,可能會遇到轉移失敗的風險,比如對象進行轉移時空間不足。如果閥值設置過低,就會使標記周期運行過於頻繁,並且有可能混合收集期回收不到空間。IHOP值如果設置合理,但是在並發周期時間過長時,可以嘗試增加並發線程數,調高ConcGCThreads。
MixedGC 混合回收調優:mixed GC就是把一部分老年區的region加到Eden和Survivor from的后面,合起來稱為collection set, 就是將被回收的集合,下次mixed GC evacuation把他們所有都一並清理。
-XX:InitiatingHeapOccupancyPercent //設置觸發標記周期的Java堆占用閾值,默認值為45。 -XX:G1MixedGCLiveThresholdPercent //設置在標記周期完成之后混合收集的數量,以維持old region
(也就是老年代)中,最多有G1MixedGCLiveThresholdPercent的存活對象。默認值為8 -XX:G1MixedGCCountTarger //一個混合收集周期中包含多少次混合收集 -XX:G1OldCSetRegionThresholdPercent //一次混合收集中被收集的old region
數量的上線,默認值是整個堆的10%
適當增加堆內存大小
高並發場景分析:
如下圖,當一個訂單服務以每秒3000單的速度請求,然后請求的訂單服務的集群數為3。假設每次請求產生的訂單的實例在內存中所占大小為1KB,具體的計算可以根據對象頭內的內容進行計算,此時訂單服務可能還會調用其他的服務,此過程也會產生對象,那么擴大20倍,此時每個節點每秒點單服務會產生20MB的內存占用,假設堆內存為4000MB,那么Young為1333MB. 65秒后這個Young區便會滿觸發Minor GC。對象晉升,步入老年代,這樣的速率會導致老年代的內存很快就滿了,就會觸發Major GC,Metaspace 區域也會頻繁的GC,這樣子會觸發Full GC。那么這個時候我們可以適當的增加 Young區大小,調整Young區跟Old區的比例。
JVM性能優化指南:
常見問題思考:
-
內存泄漏與內存溢出的區別?:內存泄漏:對象無法得到及時的回收,持續占用內存空間,從而造成內存空間的浪費。內存溢出:內存泄漏到一定的程度就會導致內存溢出,但是內存溢出也有可能是大對象導致的。
-
young gc會有stw嗎?不管什么 GC,都會有 stop-the-world,只是發生時間的長短。
-
major gc和full gc的區別?:major gc指的是老年代的gc,而full gc等於young+old+metaspace的gc。
-
G1與CMS的區別是什么?:CMS 用於老年代的回收,而 G1 用於新生代和老年代的回收。G1 使用了 Region 方式對堆內存進行了划分,且基於標記整理算法實現,整體減少了垃圾碎片的產生。
-
什么是直接內存?:直接內存是在java堆外的、直接向系統申請的內存空間。通常訪問直接內存的速度會優於Java堆。因此出於性能的考慮,讀寫頻繁的場合可能會考慮使用直接內存。
-
不可達的對象一定要被回收嗎?:即使在可達性分析法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑階段”,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆蓋 finalize 方法,或 finalize 方法已經被虛擬機調用過時,虛擬機將這兩種情況視為沒有必要執行。被判定為需要執行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象建立關聯,否則就會被真的回收。
-
方法區中的無用類回收:方法區主要回收的是無用的類,那么如何判斷一個類是無用的類的呢?判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面 3 個條件才能算是 “無用的類” :該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。加載該類的 ClassLoader 已經被回收。該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。虛擬機可以對滿足上述 3 個條件的無用類進行回收,這里說的僅僅是“可以”,而並不是和對象一樣不使用了就會必然被回收。
-
不同的引用:JDK1.2以后,Java對引用進行了擴充:強引用、軟引用、弱引用和虛引用。