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/

