前言
這篇文章最早是寫于2019-08-14,在公司的
confluence發(fā)布過。由于最近想標(biāo)記整理壓縮一些知識點(diǎn)碎片,于是便有了這篇文章的誕生。
項(xiàng)目介紹
- DS App
- DAU 百萬
- 注冊用戶近3000w
- DNU 幾十萬
線上排查工具介紹
jmap是java虛擬機(jī)自帶的一種內(nèi)存映像工具。jstat是JDK自帶的一個(gè)輕量級小工具。全稱Java Virtual Machine statistics monitoring tool,它位于java的bin目錄下,主要利用JVM內(nèi)建的指令對Java應(yīng)用程序的資源和性能進(jìn)行實(shí)時(shí)的命令行的監(jiān)控,包括了對Heap size和垃圾回收狀況的監(jiān)控。jstack是Jdk自帶的線程跟蹤工具,用于打印指定Java進(jìn)程的線程堆棧信息。
mat是一個(gè)檢查內(nèi)存泄漏的工具vjmap是唯品會(huì)開源出來的是用于排查內(nèi)存緩慢泄露,老生代增長過快原因的利器。因?yàn)?code>jmap -histo PID 打印的是整個(gè)Heap的對象統(tǒng)計(jì)信息,而為了定位上面的問題,我們需要專門查看OldGen對象,和Survivor區(qū)大齡對象的工具。(只支持CMS和ParallelGC,不支持G1)jvisualvm是JDK自帶的監(jiān)控程序。能夠監(jiān)控線程,內(nèi)存情況,查看方法的CPU時(shí)間和內(nèi)存中的對象,已被GC的對象,反向查看分配的堆棧jconsole是JDK自帶的監(jiān)控程序。用于對JVM中內(nèi)存,線程和類等的監(jiān)控。
線上排查使用到的命令
查看進(jìn)程使用gc情況: jstat -gc 16969<pid> 5000(打印時(shí)間間隔)
-
S0C:年輕代中第一個(gè)survivor(幸存區(qū))的容量 (KB) -
S1C:年輕代中第二個(gè)survivor(幸存區(qū))的容量 (KB) -
S0U:年輕代中第一個(gè)survivor(幸存區(qū))目前已使用空間 (KB) -
S1U:年輕代中第二個(gè)survivor(幸存區(qū))目前已使用空間 (KB) -
EC:年輕代中Eden(伊甸園)的容量 (KB) -
EU:年輕代中Eden(伊甸園)目前已使用空間 (KB) -
OC:Old代的容量 (KB) -
OU:Old代目前已使用空間 (KB) -
MC:元空間的容量 (KB) -
MU:元空間目前已使用空間 (KB) -
CCSC:壓縮類空間大小 -
CCSU:壓縮類空間使用大小 -
YGC:從應(yīng)用程序啟動(dòng)到采樣時(shí)年輕代中gc次數(shù) -
YGCT:從應(yīng)用程序啟動(dòng)到采樣時(shí)年輕代中gc所用時(shí)間(s) -
FGC:從應(yīng)用程序啟動(dòng)到采樣時(shí)old代(全gc)gc次數(shù) -
FGCT:從應(yīng)用程序啟動(dòng)到采樣時(shí)old代(全gc)gc所用時(shí)間(s) -
GCT:從應(yīng)用程序啟動(dòng)到采樣時(shí)gc用的總時(shí)間(s)
類加載統(tǒng)計(jì)
jstat -class 16969<pid>
java進(jìn)程堆棧信息打印
jstack 16969<pid>
查看pid對應(yīng)的線程使用CPU情況和內(nèi)存占用:
top -Hp pid
打印當(dāng)前java堆中各個(gè)對象的數(shù)量,大小一頁數(shù)據(jù)為50
jmap -histo pid | more -n 50
查看java堆中實(shí)例數(shù)數(shù)量前10的類
jmap -histo pid | sort -n -r -k 2 | head -10
查看java堆中容量前10的類
jmap -histo pid | sort -n -r -k 3 | head 10
查看整個(gè)JVM內(nèi)存狀態(tài)
jmap -heap <pid>
快照java堆信息到指定文件(會(huì)觸發(fā)Full GC, 如果快照文件過大,會(huì)使目標(biāo)進(jìn)程stop world)
jmap -dump:format=b,file=quMall.dump 16969<pid>
顯示堆中活躍對象的統(tǒng)計(jì)信息,比如實(shí)例數(shù)量,占用空間(會(huì)觸發(fā)Full GC)
jmap -histo:live <pid>
查看進(jìn)程使用的GC算法(實(shí)際上用到了javaagent技術(shù)來實(shí)現(xiàn)的:
jinfo -flags 23467<pid>
查看應(yīng)用運(yùn)行的時(shí)間
ps -p 11864<pid> -o etime
顯示垃圾回收的相關(guān)信息, 同時(shí)顯示最后一次或當(dāng)前正在發(fā)生的垃圾回收的誘因
jstat -gccause <pid> 5000<interval>
查看GC使用情況(比如s0區(qū)使用了多少):
jstat -gcutil 6011<pid>
查看JVM內(nèi)存中三代(young,old,metaspace)對象的使用和占用大小
jstat -gccapacity <pid>
查看metaspace中對象的信息及其占用量
jstat -gcmetacapacity<pid>
查看年輕代對象的信息
jstat -gcnew <pid>
查看年輕代對象的信息及其占用量
jstat -gcnewcapacity <pid>
查看old代對象的信息
jstat -gcold <pid>
查看old代對象的信息及其占用量
jstat -gcoldcapacity <pid>
查看進(jìn)程是否啟動(dòng)AdaptiveSizePolicy策略
jinfo -flag UseAdaptiveSizePolicy 26626<pid>
查看MetaSpace默認(rèn)初始化大小(默認(rèn)是20.8MB)
java -XX:+PrintFlagsInitial|grep Meta
打印整個(gè)堆中對象的統(tǒng)計(jì)信息,按對象的total size排序
./vjmap.sh -all PID > /tmp/histo.log
推薦,打印老年代的對象統(tǒng)計(jì)信息,按對象的oldgen size排序,比-all快很多,暫時(shí)只支持CMS
./vjmap.sh -old PID > /tmp/histo-old.log
推薦,打印Survivor區(qū)的對象統(tǒng)計(jì)信息,默認(rèn)age>=3
./vjmap.sh -sur PID > /tmp/histo-sur.log
推薦,打印Survivor區(qū)的對象統(tǒng)計(jì)信息,查看age>=4的對象
./vjmap.sh -sur:minage=4 PID > /tmp/histo-sur.log
推薦,打印Survivor區(qū)的對象統(tǒng)計(jì)信息,單獨(dú)查看age=4的對象
./vjmap.sh -sur:age=4 PID > /tmp/histo-sur.log
僅輸出存活的對象,原理為正式統(tǒng)計(jì)前先執(zhí)行一次full gc
./vjmap.sh -old:live PID > /tmp/histo-old-live.log
過濾對象大小,不顯示過小的對象。按對象的oldgen size進(jìn)行過濾,只打印OldGen占用超過1K的數(shù)據(jù)
./vjmap.sh -old:minsize=1024 PID > /tmp/histo-old.log
dump文件分析
1.快照目標(biāo)進(jìn)程的堆信息到執(zhí)行文件jmap -dump:format=b,file=mall.dump 16969<pid>。dump文件比較大,如果從生產(chǎn)服務(wù)器拉到本地,會(huì)觸發(fā)帶寬報(bào)警。如果要執(zhí)行該操作,請?jiān)陔娚萄邪l(fā)團(tuán)隊(duì)事先@所有人。
2.使用mat工具打開堆快照文件。(mat的基本操作可以看參考文章)
-
Histogram可以列出內(nèi)存中的對象,對象的個(gè)數(shù)以及大小 -
Dominator Tree可以列出處于活躍狀態(tài)的大對象。 -
Top consumers通過圖形列出最大的object。 -
Leak Suspects自動(dòng)分析泄漏的原因。

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


4.點(diǎn)開Actions->Histogram,模擬搜索JDBC。發(fā)現(xiàn)關(guān)于JDBC相關(guān)的實(shí)例數(shù)還挺多的。右鍵點(diǎn)擊com.mysql.jdbc.JDBC4Connection,選擇with outgoing references 查看JDBC4Connection實(shí)例具體的依賴關(guān)系。
-
with incoming references表示的是 當(dāng)前查看的對象,被外部應(yīng)用。 -
with outGoing references表示的是 當(dāng)前對象,引用了外部對象。



5.選中一個(gè)JDBC4Connection實(shí)例,右鍵選擇Merge Shortest Paths to GC roots -> with all references查看JDBC4Connection到GC roots的路徑。
-
Paths to GC: 從當(dāng)前對象到GC roots的路徑,這個(gè)路徑解釋了為什么當(dāng)前對象還能存活,對分析內(nèi)存泄露很有幫助,這個(gè)查詢只能針對單個(gè)對象使用。 -
Merge Shortest Paths to GC:從GC roots到一個(gè)或一組對象的公共路徑。 -
exclude all phantom/weak/soft etc.reference(排除所有虛弱軟引用) ->查看剩余未被回收的強(qiáng)引用對象占用原因 &GC Roots。
6.可見JDBC4Connection被2個(gè)對象引用,一個(gè)是BasicResourcePool中的formerResources(曾經(jīng)在連接池里呆過的對象)變量,一個(gè)是MySQL JDBC Driver的ConnectionPhantomReference

NonRegisteringDriver.java

這個(gè)虛引用是在JDBC Driver在構(gòu)造connection時(shí)用來track這個(gè)connection的,在被GC回收前做一些clean up(釋放資源)的事情,所以每個(gè)connection被構(gòu)造出來后,都會(huì)被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時(shí),
JVM會(huì)將PhantomReference對象扔到refqueue。然后會(huì)通過
AbandonedConnectionCleanupThread這個(gè)線程從NonRegisteringDriver.refQueue中拿到ConnectionPhantomReference,然后執(zhí)行cleanup方法,最后刪除connectionPhantomRefs這個(gè)ConcurrentHashMap中的ConnectionPhantomReference對象,完成connection相關(guān)資源的回收。
但是我們排查的結(jié)果是ConnectionPhantomReference并沒有得到清理。經(jīng)過查閱一些資料,得出以下結(jié)論:
似乎問題是CompressionInputStream'有對同一ConnectionImpl的引用,ConnectionPhantomReference正在等待在ReferenceQueue上排隊(duì),說它有資格進(jìn)行垃圾回收。ConnectionPhantomReference通過它引用的com.mysql.jdbc.NetworkResources對象對CompressionInputStream有一個(gè)強(qiáng)引用。ConnectionPhantomReference由NonRegisteringDriver中的靜態(tài)ConcurrentHashMap強(qiáng)烈保存。結(jié)果是ConnectionPhantomReference有效地引用了它要等待GC的JDBC4Connection對象; 并使JDBC4Connection實(shí)例保持活動(dòng)狀態(tài),因此不符合垃圾回收的條件。

所以在我們程序中要定時(shí)去清除ConnectionPhantomReference引用。在quMall項(xiàng)目中一臺(tái)服務(wù)器中Full GC清除7110個(gè)PhantomReference引用,耗時(shí)竟然達(dá)到0.6s。但是通過程序定時(shí)刪除這些引用,下一次Full GC清除這些引用耗時(shí)可以忽略不計(jì)。
/**
* @author xiaoma
* @version V1.0
* @Description: 解決PhantomReference引用過多,造成內(nèi)存泄漏
* 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計(jì)劃, 初始化失敗:{0}",
ex.getMessage()
));
}
}
/**
* 每秒10s清理一次
*/
@PostConstruct
public void clear() {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
LogService.info("清除PhantomReference計(jì)劃, 初始化成功");
synchronized (MysqlConnectionPhantomRefCleaner.class) {
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (!referenceMap.isEmpty()) {
int size = referenceMap.size();
LogService.info(MessageFormat.format(
"清除PhantomReference計(jì)劃, current Mysql ConnectionPhantomReference map size:{0} start to clear",
size
));
referenceMap.clear();
}
}
}, 1, 10, TimeUnit.SECONDS);
}
}
}

7.再來講講formerResources引用,我列出formerResources
相關(guān)代碼。
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這個(gè)方法是c3p0的一個(gè)定時(shí)器里執(zhí)行的方法,用來檢查過期連接的。c3p0會(huì)定時(shí)對idle連接進(jìn)行連接池過期檢查。若空閑時(shí)間超過MaxIdleTime,則會(huì)remove,會(huì)加入到formerResources中。如果最后剩下的idle連接數(shù)超過或者小于minIdle的連接數(shù),也會(huì)相應(yīng)的進(jìn)行縮減或者擴(kuò)容,直到minIdle個(gè)連接數(shù)。
idleConnectionTest是維持連接池的idle連接和MySQL之間的心跳,防止MySQL Server端踢掉應(yīng)用的連接,而前面提到的連接池過期檢查則是c3p0對連接歸還后是否長時(shí)間沒被再次借出為依據(jù)來判斷連接是否過期。
如果配置了maxIdleTime, 處于keep alived的連接會(huì)被認(rèn)為是過期連接。連接池將丟棄這些連接,并創(chuàng)建新連接。因此,NonRegisteringDriver將慢慢地保留越來越多的JDBC4Connection對象,并且您將慢慢地發(fā)生內(nèi)存泄漏。 這里也會(huì)造成頻繁的Young GC。
checkoutResource是每次獲取連接的時(shí)候,會(huì)對連接進(jìn)行過期檢查的校驗(yàn)。如果連接空閑時(shí)間超過MaxIdleTime,該連接會(huì)標(biāo)志已經(jīng)過期,
調(diào)用removeResource,將連接加入到formerResources中。配置了MaxIdeTime參數(shù),checkout操作會(huì)加快過期連接的失效和創(chuàng)建新連接的速度,導(dǎo)致formerResources里退休的連接變多,最終加快了老年代的增長和內(nèi)存泄漏。
正確的姿勢是去掉maxIdleTime配置,
配置了idleConnectionTestPeriod這個(gè)參數(shù)即可。
類似的問題還會(huì)存在JedisPoolConfig中,這些都可以歸結(jié)成持久化對象在JVM中的存活生命周期問題。
連接池對象,本地緩存對象都是,這些對象存活時(shí)間久,處于JVM的老生代中,應(yīng)用希望盡可能的重用它們,但若結(jié)合具體場景的配置或使用不合理,導(dǎo)致這些對象并未最大化被重用,比如上面提到的過期檢查導(dǎo)致不斷有新的對象被創(chuàng)建出來,因?yàn)槭浅志没瘜ο?,很容易就進(jìn)入到了老生代,霸占了資源。

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

我們只需配置minEvictableIdleTimeMills=-1,softMinEvictableIdleTimeMillis=60000就可以上述的問題。這樣就不會(huì)對minIdle的連接進(jìn)行清理,只有當(dāng)連接數(shù)超過minIdle后,才進(jìn)行清理工作。
JVM基礎(chǔ)參數(shù)
-XX:ParallelGCThreads=8
JVM在進(jìn)行并行GC的時(shí)候,用于GC的線程數(shù),一般是機(jī)器的核數(shù)。
-XX:+UseConcMarkSweepGC
打開此開關(guān)參數(shù)后,使用ParNew+CMS+Serial Old收集器組合進(jìn)行垃圾收集。Serial Old作為CMS收集器出現(xiàn)Concurrent Mode Failure的備用垃圾收集器。
-XX:+UseParallelGC
打開此開關(guān)參數(shù)后,使用Parallel Scavenge+Serial Old收集器組合進(jìn)行垃圾收集。
-XX:+UseParallelOldGC
打開此開關(guān)參數(shù)后,使用Parallel Scavenge+Parallel Old收集器組合進(jìn)行垃圾收集。
JDK1.8 默認(rèn)使用Parallel Scavenge年輕代回收器和Parallel Old老年代回收器, 默認(rèn)打開AdaptiveSizePolicy策略。CMS默認(rèn)關(guān)閉AdaptiveSizePolicy策略。在quMall項(xiàng)目中,沒有關(guān)閉AdaptiveSizePolicy策略,造成S0,S1區(qū)只有8MB,從而導(dǎo)致頻繁Young GC。所以要手動(dòng)設(shè)置使Eden:S0:S1 = 8 : 1 : 1
-XX:SurvivorRatio=8
-XX:-UseAdaptiveSizePolicy
AdaptiveSizePolicy為了達(dá)到三個(gè)預(yù)期目標(biāo),涉及以下操作:
- 如果
GC停頓時(shí)間超過了預(yù)期值,會(huì)減小內(nèi)存大小。理論上,減小內(nèi)存,可以減少垃圾標(biāo)記等操作的耗時(shí),以此達(dá)到預(yù)期停頓時(shí)間。 - 如果應(yīng)用吞吐量小于預(yù)期,會(huì)增加內(nèi)存大小。理論上,增大內(nèi)存,可以降低
GC的頻率,以此達(dá)到預(yù)期吞吐量。 - 如果應(yīng)用達(dá)到了前兩個(gè)目標(biāo),則嘗試減小內(nèi)存,以減少內(nèi)存消耗。
引用R大說過的話:
HotSpot VM里,ParallelScavenge系的GC(UseParallelGC,UseParallelOldGC)默認(rèn)行為是SurvivorRatio如果不顯式設(shè)置就沒啥用。顯式設(shè)置到跟默認(rèn)值一樣的值則會(huì)有效果。因?yàn)?code>ParallelScavenge系的GC最初設(shè)計(jì)就是默認(rèn)打開AdaptiveSizePolicy的,它會(huì)自動(dòng)、自適應(yīng)的調(diào)整各種參數(shù)
在這里說下默認(rèn)的ParallelScavenge 和 ParallelOld垃圾回收器:
-
Paralle Scavenge(年輕代):- 新生代收集器,可以和
Serial Old、Parallel組合使用,不能和CMS組合使用。采用復(fù)制算法。使用多線程進(jìn)行垃圾回收,回收時(shí)會(huì)導(dǎo)致Stop The World。關(guān)注系統(tǒng)吞吐量。 -
-XX:MaxGCPauseMillis:設(shè)置大于0的毫秒數(shù),收集器盡可能在該時(shí)間內(nèi)完成垃圾回收。 -
-XX:GCTimeRatio:大于0小于100的整數(shù),即垃圾回收時(shí)間占總時(shí)間的比率,設(shè)置越小則希望垃圾回收所占時(shí)間越小,CPU能花更多的時(shí)間進(jìn)行系統(tǒng)操作,提高吞吐量。也就是垃圾收集時(shí)間占總時(shí)間的比率,相當(dāng)于是吞吐量的倒數(shù)。如果把此參數(shù)設(shè)置為19,那允許的最大GC時(shí)間就占總時(shí)間的5%(即1/(1+19)),默認(rèn)值為99,就是允許最大1%(即1/(1+99))的垃圾收集時(shí)間。 -
GC日志關(guān)鍵字:PSYoungGen
- 新生代收集器,可以和
-
Parallel Old(年老代)- 年老代收集器,只能和
Parallel Scavenge組合使用(Parallel Scavenge收集器的年老代版本。采用標(biāo)記-整理算法,會(huì)對垃圾回收導(dǎo)致的內(nèi)存碎片進(jìn)行整理關(guān)注吞吐量的系統(tǒng)可以將Parallel Scavenge+Parallel Old組合使用。 -
GC日志關(guān)鍵字:ParOldGen
- 年老代收集器,只能和
對于重載了 Object 類的 finalize 方法的類實(shí)例化的對象(這里稱為 f 對象),JVM 為了能在GC 對象時(shí)觸發(fā)f對象的finalize 方法的調(diào)用,將每個(gè)f對象包裝生成一個(gè)對應(yīng)的FinalReference 對象,方便 GC 時(shí)進(jìn)行處理。
SocksSocketImpl中重載了finalize方法,防止Socket 連接忘記關(guān)閉導(dǎo)致資源泄漏而進(jìn)行的保底措施。

對于RPC調(diào)用短連接場景,每調(diào)用一次就會(huì)創(chuàng)建一個(gè)Socket 對象。致使 FinalReference 對象非常多, 因此YoungGC 耗時(shí)增加。
下面參數(shù), 可以在 GC 的時(shí)候多線程并行處理Reference,降低GC時(shí)長。
-XX:+ParallelRefProcEnabled
開啟壓縮對象指針(默認(rèn)開啟),啟用CompressOops后,會(huì)壓縮的對象:
? 每個(gè)Class的屬性指針(靜態(tài)成員變量)
? 每個(gè)對象的屬性指針
? 普通對象數(shù)組的每個(gè)元素指針。
-XX:+UseCompressedOops
- 擴(kuò)展知識:
HotSpot虛擬機(jī)中,對象在內(nèi)存中存儲(chǔ)的布局,可以分為三塊區(qū)域:對象頭(header),實(shí)例數(shù)據(jù)(Instance Data),對象填充(Padding)。對象頭包括markword和類型指針(class對象指針)。如果是數(shù)組,還包括數(shù)組長度。實(shí)例數(shù)據(jù):對象實(shí)際的數(shù)據(jù)。
對象填充:對齊,按
8字節(jié)對齊。Java內(nèi)存地址按照8字節(jié)對齊,長度必須是8的倍數(shù)。
markword:用于存儲(chǔ)對象自身運(yùn)行時(shí)的數(shù)據(jù),如哈希碼,GC分代年齡,鎖狀態(tài)標(biāo)志,線程持有的鎖,偏向線程ID,偏向時(shí)間戳。在
32位JVM中,markword是32bit,類型指針是32bit,數(shù)組長度是32bit。從而得知一個(gè)普通對象頭是8個(gè)字節(jié),一個(gè)數(shù)組對象頭是16個(gè)字節(jié)。在
64位JVM中,markword是64bit,類型指針是64bit,開啟指針壓縮時(shí)是32bit,數(shù)組長度是64bit,開啟壓縮時(shí)是32bit。從而得知一個(gè)無壓縮的普通對象頭是16個(gè)字節(jié),一個(gè)開啟壓縮的普通對象頭是12個(gè)字節(jié)。一個(gè)無壓縮的數(shù)組對象頭是24個(gè)字節(jié),一個(gè)開啟壓縮的數(shù)組對象頭是16個(gè)字節(jié)。對象頭在32位系統(tǒng)占用8字節(jié),而在64位系統(tǒng)上占用16字節(jié)。-
Referece類型在32位系統(tǒng)上每個(gè)占用4字節(jié),而在64位系統(tǒng)上每個(gè)占用8字節(jié)。
Image [13].png
在Metaspace中開辟出一塊類指針壓縮空間(Compressed Class Space),默認(rèn)是1G(默認(rèn)開啟)。對象中指向類元數(shù)據(jù)的指針會(huì)被壓縮成32位。
-XX:+UseCompressedClassPointers
-XX:CompressedClassSpaceSize=1G
使用CMS時(shí),打開對年老代的壓縮??赡軙?huì)影響性能,但是可以消除碎片。
-XX:+UseCMSCompactAtFullCollection
使用CMS,設(shè)置多少次FullGc后,對年老代進(jìn)行壓縮。
由于CMS不對內(nèi)存空間進(jìn)行壓縮、整理、所以運(yùn)行一段時(shí)間以后會(huì)產(chǎn)生“碎片”,使得運(yùn)行效率降低。此值設(shè)置運(yùn)行多少次GC以后對內(nèi)存空間進(jìn)行壓縮、整理。
-XX:CMSFullGCsBeforeCompaction=0
這兩個(gè)設(shè)置一般配合使用,一般用于『降低CMS GC頻率或者增加頻率、減少GC時(shí)長』的需求
-XX:CMSInitiatingOccupancyFraction=70 是指設(shè)定CMS在對內(nèi)存占用率達(dá)到70%的時(shí)候開始GC(因?yàn)?code>CMS會(huì)有浮動(dòng)垃圾,所以一般都較早啟動(dòng)GC);
-XX:+UseCMSInitiatingOccupancyOnly 只是用設(shè)定的回收閾值(上面指定的70%),如果不指定,JVM僅在第一次使用設(shè)定值,后續(xù)則自動(dòng)調(diào)整。
CMS整個(gè)過程分為4步:
- 初始標(biāo)記(
CMS initial mark) - 并發(fā)標(biāo)記(
CMS concurrent mark) - 重新標(biāo)記(
CMS remark) - 并發(fā)清除(
CMS concurrent sweep)。在這個(gè)階段會(huì)出現(xiàn)浮動(dòng)垃圾。CMS收集器無法處理浮動(dòng)垃圾,可能出現(xiàn)Concurrent Mode Failure失敗而導(dǎo)致另一次Full GC的產(chǎn)生。由于在垃圾收集階段用戶線程還需要運(yùn)行,即還需要預(yù)留足夠的內(nèi)存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。
在CMS GC前啟動(dòng)一次ygc,目的在于減少old gen對ygc gen的引用,降低remark時(shí)的開銷, 一般CMS的GC耗時(shí)80%都在remark階段。
-XX:+CMSScavengeBeforeRemark
在進(jìn)行GC的前后打印出堆的信息
-XX:+PrintHeapAtGC
輸出GC的詳細(xì)日志
-XX:+PrintGCDetails
輸出GC的時(shí)間戳(以基準(zhǔn)時(shí)間的形式)
-XX:+PrintGCDateStamps
打印GC時(shí)各種引用的處理時(shí)間。
-XX:+PrintReferenceGC
輸出GC日志
-XX:+PrintGC
打開了就知道是多大的新生代對象晉升到老生代失敗從而引發(fā)Full GC時(shí)的。
-XX:+PrintPromotionFailure
輸出顯示在survivor空間里面有效的對象的歲數(shù)情況。
-XX:+PrintTenuringDistribution
打印產(chǎn)生GC的原因,比如AllocationFailure什么的,在JDK8已默認(rèn)打開,JDK7要顯式打開一下。
-XX:+PrintGCCause
GC日志存放的位置。
-Xloggc:logs/quMall_gc.log"
如果達(dá)到初始化值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載,同時(shí)GC會(huì)對該值進(jìn)行調(diào)整:如果釋放了大量的空間,就適當(dāng)降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時(shí),適當(dāng)提高該值。默認(rèn)值是20.8MB。如果不手動(dòng)指定元空間的大小,會(huì)因?yàn)樵臻g的擴(kuò)容機(jī)制,造成頻繁的Full GC。
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
默認(rèn)(MinHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存小于40%時(shí),JVM就會(huì)增大堆直到-Xmx的最大限制。
默認(rèn)(MaxHeapFreeRatio參數(shù)可以調(diào)整)空余堆內(nèi)存大于70%時(shí),JVM會(huì)減少堆直到 -Xms的最小限制。
注意:java應(yīng)用的jvm參數(shù)Xms與Xmx保持一致,避免因所使用的Java堆內(nèi)存不夠?qū)е骂l繁full gc以及full gc中因動(dòng)態(tài)調(diào)節(jié)Java堆大小而耗費(fèi)延長其周期。
-XX:MinHeapFreeRatio=40
-XX:MaxHeapFreeRatio=70
-Xms3072m
-Xmx3072m
如果沒有配置-XX:+DisableExplicitGC,即沒有屏蔽System.gc()觸發(fā)FullGC,那么可以通過排查GC日志中有System字樣判斷是否System.gc()觸發(fā)。
注意:如果應(yīng)用中有用到Netty并且配置了該參數(shù)會(huì)內(nèi)存溢出。
Netty中的DirectByteBuffer分配空間過程中發(fā)現(xiàn)直接內(nèi)存不足時(shí)會(huì)顯式調(diào)用System.gc(),以期通過Full GC來強(qiáng)迫已經(jīng)無用的DirectByteBuffer對象釋放掉它們關(guān)聯(lián)的native memory。
-XX:+DisableExplicitGC
GC日志分析
之前看到電商模塊中的一臺(tái)服務(wù)器實(shí)例頻繁Full GC和YGC,感覺很不正常,以下是排查定位問題的步驟。

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

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


因?yàn)槭褂玫氖?code>Parallel Scavenge、Parallel Old垃圾回收器,會(huì)啟動(dòng)AdaptiveSizePolicy策略。當(dāng)這個(gè)參數(shù)打開之后,就不需要手工指定新生代的大?。?code>-Xmn)、Eden與Survivor區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對象大小門檻(-XX:PretenureSizeThreshold)等細(xì)節(jié)參數(shù)了,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或最大的吞吐量,這種調(diào)節(jié)方式稱為GC自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics)。
所以我們要禁用這個(gè)參數(shù)且將Eden:S0:S1設(shè)置為8:1:1
-XX:SurvivorRatio=8
-XX:-UseAdaptiveSizePolicy
設(shè)置完這個(gè)參數(shù)后,再查看GC使用情況后,發(fā)現(xiàn)S0和S1區(qū)容量變大,S0區(qū)占用比也急劇減少,從而YGC減少。


3.查看GC日志,發(fā)現(xiàn)造成頻繁Full GC的原因是Metadata GC Threshold。這由于沒有設(shè)置Metaspace參數(shù),導(dǎo)致初始化大小為20.8MB的元空間不夠用,頻繁擴(kuò)容導(dǎo)致的Full GC。
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m

- 如果使用
jmap -histo:live <pid>和jmap -dump:format=b,file=quMall.dump 16969<pid>會(huì)造成Full GC。

5.可以看到Full GC時(shí)清除7100個(gè)PhantomReference引用耗時(shí)長達(dá)0.6s。根據(jù)上面的分析,我們應(yīng)該定時(shí)清除PhantomReference引用。同時(shí)設(shè)置并行處理引用。優(yōu)化后的Full GC清除PhantomReference引用耗時(shí)0.0000805s。
-XX:+ParallelRefProcEnabled

6.另外需要注意的是-Xms和 -Xmx應(yīng)該設(shè)置成一樣大,避免在每次GC 后調(diào)整堆的大小,從而造成頻繁Full GC。
之前沒有優(yōu)化時(shí),近2天GC的情況。

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

后續(xù)觀察JVM狀況


沒有優(yōu)化phantomReference,JVM參數(shù)配置有誤
FGC 124 41.5s YGC 45K 9.8min
沒有優(yōu)化phantomReference, JVM參數(shù)配置無誤
FGC 15.4 10.2s YGC 24.7k 7.45min
優(yōu)化phantomReference,沒有優(yōu)化JVM參數(shù)
FGC 10 3.7s YGC 34K 9.46min
優(yōu)化phantomReference, 優(yōu)化JVM參數(shù)
FGC 3.8 2.3s YGC 41.8K 8.9min
參考文章
-
小白系列:
- java高分局之jstat命令使用
- jstack(查看線程)、jmap(查看內(nèi)存)和jstat(性能分析)
- 通過jstack與jmap分析一次線上故障
- Java常用分析工具之jmap
- 使用Memory Analyzer tool(MAT)分析內(nèi)存泄漏
- 利用內(nèi)存分析工具(Memory Analyzer Tool,MAT)分析java項(xiàng)目內(nèi)存泄露
- allow heap & Retained heap
- 解決com.mysql.jdbc.NonRegisteringDriver的內(nèi)存泄漏
- Java程序內(nèi)存分析:使用mat工具分析內(nèi)存占用
- 內(nèi)存泄露排查原因及解決方法——內(nèi)存優(yōu)化(五)
- GC root & 使用MAT分析java堆
- 使用MAT解決OOM的一次實(shí)戰(zhàn)經(jīng)歷
- 使用Eclipse Memory Analyzer Tool(MAT)分析線上故障(一) - 視圖&功能篇
- c3p0的重連機(jī)制
- 唯品會(huì)vjmap使用
- -XX:+PrintGCTimeStamps -XX:+PrintGCDetails 日志分析
- JVM之幾種垃圾收集器簡單介紹
- Java 中的 Reference介紹
- 初步診斷你的GC
-
大佬系列:
尾言
感謝各位大佬觀看。
如果您對這篇文章有什么意見或者錯(cuò)誤需要改進(jìn)的地方,歡迎與我討論。
