JVM診斷及工具筆記(2)使用arthas定位哪裡執行了System#gc()

封面圖片不要使用微信打開文章,可以使用手機/電腦瀏覽器

 

筆者是汽車之家實時計算平台的一名小夥伴。負責flink平台,數據湖及kafka平台的設計與開發。平時擅長做平台設計,定位及解決各種疑難雜症。第二篇文章,講的點依舊很小,但是這次圖多!!! 在這裡感謝支援上篇文章的小夥伴了

前言

這篇文章是之前解決一個Flink任務在線上發生fullgc

image-20211023220400506

 

當時的想法就是fullgc發生在:

  • 堆記憶體: 通過堆記憶體監控和dump堆記憶體這兩種方式 都發現堆佔用不大 ,排除

  • metaspace: gc日誌里並沒有堆metaspace日誌且metaspace佔用很小 ,排除

因此主要把排除重點放在直接記憶體。接下來老規矩,我們長話短說會夾帶一點點心路歷程。

 

步驟

1.配置jvm參數列印gc

 clusterConf.env.java.opts=-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:${LOG_DIRS}/gc.log  

 

2.在gc日誌里發現有程式主動調用System.gc()

 

image-20211023220520256

 

當時猜測是使用的flink11版本程式碼裡面會有調用

image-20211023220653384

加了些日誌,發現並沒有調用。

3.使用arthas

3.1.下載 arthas,並attach需要監聽的jvm進程
curl -O //arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

 

 

image-20211023182623635

3.2.監聽 System.gc()方法並把日誌打到磁碟上
options unsafe true

stack java.lang.System gc -n 1 >> /tmp/gc.log &

 

image-20211023183103082

3.3 觀察日誌

過了一段時間 觀察下日誌發現

image-20220103230624856

是在Kafka connector 調用java的nio的時候調用的System.gc()

4.分析

4.1講一下這塊程式碼的大致背景

java的直接記憶體(direct memory)是不會被gc回收的,而是通過監聽持有直接記憶體資源的對象被回收的時候才把直接記憶體釋放掉的。主要原理使用到虛引用和引用隊列:當jvm發現對象已經沒有強引用,僅剩虛引用時會將其存放在Reference的discovered的列表中 —>隨後變成Pending狀態 (準備加入引動隊列) —->隨後進入Enqueued狀態(加入隊列,監聽者可以獲取從而回收資源)

 

4.2背景介紹完畢,回到剛才找到的方法棧,看倒數第二個方法

 

image-20211023200715246

棧里 DirectByteBuffer 這個類就是直接記憶體資源的持有者。

DirectByteBuffer

DirectByteBuffer 在構造時便被Cleaner監聽回收資源。束於篇幅稍微文字介紹下Cleaner, Cleaner 就是 PhantomReference(虛引用)的子類。 1.Cleaner#create的方法就是將DirectByteBuffer對象虛引用且監聽引用隊列,2.當在隊列中接收到DirectByteBuffer(此時對象已經被jvm標記discovered) 執行Deallocator的回收資源操作。

 

4.3 倒數第一個方法

image-20211023203728028

 

image-20211023204240359

來到倒數第一個方法,打開實現大家應該就能明白,這個方法是給DirectByteBuffer預留資源的。如果資源充足,萬事大吉。如果資源充足: 大家回憶下剛才說講的被虛引用DirectByteBuffer的discovered,pending,enqueued狀態, 圖中我用箭頭標記的jlra.tryHandlePendingReference() 是儘快將pending狀態的對象儘快轉換成enqueued狀態被好被Cleaner回收掉。一頓操作過後發現資源依舊不夠。那麼只能調用System.gc() 方法執行fullgc了。

注: 這套實現中 使用fullgc的好處是有部分 DirectByteBuffer對象很多次回收不掉進入老年代之後,這個時候young gc是沒有辦法回收掉的。

 

5.總結

剛才分析那麼多,其實總結起來就一句話:直接記憶體不夠了。回頭又看了下用戶任務的特點。發現數據消費量很大,但是程式碼可用的直接記憶體不多,反倒是框架可用的直接記憶體(taskmanager.memory.framework.off-heap.size)設得很大。合理調節完資源後,這個任務就再也沒有發生oom了。