大家要的線上GC問題案例來啦

先推薦一篇好文

最近寫了幾篇關於GC的文章,主要是由於線上有一些關於GC的問題,因此想順便總結一波,梳理一下GC的一些知識點和排查思路。html

以前有讀者留言說能不能寫一篇實戰經驗方面的,這不就來了嗎~java

咱們項目上用到的主要仍是CMS + ParNew的組合。因此重點看的資料也是這方面的。緩存

在學習的過程當中,也拜讀了美團技術團隊的這篇文章《Java中9種常見的CMS GC問題分析與解決》。這篇文章質量很是高,從理論知識,源碼分析,到常見的GC問題案例,囊括了分析路徑、根因、調優策略等等內容,很是詳盡且全面,尤爲是最後部分的處理流程SOP和根因魚骨圖,很是nice。牆裂推薦,值得一讀!!!微信

知道你們喜歡現成的,因此我手動copy了這兩張圖過來,有須要的自取:併發

排查問題SOP

根因魚骨圖

GC問題案例

我遇到的案例可能沒有上面文章做者那麼豐富,但也是真實遇到的幾個案例,因此借這篇文章分享出來,你們能夠參考參考,避免踩相似的坑。app

案例1 Young GC頻繁

以前有個任務會頻繁地重複調用一個接口。因此用guava cache作了一個簡單的內存緩存。結果上線後發現常常收到Young GC頻繁的告警,時間跟這個任務的啓動時間也比較吻合。框架

經過監控看到的GC圖大概是這樣:高併發

案例1

能夠看到,Young GC的次數會在某一個時間點飆升。同時伴隨着Old區域內存快速升高,最後會觸發一次Full GC。oop

根據這個狀況,能夠確定的是因爲本次代碼改動引發的。經過Heap Dump分析後發現,佔用內存較大的是一個guava cache的Map對象。源碼分析

查找代碼發現,使用guava cache的時候,沒有設置最大緩存數量和弱引用,而是設置了一個幾分鐘的過時時間。而這個任務的量又比較大,到線上後很快就緩存了大量的對象,致使頻繁觸發Young GC,但又因爲有引用GC不掉(這個從Survivor區的內存大小圖像能夠推測),因此慢慢代數比較多的對象就晉升到了老年代,後面老年代內存到達必定閾值引起Full GC。

後面經過設置最大緩存數量解決了這個問題。又積累了一個寶貴的經驗,完美!

案例2 Young GC和Old GC都頻繁

在線上灰度環境中發現收到Young GC和Old GC頻繁的告警。監控看到的GC圖大概長這樣:

案例2

根據GC圖大概能夠看出來,Young GC和Old GC都很是頻繁,且每次都能回收走大量的對象。那能夠簡單地推測:確實是產生了大量的對象,且極有可能有一部分大對象。小對象引起的Young GC頻繁,而大對象引起了Old GC頻繁。

排查下來也是一段代碼引發的。對於一個查詢和排序分頁的SQL,同時這個SQL須要join多張表,在分庫分表下,直接調用SQL性能不好,甚至超時。因而想了個比較low的辦法:查單表,把全部數據查出來,在內存排序分頁。用了一個List來保存數據,而有些數據量大,形成了這個現象。用Heap Dump分析,也印證了這個猜想,List類型的對象佔用了大量的空間。

案例3 接口線程池滿和Full GC

這是一個報接口線程池滿的問題。但每次都會在同一時間Full GC。監控圖大概長這樣:

案例3

從時間上來看,先是Java線程數量飆升,而後觸發Full GC。後面重啓後,Java線程數量恢復正常水位。

這裏涉及到一個冷知識:一個Java線程默認會佔用多少內存?

這個參數能夠控制: -XX:ThreadStackSize。在64 位的Linux下, 默認是1 MB(這個說法也不全對,仍是取決於棧深度)。Java 11對這個有一個優化,能夠減小內存佔用。詳情能夠參考這篇文章: https://dzone.com/articles/ho...

排查下來根因是這個應用仍是使用的Log4j 1,而Log4j 1有性能問題,在高併發下,下面這段代碼的同步塊可能會引發大量線程阻塞:

void callAppenders(LoggingEvent event) {
    int writes = 0;

    for(Category c = this; c != null; c=c.parent) {
        // Protected against simultaneous call to addAppender, removeAppender,...
        synchronized(c) {
            if(c.aai != null) {
                writes += c.aai.appendLoopOnAppenders(event);
            }
            if(!c.additive) {
                break;
            }
        }
    }

    if(writes == 0) {
        repository.emitNoAppenderWarning(this);
    }
}

解決辦法就是減小日誌打印,升級日誌框架到Log4j 2或者Logback。

案例4 應用啓動時Full GC頻繁

這個是較早的一個案例了,GC圖已經找不到了。

但引起Full GC頻繁的大概就這幾種可能性:

  • 調用System.gc()
  • Old區空間不足
  • 永久代/元空間滿

根據代碼和GC圖排除了前面兩種可能性,那就是元空間滿了。在Java 8中,XX:MaxMetaspaceSize是沒有上限的,最大容量與機器的內存有關;可是XX:MetaspaceSize是有一個默認值的:21M。而若是應用須要進元空間的對象較多(好比有大量代碼),就會頻繁觸發Full GC。解決辦法是能夠經過JVM參數指定元空間大小:-XX:MetaspaceSize=128M

總結

能夠看到上面的大多數案例都是代碼改動或者應用框架引發的。通常公司內都會有默認的一套JVM參數,真正須要JVM參數調優解決問題的case實際上是比較少的。

並且有時候GC問題可能並非問題的根源,有多是其它問題引起的GC問題,在實際排查的時候要根據日誌及時間線去判斷。

至於怎麼去判斷是否「頻繁」和「耗時長」?雖然有一些公式計算,但我以爲只要不影響現有的業務,那就是能夠接受的。而若是某個應用的GC表現明顯不一樣於以往的平均水平,或者其它類似應用的水平,那就有理由懷疑是否是存在GC問題了。

不少時候GC問題不充分的壓測是測試不出來的,而壓測成本又比較大。常常會在線上灰度發佈中觀察到,因此須要在灰度發佈的時候密切觀察系統的監控和告警。若是有條件,能夠上紅藍部署,下降風險。

GC問題仍是蠻複雜的,須要大量的經驗和理論知識。遇到以後能夠總結分析一下根因,平時也能夠看看其餘人的博客,吸收經驗,這樣就能不斷完善本身的知識體系。

求個支持

我是Yasin,一個堅持技術原創的博主,個人微信公衆號是:編了個程

都看到這兒了,若是以爲個人文章寫得還行,不妨支持一下。

文章會首發到公衆號,閱讀體驗最佳,歡迎你們關注。

你的每個轉發、關注、點贊、評論都是對我最大的支持!

還有學習資源、和一線互聯網公司內推哦

求個支持

我是Yasin,一個堅持技術原創的博主,個人微信公衆號是:編了個程

都看到這兒了,若是以爲個人文章寫得還行,不妨支持一下。

文章會首發到公衆號,閱讀體驗最佳,歡迎你們關注。

你的每個轉發、關注、點贊、評論都是對我最大的支持!

還有學習資源、和一線互聯網公司內推哦

相關文章
相關標籤/搜索