天天看點

為什麼JVM每隔一小時執行一次Full GC_

為什麼JVM每隔一小時執行一次Full GC_

随着接口自動化監控的完善,線上大多數的接口都被自動化監控系統接入,是以開發、測試人員可以及時的了解API的健康情況,監控内容包括檢查API的狀态碼是否正常、傳回的内容斷言是否成功等,異常的接口會對外發出報警資訊,從某個時間周期開始我們發現了報警内容的一些異常情況,每天都會收到一些接口狀态碼為502的報警。

為什麼JVM每隔一小時執行一次Full GC_
報警資訊由自動化監控系統發給釘釘報警群

根據報警的時間我們查了對應服務的日志,并沒有發現有任何的異常資訊,跟運維同學溝通後,在這個時間上運維那邊也并沒有對Tomcat、Nginx做相關操作,由于也沒有業務人員回報線上業務存在問題,于是502的問題暫時被擱置了起來,随着時間推移,502報警資訊每天還是不定期的如約而至,這讓報警群顯得有些“不安靜”,于是這個問題重新被重視起來,自動化監控的政策是每5分鐘跑一次接口list、不定期報出502說明在監控執行這段時間内服務可能會有問題,由于是5分鐘執行一次,是以每次502的周期、時間都不固定,這就解釋了為什麼報警時間、報警次數沒有任何規律。如果每天接口真的存在若幹次的502、那麼一定會影響到這個時間上的使用者體驗,而這樣的情況是不能夠被接受的。

Nginx 502錯誤的原因比較多,reload會造成502、代理伺服器無響應也會造成502,由于排除了運維對Nginx的操作,是以直接把目光放到了Tomcat上,由于Tomcat本身服務正常,并沒有報警,是以502的原因大機率是JVM的GC導緻的STW,于是我們找了JVM的監控資料。

為什麼JVM每隔一小時執行一次Full GC_
為什麼JVM每隔一小時執行一次Full GC_
監控資訊由OpenFalcon提供

當看到gc監控後第一反應:驚呆了!!!剛好每個小時Full GC一次,對比接口報警時間後發現,報警的時間剛好能夠跟gc時間對應上,Why???

從圖中可以看到每間隔一個小時就會執行一次FGC,FGC後old區使用記憶體降低,說明記憶體被回收了,但是在回收之前總使用記憶體隻有200多M,JVM設定的參數是3G,為什麼在200M是FGC了呢,Why???

JVM參數為 -Xms3072m -Xmx3072m -XX:PermSize=1536M -XX:MaxNewSize=1536m -MaxPermSize=1536m

帶着疑問我們先看下JVM在什麼情況下會執行FGC

  1. old區滿時
  2. young區存活對象複制到old區,old區記憶體不夠時
  3. 方法區滿時
  4. 調用System.gc()

FGC發生在old區使用了200M左右,排除1和2,方法區最大設定的1.5G,排除3,是以4是最大的嫌疑。

如果是因為調用System.gc()方法導緻的,那麼我隻需要禁用調System.gc(),然後在觀察gc的情況就可以了,JVM提供了禁用System.gc()的方法,在JVM啟動參數新增 -XX:DisableExplicitGC,然後重新開機Tomcat。

為什麼JVM每隔一小時執行一次Full GC_

-XX:DisableExplicitGC

在JVM增加以上參數并重新開機

為什麼JVM每隔一小時執行一次Full GC_
禁用System.gc()後的gc情況

過了一個半小時仍然沒有FGC,基本可以确認是System.gc()導緻的FGC,但是直接禁用會存在隐患,一些使用了堆外記憶體、或者某些架構依賴System.gc()禁用後會出現不可預知的問題,如果不能直接禁用System.gc,就需要找出是誰調用了System.gc方法,然後在優化。

查閱資料後發現Tomcat有個防止記憶體洩漏的JreMemoryLeakPreventionListener監聽器,會注冊一個定時器,每一個小時調用System.gc(),然而當我們反編譯我們所使用的Tomcat版本後發現,這個定時器的周期已經改為Long.valueOf(Long.MAX_VALUE - 1) 是以也就排除了這個原因。

Class<?> clazz = Class.forName("sun.misc.GC");
Method method = clazz.getDeclaredMethod(
    "requestLatency",
    new Class[] {long.class});
method.invoke(null, Long.valueOf(Long.MAX_VALUE - 1));
           
反射調用sun.misc.GC.requestLatency,傳入latency參數為Long.valueOf(Long.MAX_VALUE - 1)

繼續尋找…之後發現Java的RMI中也有每隔一個小時執行一次gc調用

為什麼JVM每隔一小時執行一次Full GC_
為什麼JVM每隔一小時執行一次Full GC_
為什麼JVM每隔一小時執行一次Full GC_
  • DGC:為RMI分布式垃圾收集(DGC)提供類和接口。 當RMI伺服器将對象傳回給其用戶端(遠端方法的調用者)時,它會跟蹤遠端對象在用戶端中的使用情況。 當用戶端上沒有對遠端對象的引用時,或者引用的“租約”到期且未續訂時,伺服器會收集遠端對象。
  • sun.rmi.transport.DGCClient會注冊一個延時GC任務,會建立一個Daemon的線程,run方法中如果滿足間隔時間就會執行System.gc()方法

我們系統中并沒有單獨使用了RMI協定,于是馬上就想到了Dubbo,因為Dubbo是支援RMI協定的,而我們的RPC架構是使用的Dubbo。

為什麼JVM每隔一小時執行一次Full GC_
Dubbo monitor中可以檢視服務注冊相關資訊

因曆史原因,在一些比較陳舊的系統中有使用到Dubbo的RMI協定,而所有新系統則都使用了Dubbo協定,為了驗證是因為Dubbo的RMI協定引發的FGC,我們把RMI的DGC的gc時間設定為2個小時

-Dsun.rmi.dgc.client.gcInterval=7200000 -Dsun.rmi.dgc.server.gcInterval=7200000

在jvm中加入以上參數并重新開機服務

為什麼JVM每隔一小時執行一次Full GC_
從上圖可以看出,12點10分左右、13點10分左右間隔一小時觸發了GC,在加入jvm參數并重新開機服務後14點第一次GC後,在16點過了2個小時後觸發了第二次GC,由此确定問題是RMI的DGC導緻的。

為了确定将RMI協定改為Dubbo協定後問題可以解決,于是我們将DGC時間調整為5秒,并分别使用RMI協定和Dubbo協定來看FGC的次數變化

RMI協定

為什麼JVM每隔一小時執行一次Full GC_
可以看到每5秒一次FGC

Dubbo協定

為什麼JVM每隔一小時執行一次Full GC_
FGC次數沒有變化

問題已經定位出來了,接下來隻需要将RMI協定替換成Dubbo協定,然後重新開機所有相關服務即可,如何找出所有使用了RMI協定的服務呢?

打開Dubbo monitor,按下F12,在Console中輸入:

$('table').eq(1).find('tr').each(function(){
    var app = $(this).find('td').eq(0).html();
    $.ajax({url : 'http://DubboMonitorAddress/providers.html?application=' + app, success:function(data){
        if(data.indexOf('rmi') != -1) {
            console.log(app)
        }
    }})
});
           
先執行引入jquery,然後執行腳本,可以在console中看到所有使用了rmi協定的dubbo服務

在一段時間後,我們完成了所有服務的協定替換,之後在也沒有出現過502報警了。

為什麼JVM每隔一小時執行一次Full GC_