背景:
某一天系統出現了請求超時,然后通過日志查看,程序執行到某一個位置,直接停下來來了,或者說所有的線程的執行都停下來了。而且是該時間段,請求處理變慢。排查相關的服務,并沒有出現死鎖,異常,內存不足的情況,并且不是特定的接口超時,而且超時的時間也比較散亂。于是有同事提出來有沒有可能系統正在做full GC。于是我們開始朝著這個方向排查。
確定請求超時的原因
第一步:獲取GClog:通過GClog于請求超時的時間段對比,發現請求超時的時間,系統真正進行fullGC。??STW 機制的本質??Full GC 需掃描并清理??整個堆內存??(包括新生代、老年代、元空間),為確保垃圾回收過程中對象引用關系的一致性,JVM ??必須暫停所有應用線程??。這種全局暫停稱為 STW(Stop-The-World)。
??表現??:所有用戶請求卡頓、任務隊列堆積、監控指標顯示線程狀態為 BLOCKED或 WAITING。
??耗時??:通常持續 ??百毫秒至數秒??,堆越大、存活對象越多,暫停時間越長(例如 10GB 堆可能暫停 5 秒以上)。
而且每一次的full GC都是長達:5-6 s 也就線程暫停了5-6s。并且每次超時都是在full GC發生的時候,所以基本上可以確認就是full GC導致的。 GC導致的系統卡頓的特點:1. 無特定服務 2.通過系統執行日志發現線程執行情況 3.通過GC日志判斷
確定full GC的原因
1.確定了GC導致的系統吞吐量降低,延遲抬升,接下來需要解決full GC的問題。
導致full GC的原因一般有哪些?
1.首先建議你們自習看GClog,因為好的GClog他會直接告訴你full GC的原因。
2.其次你也需要知道哪些會導致full GC,
第一:內存空間不足
1.老年代空間不足、元空間溢出、新生代晉升壓力
第二: 顯示觸發
System.gc()
其實GClog已經給了原因:reason:sys,其實就告訴是system.gc導致的,其實也有遇到內存空間不足的情況,給的原因:reason:af(allocation fail)分配失敗,也就是分配老年代空間不足。
也可以通過gc log 里面的老年代的空間進行分析,會發現在system.gc 前,heap size 在gc前有很多的空間,而且老年代也夠的。那么其實就可以判斷不是第一個原因,而是第二個原因。
但是System.gc()是誰發起的呢?首先全局搜索代碼,我們的業務代碼并沒有直接調用System.gc(),那么會是什么觸發System.gc()呢。以及我們能不能直接禁用System.gc()。
System.gc()可以直接禁用嘛
首先不建議禁用System.gc(),因為有可能你使用的第三方的框架,或者你以來的組建需要通過System.gc(),清理對內內存。
比如: ??
堆外內存回收依賴??
??DirectByteBuffer 等堆外內存??:其清理依賴于關聯的 Cleaner對象被 GC 回收。若禁用 System.gc(),堆外內存可能無法及時釋放,導致 OutOfMemoryError(即使堆內存充足)。??典型案例??:Netty 等NIO 框架需定期觸發 Full GC 釋放堆外內存。禁用后可能需等待 JVM 自動觸發 Full GC,延遲釋放可能引發內存溢出。
System.gc()原因排查
那么如果不禁用,我們應該怎么做?到了這里我們的解決思路是什么?這里提到了直接內存,那有沒有可能就是直接內存不夠導致的fullGC呢?如果是這個懷疑那怎么證明?直接內存不足?那直接內存使用了多少?于是我們想到,不如打印直接內存看看,看看使用情況,以及我們可以結合GClog再進一步觀察一下。其實如果對直接內存熟悉的同學,不一定需要打印,GClog里面會有一個虛引用的數量,虛引用和直接內存又是什么關系呢?
虛引用是管理直接內存的“監控觸發器”?
??虛引用的作用??虛引用是 Java 中最弱的引用類型(PhantomReference),??無法通過 get()獲取對象實例??(始終返回 null),其主要功能是??跟蹤對象被垃圾回收的時機??。虛引用必須與 ReferenceQueue關聯,當對象被 GC 回收時,虛引用會被加入隊列,從而觸發后續清理操作。 人話:目標對象被GC回收,目標對象的虛引用會加入隊列,觸發后續動作。
直接內存的特殊性??直接內存(如 DirectByteBuffer分配的內存)位于 JVM 堆外,由操作系統管理。??其生命周期不受 JVM 垃圾回收器直接控制??,但堆內的 DirectByteBuffer對象本身是受 GC 管理的。當該對象被回收時,其關聯的直接內存需要手動釋放(通過 Cleaner機制),否則會導致??堆外內存泄漏。
??監控對象回收??:虛引用綁定到 DirectByteBuffer對象上,當該對象被 GC 回收時,虛引用被加入 ReferenceQueue。
??觸發資源釋放??:通過輪詢 ReferenceQueue,程序可執行堆外內存的釋放(如調用 Unsafe.freeMemory())。
??防止內存泄漏??:此機制確保堆外內存不會因對象回收而遺留未釋放的資源。
堆內:DirectByteBuffer對象 – 虛引用 – 堆外內存。所以可以理解是一種橋梁。
DBBs use a PhantomReference which is essentially a more flexible finalizer and they allow the native memory of the DBB to be freed once there are no longer any live Java references. Finalizers and their ilk are generally not recommended because their cleanup time by the garbage collector is non-deterministic.
所以聊到這里我們也會發現,其實通過GC,清理DirectByteBuffer對象,虛引用被加入 ReferenceQueue,進而清理堆外內存的空間。所以達到了JVM對堆外內存的控制。 所以你可以通過判斷虛引用的數量,判斷DirectByteBuffer對象數量,雖然只是數量,但你也可以間接判斷堆外內存的使用情況。
import java.lang.management.ManagementFactory;
import java.lang.management.BufferPoolMXBean;
import java.util.List;public class DirectMemoryMonitor {public static void main(String[] args) {List<BufferPoolMXBean> pools = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class);for (BufferPoolMXBean pool : pools) {if ("direct".equals(pool.getName())) {System.out.println("直接內存使用: " + formatBytes(pool.getMemoryUsed()) + " / " + formatBytes(pool.getTotalCapacity()));}}}private static String formatBytes(long bytes) {if (bytes < 1024) return bytes + " B";int exp = (int) (Math.log(bytes) / Math.log(1024));char unit = "KMGTPE".charAt(exp-1);return String.format("%.1f %sB", bytes / Math.pow(1024, exp), unit);}
}
打印成功后發現:當直接內存達到64M,96M,128M的時候系統就會發生full GC。所以到這里我們通過直接內存的使用情況初步懷疑就是直接內存不足導致的。我們使用的是IBM JDK 1.8所以我們也咨詢了IBM的同事,他們給我們丟了一個鏈接:
https://publib.boulder.ibm.com/httpserv/cookbook/WebSphere_Application_Server-WAS_traditional-HTTP.html
https://publib.boulder.ibm.com/httpserv/cookbook/Troubleshooting-Troubleshooting_Java.html#Troubleshooting-Troubleshooting_Java-Excessive_Direct_Byte_Buffers
There are two main types of problems with Direct Byte Buffers:
Excessive native memory usage
Excessive performance overhead due to System.gc calls by the DBB code
This section primarily discusses issue 1. For issue 2, note that IBM Java starts with a soft limit of 64MB and increases by 32MB chunks with a System.gc each time, so consider setting -XX:MaxDirectMemorySize=$BYTES (e.g. -XX:MaxDirectMemorySize=1024m) to avoid this upfront cost (although read on for how to size this).
This type of problem is particularly bad with generational collectors because the whole purpose of a generational collector is to minimize the collection of the tenured space (ideally never needing to collect it). If a DBB is tenured, because the size of the Java object is very small, it puts little pressure on the tenured heap. Even if the DBB is ready to be garbage collected, the PhantomReference can only become ready during a tenured collection. Here is a description of this problem (which also talks about native classloader objects, but the principle is the same):(人話:如果DBB已經進入了老年代,除非full GC 回收老年代空間,否則不會回收DBB,從而導致DBB泄漏)
In most cases, something like -XX:MaxDirectMemorySize=1024m (and ensuring -Xdisableexplicitgc is not set) is a reasonable solution to the problem.
A system dump or HPROF dump may be loaded in the IBM Memory Analyzer Tool & the IBM Extensions for Memory Analyzer DirectByteBuffer plugin may be run to show how much of the DBB native memory is available for garbage collection. For example:
規律基本上對上了,至此可以得出結論:堆外內存不足,導致的顯示GC的發生。
所以至此我們需要解決的就是DBB的問題:
這里的解決方案:
1.通過設置DBB的大小,給一個1G,如果不是DBB的泄漏問題,可以通過minor GC或者major GC清理空間,從而清理直接內存,而由于給了1G就不會因為達到了64M,96M等進行full GC了。總的來說就是:不那么容易滿,加強一下GC清理多一些DBB,趕上DBB增加的速度。
加強一下GC清理多一些,如何清理多一些呢?
2.打印分配的DBB的tracelog看看誰在分配,其實我們也打印了發現IBM WAS底層很多地方都在使用DBB。所以很難阻止。
IBM WAS也給我們一個參數減少在HTTP請求中使用DBB,但是代價就是處理會變慢一些。