【日活百萬DS App】一次線上JVM問題定位排查

前言

這篇文章最早是寫于2019-08-14,在公司的confluence發(fā)布過。由于最近想標記整理壓縮一些知識點碎片,于是便有了這篇文章的誕生。

項目介紹

  • DS App
  • DAU 百萬
  • 注冊用戶近3000w
  • DNU 幾十萬

線上排查工具介紹

  • jmapjava虛擬機自帶的一種內存映像工具。

  • jstatJDK自帶的一個輕量級小工具。全稱Java Virtual Machine statistics monitoring tool,它位于javabin目錄下,主要利用JVM內建的指令對Java應用程序的資源和性能進行實時的命令行的監(jiān)控,包括了對Heap size和垃圾回收狀況的監(jiān)控。

  • jstackJdk自帶的線程跟蹤工具,用于打印指定Java進程的線程堆棧信息。

  • mat是一個檢查內存泄漏的工具

  • vjmap是唯品會開源出來的是用于排查內存緩慢泄露,老生代增長過快原因的利器。因為jmap -histo PID 打印的是整個Heap的對象統計信息,而為了定位上面的問題,我們需要專門查看OldGen對象,和Survivor區(qū)大齡對象的工具。(只支持CMSParallelGC,不支持G1)

  • jvisualvmJDK自帶的監(jiān)控程序。能夠監(jiān)控線程,內存情況,查看方法的CPU時間和內存中的對象,已被GC的對象,反向查看分配的堆棧

  • jconsoleJDK自帶的監(jiān)控程序。用于對JVM中內存,線程和類等的監(jiān)控。

線上排查使用到的命令

查看進程使用gc情況: jstat -gc 16969<pid> 5000(打印時間間隔)

  • S0C:年輕代中第一個survivor(幸存區(qū))的容量 (KB)
  • S1C:年輕代中第二個survivor(幸存區(qū))的容量 (KB)
  • S0U:年輕代中第一個survivor(幸存區(qū))目前已使用空間 (KB)
  • S1U:年輕代中第二個survivor(幸存區(qū))目前已使用空間 (KB)
  • EC:年輕代中Eden(伊甸園)的容量 (KB)
  • EU:年輕代中Eden(伊甸園)目前已使用空間 (KB)
  • OCOld代的容量 (KB)
  • OUOld代目前已使用空間 (KB)
  • MC:元空間的容量 (KB)
  • MU:元空間目前已使用空間 (KB)
  • CCSC:壓縮類空間大小
  • CCSU:壓縮類空間使用大小
  • YGC:從應用程序啟動到采樣時年輕代中gc次數
  • YGCT:從應用程序啟動到采樣時年輕代中gc所用時間(s)
  • FGC:從應用程序啟動到采樣時old代(全gc)gc次數
  • FGCT:從應用程序啟動到采樣時old代(全gc)gc所用時間(s)
  • GCT:從應用程序啟動到采樣時gc用的總時間(s)

類加載統計
jstat -class 16969<pid>

java進程堆棧信息打印
jstack 16969<pid>

查看pid對應的線程使用CPU情況和內存占用:
top -Hp pid

打印當前java堆中各個對象的數量,大小一頁數據為50
jmap -histo pid | more -n 50

查看java堆中實例數數量前10的類
jmap -histo pid | sort -n -r -k 2 | head -10

查看java堆中容量前10的類
jmap -histo pid | sort -n -r -k 3 | head 10

查看整個JVM內存狀態(tài)
jmap -heap <pid>

快照java堆信息到指定文件(會觸發(fā)Full GC, 如果快照文件過大,會使目標進程stop world)
jmap -dump:format=b,file=quMall.dump 16969<pid>

顯示堆中活躍對象的統計信息,比如實例數量,占用空間(會觸發(fā)Full GC)
jmap -histo:live <pid>

查看進程使用的GC算法(實際上用到了javaagent技術來實現的:
jinfo -flags 23467<pid>

查看應用運行的時間
ps -p 11864<pid> -o etime

顯示垃圾回收的相關信息, 同時顯示最后一次或當前正在發(fā)生的垃圾回收的誘因
jstat -gccause <pid> 5000<interval>

查看GC使用情況(比如s0區(qū)使用了多少):
jstat -gcutil 6011<pid>

查看JVM內存中三代(young,old,metaspace)對象的使用和占用大小
jstat -gccapacity <pid>

查看metaspace中對象的信息及其占用量
jstat -gcmetacapacity<pid>

查看年輕代對象的信息
jstat -gcnew <pid>

查看年輕代對象的信息及其占用量
jstat -gcnewcapacity <pid>

查看old代對象的信息
jstat -gcold <pid>

查看old代對象的信息及其占用量
jstat -gcoldcapacity <pid>

查看進程是否啟動AdaptiveSizePolicy策略
jinfo -flag UseAdaptiveSizePolicy 26626<pid>

查看MetaSpace默認初始化大小(默認是20.8MB)
java -XX:+PrintFlagsInitial|grep Meta

打印整個堆中對象的統計信息,按對象的total size排序
./vjmap.sh -all PID > /tmp/histo.log

推薦,打印老年代的對象統計信息,按對象的oldgen size排序,比-all快很多,暫時只支持CMS
./vjmap.sh -old PID > /tmp/histo-old.log

推薦,打印Survivor區(qū)的對象統計信息,默認age>=3
./vjmap.sh -sur PID > /tmp/histo-sur.log

推薦,打印Survivor區(qū)的對象統計信息,查看age>=4的對象
./vjmap.sh -sur:minage=4 PID > /tmp/histo-sur.log

推薦,打印Survivor區(qū)的對象統計信息,單獨查看age=4的對象
./vjmap.sh -sur:age=4 PID > /tmp/histo-sur.log

僅輸出存活的對象,原理為正式統計前先執(zhí)行一次full gc
./vjmap.sh -old:live PID > /tmp/histo-old-live.log

過濾對象大小,不顯示過小的對象。按對象的oldgen size進行過濾,只打印OldGen占用超過1K的數據
./vjmap.sh -old:minsize=1024 PID > /tmp/histo-old.log

dump文件分析

1.快照目標進程的堆信息到執(zhí)行文件jmap -dump:format=b,file=mall.dump 16969<pid>。dump文件比較大,如果從生產服務器拉到本地,會觸發(fā)帶寬報警。如果要執(zhí)行該操作,請在電商研發(fā)團隊事先@所有人。

2.使用mat工具打開堆快照文件。(mat的基本操作可以看參考文章)

  • Histogram可以列出內存中的對象,對象的個數以及大小
  • Dominator Tree可以列出處于活躍狀態(tài)的大對象。
  • Top consumers通過圖形列出最大的object
  • Leak Suspects自動分析泄漏的原因。
Image.png

3.點開Reports->Leak Suspects 查看有可能內存泄漏的地方,
,可以體現出哪些對象被保持在內存中,以及為什么它們沒有被垃圾回收。
圖1可以看到WebappClassLoader占用了25,177,944字節(jié)的容量,
這是tomcat的類加載器,JDK自帶的系統類加載器中占用比較多的是HashMap,這個其實比較正常。 圖2可以看到JDBC4Connection的實例有539個,占用的空間是22,302,560字節(jié),這個是比較可疑。

Image [1].png
Image [2].png

4.點開Actions->Histogram,模擬搜索JDBC。發(fā)現關于JDBC相關的實例數還挺多的。右鍵點擊com.mysql.jdbc.JDBC4Connection,選擇with outgoing references 查看JDBC4Connection實例具體的依賴關系。

  • with incoming references表示的是 當前查看的對象,被外部應用。
  • with outGoing references表示的是 當前對象,引用了外部對象。
Image [4].png
Image [5].png
Image [6].png

5.選中一個JDBC4Connection實例,右鍵選擇Merge Shortest Paths to GC roots -> with all references查看JDBC4ConnectionGC roots的路徑。

  • Paths to GC : 從當前對象到GC roots的路徑,這個路徑解釋了為什么當前對象還能存活,對分析內存泄露很有幫助,這個查詢只能針對單個對象使用。
  • Merge Shortest Paths to GC :從GC roots到一個或一組對象的公共路徑。
  • exclude all phantom/weak/soft etc.reference(排除所有虛弱軟引用) ->查看剩余未被回收的強引用對象占用原因 & GC Roots。

6.可見JDBC4Connection被2個對象引用,一個是BasicResourcePool中的formerResources(曾經在連接池里呆過的對象)變量,一個是MySQL JDBC DriverConnectionPhantomReference

Image [6].png

NonRegisteringDriver.java

Image [7].png

這個虛引用是在JDBC Driver在構造connection時用來track這個connection的,在被GC回收前做一些clean up(釋放資源)的事情,所以每個connection被構造出來后,都會被track,這是Driver為了防止有資源隨著連接回收而未釋放的手段。


public class AbandonedConnectionCleanupThread extends Thread {
    private static boolean running = true;
    private static Thread threadRef = null;

    public AbandonedConnectionCleanupThread() {
        super("Abandoned connection cleanup thread");
    }

    public void run() {
        threadRef = this;
        while (running) {
            try {
                Reference<? extends ConnectionImpl> ref = NonRegisteringDriver.refQueue.remove(100);
                if (ref != null) {
                    try {
                        ((ConnectionPhantomReference) ref).cleanup();
                    } finally {
                        NonRegisteringDriver.connectionPhantomRefs.remove(ref);
                    }
                }

            } catch (Exception ex) {
                // no where to really log this if we're static
            }
        }
    }

    public static void shutdown() throws InterruptedException {
        running = false;
        if (threadRef != null) {
            threadRef.interrupt();
            threadRef.join();
            threadRef = null;
        }
    }

}

PhantomReference引用的對象在被GC時,
JVM會將PhantomReference對象扔到refqueue。然后會通過
AbandonedConnectionCleanupThread這個線程從NonRegisteringDriver.refQueue中拿到ConnectionPhantomReference,然后執(zhí)行cleanup方法,最后刪除connectionPhantomRefs這個ConcurrentHashMap中的ConnectionPhantomReference對象,完成connection相關資源的回收。

但是我們排查的結果是ConnectionPhantomReference并沒有得到清理。經過查閱一些資料,得出以下結論:

似乎問題是CompressionInputStream'有對同一ConnectionImpl的引用,ConnectionPhantomReference正在等待在ReferenceQueue上排隊,說它有資格進行垃圾回收。ConnectionPhantomReference通過它引用的com.mysql.jdbc.NetworkResources對象對CompressionInputStream有一個強引用。ConnectionPhantomReferenceNonRegisteringDriver中的靜態(tài)ConcurrentHashMap強烈保存。結果是ConnectionPhantomReference有效地引用了它要等待GCJDBC4Connection對象; 并使JDBC4Connection實例保持活動狀態(tài),因此不符合垃圾回收的條件。

Image [8].png

所以在我們程序中要定時去清除ConnectionPhantomReference引用。在quMall項目中一臺服務器中Full GC清除7110個PhantomReference引用,耗時竟然達到0.6s。但是通過程序定時刪除這些引用,下一次Full GC清除這些引用耗時可以忽略不計。

/**
 * @author xiaoma
 * @version V1.0
 * @Description: 解決PhantomReference引用過多,造成內存泄漏
 * PhantomReference, 7110 refs, 587 refs, 0.6012897 secs
 * @date 2019/8/12 15:12
 */
@Service
public class MysqlConnectionPhantomRefCleaner implements InitializingBean {

    private Field declaredField;
    private ConcurrentHashMap referenceMap;

    @Override
    public void afterPropertiesSet() throws Exception {
        try {
            declaredField = NonRegisteringDriver.class.getDeclaredField("connectionPhantomRefs");
            declaredField.setAccessible(true);
            referenceMap = (ConcurrentHashMap<?, ?>) declaredField.get(null);
        } catch (Exception ex) {
            LogService.info(MessageFormat.format(
                    "清除PhantomReference計劃, 初始化失敗:{0}",
                    ex.getMessage()
            ));
        }
    }

    /**
     * 每秒10s清理一次
     */
    @PostConstruct
    public void clear() {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        LogService.info("清除PhantomReference計劃, 初始化成功");

        synchronized (MysqlConnectionPhantomRefCleaner.class) {
            scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    if (!referenceMap.isEmpty()) {
                        int size = referenceMap.size();
                        LogService.info(MessageFormat.format(
                                "清除PhantomReference計劃, current Mysql ConnectionPhantomReference map size:{0} start to clear",
                                size

                        ));
                        referenceMap.clear();
                    }
                }
            }, 1, 10, TimeUnit.SECONDS);
        }
    }

}
Image [9].png

7.再來講講formerResources引用,我列出formerResources
相關代碼。

private void removeResource(Object resc, boolean synchronous){
    ...
    unused.remove(resc);
    destroyResource(resc, synchronous, checked_out);

    addToFormerResources( resc );

    asyncFireResourceRemoved( resc, false, managed.size(), unused.size(), excluded.size() );
    ...
}

private void cullExpired(){
    ...
    if ( shouldExpire( resc ) )
    {
        if ( logger.isLoggable( MLevel.FINER ) )
            logger.log( MLevel.FINER, "Removing expired resource: " + resc + " [" + this + "]");

        target_pool_size = Math.max( min, target_pool_size - 1 ); //expiring a resource resources the target size to match

        removeResource( resc );
    }
    ...
}

public Object checkoutResource( long timeout ){

    Object resc = prelimCheckoutResource( timeout );

...
}

private synchronized Object prelimCheckoutResource( long timeout ){
    ...
    Object  resc = unused.get(0);
    ...
    else if ( shouldExpire( resc ) ){
      removeResource( resc );
      ensureMinResources();
      return prelimCheckoutResource( timeout );
    }
}

cullExpired這個方法是c3p0的一個定時器里執(zhí)行的方法,用來檢查過期連接的。c3p0會定時對idle連接進行連接池過期檢查。若空閑時間超過MaxIdleTime,則會remove,會加入到formerResources中。如果最后剩下的idle連接數超過或者小于minIdle的連接數,也會相應的進行縮減或者擴容,直到minIdle個連接數。

idleConnectionTest是維持連接池的idle連接和MySQL之間的心跳,防止MySQL Server端踢掉應用的連接,而前面提到的連接池過期檢查則是c3p0對連接歸還后是否長時間沒被再次借出為依據來判斷連接是否過期。

如果配置了maxIdleTime, 處于keep alived的連接會被認為是過期連接。連接池將丟棄這些連接,并創(chuàng)建新連接。因此,NonRegisteringDriver將慢慢地保留越來越多的JDBC4Connection對象,并且您將慢慢地發(fā)生內存泄漏。 這里也會造成頻繁的Young GC。

checkoutResource是每次獲取連接的時候,會對連接進行過期檢查的校驗。如果連接空閑時間超過MaxIdleTime,該連接會標志已經過期,
調用removeResource,將連接加入到formerResources中。配置了MaxIdeTime參數,checkout操作會加快過期連接的失效和創(chuàng)建新連接的速度,導致formerResources里退休的連接變多,最終加快了老年代的增長和內存泄漏。

正確的姿勢是去掉maxIdleTime配置,
配置了idleConnectionTestPeriod這個參數即可。

類似的問題還會存在JedisPoolConfig中,這些都可以歸結成持久化對象在JVM中的存活生命周期問題。
連接池對象,本地緩存對象都是,這些對象存活時間久,處于JVM的老生代中,應用希望盡可能的重用它們,但若結合具體場景的配置或使用不合理,導致這些對象并未最大化被重用,比如上面提到的過期檢查導致不斷有新的對象被創(chuàng)建出來,因為是持久化對象,很容易就進入到了老生代,霸占了資源。

Image [10].png

上圖是JedisPoolConfig默認配置,每30秒進行一次掃描,如果發(fā)現有空閑時間超過60s連接,執(zhí)行清除操作。如果當前連接少于minIdle個,則會再創(chuàng)建新的。而清除掉的連接若未及時的被YGC掉,會進入到老年代。

Image [11].png

我們只需配置minEvictableIdleTimeMills=-1,softMinEvictableIdleTimeMillis=60000就可以上述的問題。這樣就不會對minIdle的連接進行清理,只有當連接數超過minIdle后,才進行清理工作。

JVM基礎參數

-XX:ParallelGCThreads=8
JVM在進行并行GC的時候,用于GC的線程數,一般是機器的核數。

-XX:+UseConcMarkSweepGC
打開此開關參數后,使用ParNew+CMS+Serial Old收集器組合進行垃圾收集。Serial Old作為CMS收集器出現Concurrent Mode Failure的備用垃圾收集器。

-XX:+UseParallelGC
打開此開關參數后,使用Parallel Scavenge+Serial Old收集器組合進行垃圾收集。

-XX:+UseParallelOldGC
打開此開關參數后,使用Parallel Scavenge+Parallel Old收集器組合進行垃圾收集。

JDK1.8 默認使用Parallel Scavenge年輕代回收器和Parallel Old老年代回收器, 默認打開AdaptiveSizePolicy策略。CMS默認關閉AdaptiveSizePolicy策略。在quMall項目中,沒有關閉AdaptiveSizePolicy策略,造成S0,S1區(qū)只有8MB,從而導致頻繁Young GC。所以要手動設置使Eden:S0:S1 = 8 : 1 : 1

-XX:SurvivorRatio=8
-XX:-UseAdaptiveSizePolicy

AdaptiveSizePolicy為了達到三個預期目標,涉及以下操作:

  • 如果 GC停頓時間超過了預期值,會減小內存大小。理論上,減小內存,可以減少垃圾標記等操作的耗時,以此達到預期停頓時間。
  • 如果應用吞吐量小于預期,會增加內存大小。理論上,增大內存,可以降低 GC的頻率,以此達到預期吞吐量。
  • 如果應用達到了前兩個目標,則嘗試減小內存,以減少內存消耗。

引用R大說過的話:
HotSpot VM里,ParallelScavenge系的GCUseParallelGC,UseParallelOldGC)默認行為是SurvivorRatio如果不顯式設置就沒啥用。顯式設置到跟默認值一樣的值則會有效果。因為ParallelScavenge系的GC最初設計就是默認打開AdaptiveSizePolicy的,它會自動、自適應的調整各種參數

在這里說下默認的ParallelScavengeParallelOld垃圾回收器:

  • Paralle Scavenge(年輕代):

    • 新生代收集器,可以和Serial OldParallel組合使用,不能和CMS組合使用。采用復制算法。使用多線程進行垃圾回收,回收時會導致Stop The World。關注系統吞吐量。
    • -XX:MaxGCPauseMillis:設置大于0的毫秒數,收集器盡可能在該時間內完成垃圾回收。
    • -XX:GCTimeRatio:大于0小于100的整數,即垃圾回收時間占總時間的比率,設置越小則希望垃圾回收所占時間越小,CPU能花更多的時間進行系統操作,提高吞吐量。也就是垃圾收集時間占總時間的比率,相當于是吞吐量的倒數。如果把此參數設置為19,那允許的最大GC時間就占總時間的5%(即1 /(1+19)),默認值為99,就是允許最大1%(即1 /(1+99))的垃圾收集時間。
    • GC日志關鍵字:PSYoungGen
  • Parallel Old(年老代)

    • 年老代收集器,只能和Parallel Scavenge組合使用(Parallel Scavenge收集器的年老代版本。采用標記-整理算法,會對垃圾回收導致的內存碎片進行整理關注吞吐量的系統可以將Parallel Scavenge+Parallel Old組合使用。
    • GC日志關鍵字:ParOldGen

對于重載了 Object 類的 finalize 方法的類實例化的對象(這里稱為 f 對象),JVM 為了能在GC 對象時觸發(fā)f對象的finalize 方法的調用,將每個f對象包裝生成一個對應的FinalReference 對象,方便 GC 時進行處理。

SocksSocketImpl中重載了finalize方法,防止Socket 連接忘記關閉導致資源泄漏而進行的保底措施。

Image [12].png

對于RPC調用短連接場景,每調用一次就會創(chuàng)建一個Socket 對象。致使 FinalReference 對象非常多, 因此YoungGC 耗時增加。
下面參數, 可以在 GC 的時候多線程并行處理Reference,降低GC時長。
-XX:+ParallelRefProcEnabled

開啟壓縮對象指針(默認開啟),啟用CompressOops后,會壓縮的對象:
? 每個Class的屬性指針(靜態(tài)成員變量)
? 每個對象的屬性指針
? 普通對象數組的每個元素指針。
-XX:+UseCompressedOops

  • 擴展知識:
    • HotSpot虛擬機中,對象在內存中存儲的布局,可以分為三塊區(qū)域:對象頭(header),實例數據(Instance Data),對象填充(Padding)。對象頭包括markword和類型指針(class對象指針)。如果是數組,還包括數組長度。

    • 實例數據:對象實際的數據。

    • 對象填充:對齊,按8字節(jié)對齊。Java內存地址按照8字節(jié)對齊,長度必須是8的倍數。
      markword:用于存儲對象自身運行時的數據,如哈希碼,GC分代年齡,鎖狀態(tài)標志,線程持有的鎖,偏向線程ID,偏向時間戳。

    • 32JVM中,markword32bit,類型指針是32bit,數組長度是32bit。從而得知一個普通對象頭是8個字節(jié),一個數組對象頭是16個字節(jié)。

    • 64位JVM中,markword是64bit,類型指針是64bit,開啟指針壓縮時是32bit,數組長度是64bit,開啟壓縮時是32bit。從而得知一個無壓縮的普通對象頭是16個字節(jié),一個開啟壓縮的普通對象頭是12個字節(jié)。一個無壓縮的數組對象頭是24個字節(jié),一個開啟壓縮的數組對象頭是16個字節(jié)。對象頭在32位系統占用8字節(jié),而在64位系統上占用16字節(jié)。

    • Referece類型在32位系統上每個占用4字節(jié),而在64位系統上每個占用8字節(jié)。

      Image [13].png

Metaspace中開辟出一塊類指針壓縮空間(Compressed Class Space),默認是1G(默認開啟)。對象中指向類元數據的指針會被壓縮成32位。

-XX:+UseCompressedClassPointers
-XX:CompressedClassSpaceSize=1G

使用CMS時,打開對年老代的壓縮??赡軙绊懶阅?,但是可以消除碎片。
-XX:+UseCMSCompactAtFullCollection

使用CMS,設置多少次FullGc后,對年老代進行壓縮。
由于CMS不對內存空間進行壓縮、整理、所以運行一段時間以后會產生“碎片”,使得運行效率降低。此值設置運行多少次GC以后對內存空間進行壓縮、整理。

-XX:CMSFullGCsBeforeCompaction=0

這兩個設置一般配合使用,一般用于『降低CMS GC頻率或者增加頻率、減少GC時長』的需求

-XX:CMSInitiatingOccupancyFraction=70 是指設定CMS在對內存占用率達到70%的時候開始GC(因為CMS會有浮動垃圾,所以一般都較早啟動GC);

-XX:+UseCMSInitiatingOccupancyOnly 只是用設定的回收閾值(上面指定的70%),如果不指定,JVM僅在第一次使用設定值,后續(xù)則自動調整。

CMS整個過程分為4步:

  • 初始標記(CMS initial mark)
  • 并發(fā)標記(CMS concurrent mark)
  • 重新標記(CMS remark)
  • 并發(fā)清除(CMS concurrent sweep)。在這個階段會出現浮動垃圾。CMS收集器無法處理浮動垃圾,可能出現Concurrent Mode Failure失敗而導致另一次Full GC的產生。由于在垃圾收集階段用戶線程還需要運行,即還需要預留足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發(fā)收集時的程序運作使用。

CMS GC前啟動一次ygc,目的在于減少old genygc gen的引用,降低remark時的開銷, 一般CMSGC耗時80%都在remark階段。
-XX:+CMSScavengeBeforeRemark

在進行GC的前后打印出堆的信息
-XX:+PrintHeapAtGC

輸出GC的詳細日志
-XX:+PrintGCDetails

輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps

打印GC時各種引用的處理時間。
-XX:+PrintReferenceGC

輸出GC日志
-XX:+PrintGC

打開了就知道是多大的新生代對象晉升到老生代失敗從而引發(fā)Full GC時的。
-XX:+PrintPromotionFailure

輸出顯示在survivor空間里面有效的對象的歲數情況。
-XX:+PrintTenuringDistribution

打印產生GC的原因,比如AllocationFailure什么的,在JDK8已默認打開,JDK7要顯式打開一下。
-XX:+PrintGCCause

GC日志存放的位置。

-Xloggc:logs/quMall_gc.log"

如果達到初始化值就會觸發(fā)垃圾收集進行類型卸載,同時GC會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時,適當提高該值。默認值是20.8MB。如果不手動指定元空間的大小,會因為元空間的擴容機制,造成頻繁的Full GC。

-XX:MetaspaceSize=256m 
-XX:MaxMetaspaceSize=256m

默認(MinHeapFreeRatio參數可以調整)空余堆內存小于40%時,JVM就會增大堆直到-Xmx的最大限制。
默認(MaxHeapFreeRatio參數可以調整)空余堆內存大于70%時,JVM會減少堆直到 -Xms的最小限制。
注意:java應用的jvm參數XmsXmx保持一致,避免因所使用的Java堆內存不夠導致頻繁full gc以及full gc中因動態(tài)調節(jié)Java堆大小而耗費延長其周期。

-XX:MinHeapFreeRatio=40
-XX:MaxHeapFreeRatio=70
-Xms3072m 
-Xmx3072m

如果沒有配置-XX:+DisableExplicitGC,即沒有屏蔽System.gc()觸發(fā)FullGC,那么可以通過排查GC日志中有System字樣判斷是否System.gc()觸發(fā)。
注意:如果應用中有用到Netty并且配置了該參數會內存溢出。
Netty中的DirectByteBuffer分配空間過程中發(fā)現直接內存不足時會顯式調用System.gc(),以期通過Full GC來強迫已經無用的DirectByteBuffer對象釋放掉它們關聯的native memory

-XX:+DisableExplicitGC

GC日志分析

之前看到電商模塊中的一臺服務器實例頻繁Full GCYGC,感覺很不正常,以下是排查定位問題的步驟。

Image [14].png

1.先定位整個堆中哪些對象的實例數和占用空間比較多。話外音:如果垃圾回收器使用的是CMSParallelGC,可以使用vjmap快速定位年老代和Survivor區(qū)的對象統計信息,減少卡頓時間。
使用jmap -histo:live <pid>命令,可以看到跟JDBC相關類的實例數和占用空間比較多。為了進一步驗證猜想,可以dump文件導入到mat工具進行分析(上面已經驗證了JDBC存在內存泄漏)。

Image [15].png

2.使用jstat -gc 16969<pid> 5000<interval>命令查看gc情況,發(fā)現S0S1區(qū)只有7-8MB。接著使用jstat -gcutil 6011<pid>查看堆中各區(qū)域使用情況,發(fā)現S0區(qū)占比很高。

Image [16].png
Image [17].png

因為使用的是Parallel Scavenge、Parallel Old垃圾回收器,會啟動AdaptiveSizePolicy策略。當這個參數打開之后,就不需要手工指定新生代的大?。?code>-Xmn)、EdenSurvivor區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對象大小門檻(-XX:PretenureSizeThreshold)等細節(jié)參數了,虛擬機會根據當前系統的運行情況收集性能監(jiān)控信息,動態(tài)調整這些參數以提供最合適的停頓時間或最大的吞吐量,這種調節(jié)方式稱為GC自適應的調節(jié)策略(GC Ergonomics)。

所以我們要禁用這個參數且將Eden:S0:S1設置為8:1:1

-XX:SurvivorRatio=8
-XX:-UseAdaptiveSizePolicy

設置完這個參數后,再查看GC使用情況后,發(fā)現S0S1區(qū)容量變大,S0區(qū)占用比也急劇減少,從而YGC減少。

Image [18].png
Image [19].png

3.查看GC日志,發(fā)現造成頻繁Full GC的原因是Metadata GC Threshold。這由于沒有設置Metaspace參數,導致初始化大小為20.8MB的元空間不夠用,頻繁擴容導致的Full GC。

-XX:MetaspaceSize=256m 
-XX:MaxMetaspaceSize=256m
Image [20].png
  1. 如果使用jmap -histo:live <pid>jmap -dump:format=b,file=quMall.dump 16969<pid>會造成Full GC。
jmap命令導致的Full GC.jpg

5.可以看到Full GC時清除7100PhantomReference引用耗時長達0.6s。根據上面的分析,我們應該定時清除PhantomReference引用。同時設置并行處理引用。優(yōu)化后的Full GC清除PhantomReference引用耗時0.0000805s

-XX:+ParallelRefProcEnabled
Image [21].png

6.另外需要注意的是-Xms-Xmx應該設置成一樣大,避免在每次GC 后調整堆的大小,從而造成頻繁Full GC。

之前沒有優(yōu)化時,近2天GC的情況。

優(yōu)化前.jpg

優(yōu)化后,近2天GC的情況。

優(yōu)化后.jpg


后續(xù)觀察JVM狀況

優(yōu)化后.png
優(yōu)化前.png
沒有優(yōu)化phantomReference,JVM參數配置有誤
FGC 124  41.5s YGC  45K 9.8min

沒有優(yōu)化phantomReference, JVM參數配置無誤
FGC 15.4  10.2s YGC  24.7k  7.45min

優(yōu)化phantomReference,沒有優(yōu)化JVM參數
FGC  10  3.7s  YGC  34K  9.46min


優(yōu)化phantomReference, 優(yōu)化JVM參數
FGC  3.8  2.3s  YGC  41.8K  8.9min





參考文章


尾言

感謝各位大佬觀看。
如果您對這篇文章有什么意見或者錯誤需要改進的地方,歡迎與我討論。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容