NonRegisteringDriver造成的記憶體頻繁FullGc

  • 2020 年 4 月 15 日
  • 筆記

 

 

 某天上伺服器看了下gc情況,發現狀況不對,啟動了才2天的伺服器發生了360次fullgc,這個頻率肯定高了

 

說明

S0C、S1C、S0U、S1U:Survivor 0/1區容量(Capacity)和使用量(Used)
EC、EU:Eden區容量和使用量
OC、OU:年老代容量和使用量
PC、PU:永久代容量和使用量
YGC、YGT:年輕代GC次數和GC耗時
FGC、FGCT:Full GC次數和Full GC耗時
GCT:GC總耗時<br><br>

 

jmap下載下記憶體看下

 

jmap -dump:format=b,file=文件名 [pid]

 

用eclipse的mat工具打開,很明顯的就能發現NonRegisteringDriver里的ConcurrentHashMap佔用了大量記憶體

 

 

 

去NonRegisteringDriver的源碼看看,發現該類只有一個ConcurrentHashMap,也就是connectionPhantomRefs,裡面存放了ConnectionImpl的虛引用

 

 

 

 

繼續往下找,發現在這個位置會放入該map

 

 

看下調用鏈路,猜測是獲取資料庫鏈接的時候丟了個jdbcConnection的虛引用進這個map

 

 

 

繼續找找看看這個存放了虛引用的map在哪會被使用,發現 AbandonedConnectionCleanupThread里起了個執行緒去遍歷虛引用的隊列,在虛引用被回收後會進入這個隊列,在這邊獲取後進行鏈接的cleanup操作

 

 

 至此已經清楚ConnectionPhantomReference的作用就是在gc回收之後,能讓程式碼從隊列里獲取實例再進行相關資源的回收

 

那麼問題來了:

1:為什麼會造成那麼頻繁的fullgc

2:connectionPhantomRefs這個map的作用是什麼

 

1.1:java的堆記憶體對象的ygc次數過多後將會進入老年代

一、對象何時進入老年代
(1)當對象首次創建時, 會放在新生代的eden區, 若沒有GC的介入,會一直在eden區, GC後,是可能進入survivor區或者年老代

(2)當對象年齡達到一定的大小 ,就會離開年輕代, 進入老年代。 而對象的年齡是由GC的次數決定的

-XX:MaxTenuringThreshold=n  新生代的對象最多經歷n次GC, 就能晉陞到老年代, 但不是必要條件   

-XX:TargetSurvivorRatio=n  用於設置Survivor區的目標使用率,即當survivor區GC後使用率超過這個值, 就可能會使用較小的年齡作為晉陞年齡

(3)除年齡外, 對象體積也會影響對象的晉陞的, 若對象體積太大, 新生代無法容納這個對象

-XX:PretenureSizeThreshold  即對象的大小大於此值, 就會繞過新生代, 直接在老年代分配, 此參數只對串列回收器以及ParNew回收有效, 而對ParallelGC回收器無效

1.2:程式碼使用的為HikariCP連接池,而HikariCP有幾個參數

idleTimeout (連接空閑超時時間,默認 10 分鐘)
maxLifetime(連接最大生存時間,默認 30 分鐘)
maxPoolSize (連接池最大連接數)
minIdle(最小空閑連接數,默認等於 maxPoolSize)

可以看出當資料庫連接空閑時間超過了 idleTimeout,那麼將會關閉,直到數量為 minIdle。還有當資料庫連接存活時間達到了 maxLifetime ,那麼連接也會關閉,然後再創建新的連接,而每次創建新的連接都將重複上述步驟

1.3:而我的系統默認配置了500個連接數(minIdle=maxPoolSize=500),而查看阿里雲的監控可以得知活躍的連接數基本就十幾個,導致jdbc連接瘋狂的在關閉打開,而在10分鐘之內經過多次ygc之後,ConnectionImpl都進入了老年代,而老年代的gc間隔很長,就導致connectionPhantomRefs里堆積了一大堆虛引用的對象,而觸發一次gc後會釋放

 

 

2:為了驗證這個map的作用,我建了個類

public class Test  {
    public static ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
    public static Map<PhantomReference, PhantomReference> map = new HashMap<>();
    public PhantomReference<Object> phanRef;

    public void test() throws InterruptedException {
        Object obj = new Object();

        phanRef = new PhantomReference<Object>(obj, refQueue);

        //   map.put(phanRef, phanRef);

} }

 

然後在另外一個類裡面執行以下方法

 public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        test.test();
        //PhantomReference<Object> s = drugTest.phanRef;
        //System.out.println(s);
        test = null;
        System.gc();
        System.gc();
        System.gc();
        System.out.println(test.refQueue.poll());
        //System.out.println(s);
        //s = null;
        System.out.println(test.refQueue.poll());
    }

 

當map.put(phanRef, phanRef);這行程式碼被注釋時,輸出

null
null

 

當map.put(phanRef, phanRef);這行程式碼存在時,輸出

null
java.lang.ref.PhantomReference@4d591d15

 

當map.put(phanRef, phanRef);這行程式碼存在時,並且取消PhantomReference<Object> s = drugTest.phanRef; s = null;的注釋時輸出

null
java.lang.ref.PhantomReference@4d591d15

 

從這個實驗可以看出,當phanRef沒有被其他地方引用時,當他所屬的實例類被回收後他並不會進入到弱引用的隊列中(可能和gc機制有關吧)

 

解決方案:

1:調整HikariCP連接池的參數

2:調整jvm堆記憶體的參數

3:手動刪除connectionPhantomRefs這個ConcurrentHashMap中的數據

4:手動調system.gc(我覺得這個方法很扯淡,手動掉還不如讓jvm自己判斷調呢,反正不會記憶體溢出)

 

參考文章

//www.jianshu.com/p/6d37afd1f072

//www.cnblogs.com/newcj/archive/2011/05/15/2046882.html

//www.cnblogs.com/yaowen/p/10975241.html

//blog.csdn.net/u010657094/article/details/104042326/

Tags: