近期公司公網(wǎng)接口被頻發(fā)攻擊刷垃圾數(shù)據(jù),某些不常用接口一晚上被刷了幾十萬次,此背景下接口項(xiàng)目頻繁出現(xiàn)OOM的情況,主要表現(xiàn)如下圖:

如圖所示,每次fgc都無法回收內(nèi)存,很明顯項(xiàng)目代碼中有內(nèi)存泄漏的情況存在,只能重啟項(xiàng)目臨時(shí)救急。隨后看近期代碼變動(dòng)記錄也未發(fā)現(xiàn)明顯問題,只得讓運(yùn)維協(xié)助導(dǎo)出內(nèi)存dump來分析具體原因了。
分析問題前先明確下內(nèi)存異常的概念:
內(nèi)存溢出 out of memory,是指程序在申請(qǐng)內(nèi)存時(shí),沒有足夠的內(nèi)存空間供其使用,出現(xiàn)out of memory;
內(nèi)存泄露 memory leak,是指程序在申請(qǐng)內(nèi)存后,無法釋放已申請(qǐng)的內(nèi)存空間,一次內(nèi)存泄露危害可以忽略,但內(nèi)存泄露堆積后果很嚴(yán)重,無論多少內(nèi)存,遲早會(huì)被占光。
memory leak最終會(huì)導(dǎo)致out of memory!
第一次排查
第一次導(dǎo)出使用的命令是jmap -dump:format=b,file=heap.bin <pid>。沒有選在內(nèi)存占用最高的時(shí)候?qū)С?,所以文件只?G多大小,這不會(huì)影響實(shí)例占用內(nèi)存的比例情況因?yàn)閷?duì)象占用是逐漸遞增成比例的。不過這個(gè)命令有個(gè)問題就是導(dǎo)出的內(nèi)存不是fullgc以后的內(nèi)存情況,因?yàn)閮?nèi)存溢出的出現(xiàn)是fullgc后無法回收的那些對(duì)象有問題,當(dāng)時(shí)心知這個(gè)命令會(huì)有問題但是當(dāng)時(shí)文件已經(jīng)導(dǎo)出不想太麻煩運(yùn)維再導(dǎo)一次也就沒有太在意,事實(shí)證明嫌麻煩的后果就是事情會(huì)變得更麻煩了。
-
拿到dump文件后使用jdk中自帶的jvisualvm進(jìn)行分析,單看內(nèi)存占用情況如下:
內(nèi)存類信息.png -
內(nèi)存中hashMap對(duì)象實(shí)例數(shù)最多且占用內(nèi)存也是最多的,我們雙擊map對(duì)象查看詳情,一般這種實(shí)例數(shù)較多的我們很容易就能找到問題對(duì)象,如下圖所示,通過查看多個(gè)對(duì)象實(shí)例發(fā)現(xiàn)key是X509CertImpl的Map對(duì)象有很多,我們點(diǎn)擊查看垃圾回收根結(jié)點(diǎn)查看
最近的垃圾回收節(jié)點(diǎn).png -
查看根結(jié)點(diǎn)后會(huì)把對(duì)象調(diào)用路徑完整的展示出來,我們把根路徑復(fù)制出來看看具體是哪些調(diào)用。如下圖點(diǎn)擊復(fù)制從根開始的路徑
復(fù)制從根開始的路徑.png
- 最后得到如下的一個(gè)完整路徑,發(fā)現(xiàn)是阿里云 oss的引用,初步發(fā)現(xiàn)問題后就結(jié)合項(xiàng)目代碼一起比對(duì)檢查。
this - value: java.util.HashMap$Node #335002
<- [22] - class: java.util.HashMap$Node[], value: java.util.HashMap$Node #335002
<- table - class: java.util.HashMap, value: java.util.HashMap$Node[] #28817
<- map - class: java.util.HashSet, value: java.util.HashMap #32007
<- trustedCerts - class: sun.security.ssl.X509TrustManagerImpl, value: java.util.HashSet #12745
<- trustManager - class: org.apache.http.ssl.SSLContextBuilder$TrustManagerDelegate, value: sun.security.ssl.X509TrustManagerImpl #2346
<- tm - class: sun.security.ssl.AbstractTrustManagerWrapper, value: org.apache.http.ssl.SSLContextBuilder$TrustManagerDelegate #2357
<- trustManager - class: sun.security.ssl.SSLContextImpl$TLSContext, value: sun.security.ssl.AbstractTrustManagerWrapper #2358
<- context - class: sun.security.ssl.SSLSocketFactoryImpl, value: sun.security.ssl.SSLContextImpl$TLSContext #2343
<- socketfactory - class: org.apache.http.conn.ssl.SSLConnectionSocketFactory, value: sun.security.ssl.SSLSocketFactoryImpl #2363
<- val - class: java.util.concurrent.ConcurrentHashMap$Node, value: org.apache.http.conn.ssl.SSLConnectionSocketFactory #2345
<- [11] - class: java.util.concurrent.ConcurrentHashMap$Node[], value: java.util.concurrent.ConcurrentHashMap$Node #162146
<- table - class: java.util.concurrent.ConcurrentHashMap, value: java.util.concurrent.ConcurrentHashMap$Node[] #13097
<- map - class: org.apache.http.config.Registry, value: java.util.concurrent.ConcurrentHashMap #22642
<- socketFactoryRegistry - class: org.apache.http.impl.conn.DefaultHttpClientConnectionOperator, value: org.apache.http.config.Registry #7070
<- connectionOperator - class: org.apache.http.impl.conn.PoolingHttpClientConnectionManager, value: org.apache.http.impl.conn.DefaultHttpClientConnectionOperator #2345
<- [2073] - class: java.lang.Object[], value: org.apache.http.impl.conn.PoolingHttpClientConnectionManager #2342
<- elementData - class: java.util.ArrayList, value: java.lang.Object[] #156896
<- connectionManagers - class: com.aliyun.oss.common.comm.IdleConnectionReaper, value: java.util.ArrayList #74889
<- <class> (thread object) - class: com.aliyun.oss.common.comm.IdleConnectionReaper, value: com.aliyun.oss.common.comm.IdleConnectionReaper class IdleConnectionReaper
- 排查代碼發(fā)現(xiàn)項(xiàng)目中上傳頭像的功能使用到了oss,代碼如下
public static boolean fileUploadToOSS(InputStream uploadFilePath, String objectKey){
URL url = null;
// 使用默認(rèn)的OSS服務(wù)器地址創(chuàng)建OSSClient對(duì)象,不叫OSS_ENDPOINT代表使用杭州節(jié)點(diǎn),青島節(jié)點(diǎn)要加上不然包異常
OSSClient client = new OSSClient(OSSConfigure.getInstance().getOssEndPoint()
, OSSConfigure.getInstance().getAccessId(), OSSConfigure.getInstance().getAccessKey());
try {
ObjectMetadata objectMeta = new ObjectMetadata();
objectMeta.setCacheControl("no-cache");
objectMeta.setHeader("Pragma", "no-cache");
objectMeta.setContentEncoding("utf-8");
objectMeta.setContentType("image/png");
client.putObject(bucketName, Objectkey, file, objectMeta);
return true;
} catch (FileNotFoundException e) {
log.error("文件圖片OSS上傳失??!", e);
}
return false;
}
- 代碼中發(fā)現(xiàn)每次上傳頭像都會(huì)new一個(gè)新的OssClient,而且圖片上傳完之后還沒有手動(dòng)關(guān)閉鏈接,并且圖片流也沒有進(jìn)行關(guān)閉,另一個(gè)關(guān)鍵點(diǎn)是導(dǎo)出這個(gè)dump那段時(shí)間上傳頭像的接口正在被刷,OssClient的對(duì)象創(chuàng)建了很多并且沒有關(guān)閉,種種跡象告訴我這肯定是問題的根源。
正確的使用方式參考:https://bbs.aliyun.com/simple/t257085.html,
其他人也有同樣的問題:https://blog.csdn.net/ashur619/article/details/82835662,這個(gè)情況和我們極相似。
最后修改完代碼并上線,自此我天真的以為問題已經(jīng)得到了徹底解決,但是沒有過幾天同樣的問題又出現(xiàn)了,當(dāng)時(shí)感覺滿腦子問號(hào)???.png
沒辦法問題來了就接著找吧 !
第二次排查
-
因?yàn)橹笆褂玫膉map命令有問題這次加個(gè)histo:live參數(shù),完整命令jmap -dump:format=b,live,file=heap.bin <pid>,這個(gè)命令會(huì)執(zhí)行一次fgc,并導(dǎo)出gc后的內(nèi)存情況,并且下載了mat來更清晰的解析dump文件。首次打開文件mat會(huì)生成自己的泄漏建議報(bào)告,我們可以打開看一下它提供的分析報(bào)告,選Leak Suspects Report點(diǎn)finish即可,分析后生成圖表和問題報(bào)告,problem a幾乎把1.6的內(nèi)存快占滿了,并且也準(zhǔn)確指出了問題出現(xiàn)在哪
The memory is accumulated in one instance of com.alibaba.fastjson.util.IdentityHashMap$Entry[]
report.png -
點(diǎn)擊details進(jìn)去后查看具體情況,我們發(fā)現(xiàn)對(duì)象ParseConfig占用了93%的內(nèi)存空間,而ParseConfig下引用了N多個(gè)fastjson自定義的IdentityHashMap
image.png -
我們?cè)倏聪聝?nèi)存空間中整體的對(duì)象實(shí)例情況,到overview頁面點(diǎn)histogram查看實(shí)例直方圖如下:
image.png -
ParserConfig只有一個(gè)實(shí)例 ,我們結(jié)合fastjson源碼看下這個(gè)ParserConfig和IdentityHashMap的具體關(guān)系
image.png -
ParseConfig是一個(gè)單例,IdentityHashMap是用來存放json對(duì)象反序列化器的,并且key是一個(gè)反射類Type類型,我們?cè)倏聪聻槭裁磿?huì)存放這么多的反序列化器呢,因?yàn)樵瓌t上一種類型的對(duì)象(一種Type對(duì)象)只需要一個(gè)反序列化器就可以了。下面我們看下這個(gè)deserializers里都存了哪些對(duì)象,點(diǎn)擊ParserConfig對(duì)象java Basics Open In Dominator Tree,查看依賴樹
image.png -
打開后如下,分析發(fā)現(xiàn)IdentityHashMap的key是一個(gè)gson的對(duì)象,而需要反序列化的類是我們項(xiàng)目中自定義的,我又打開了其他的IdentityHashMap發(fā)現(xiàn)里面的結(jié)構(gòu)和反序列化對(duì)象都是一樣的,同對(duì)象為什么會(huì)生成如此多個(gè)反序列化器呢,Map存了這么多對(duì)象說明map的key都是不一樣的,因?yàn)閙ap的key是不能重復(fù)的,IdentityHashMap同樣也是,再查看了這些IdentityHashMap的key發(fā)現(xiàn)每個(gè)key的類型都是ParameterizedTypeImpl,但是內(nèi)存地址值都是不一樣的,說明key每次都是new的一個(gè)新ParameterizedTypeImpl對(duì)象。
image.png 再看了下IdentityHashMap的put方法,key是直接拿對(duì)象的hash值做運(yùn)算并存放到buckets指定坐標(biāo)上的,如此我們可以確定這跟key的生成是有關(guān)系的,我們結(jié)合上面查到的RespVipBaseDTO再看下key對(duì)象到底是怎么生成的,代碼中JSON.parseObject()的時(shí)候會(huì)到ParserConfig中按key尋找對(duì)應(yīng)的反序列化器,沒有的話就會(huì)新創(chuàng)建一個(gè)新的。
RespBaseDTO<RecommendContentDTO> result =
JSON.parseObject(resultString, new TypeToken<RespVipBaseDTO<RecommendContentDTO>>() {}.getType());
- 如上代碼會(huì)用new TypeToken(){}.getType()當(dāng)作key去查找合適的反序列化器,那我們看下這個(gè)key是怎么生成的。這里只貼出創(chuàng)建時(shí)的代碼,如下代碼,我們發(fā)現(xiàn)ParameterizedTypeImpl都是通過new創(chuàng)建的,說明每次使用TypeToken時(shí)都會(huì)創(chuàng)建一個(gè)新的,這也是為什么IdentityHashMap越來越多的根本原因。
if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType) type;
return new ParameterizedTypeImpl(p.getOwnerType(), p.getRawType(), p.getActualTypeArguments());
}
- 其實(shí)原因還是開發(fā)人員在使用json時(shí)串用了不同廠商的json工具,TypeToken是谷歌gson的類,使用的JSONObject卻是阿里fastjson的,正確的使用方法應(yīng)該是使用TypeReference,TypeReference.getType()時(shí)同樣會(huì)返回一個(gè)Type類型,但是這個(gè)Type類型是通過java反射獲取到的類的基本信息,以此Type作為key是不會(huì)有這樣的問題的:
BaseReqDTO<CommonReqDTO> baseReqDTO = parseRequest(request, new TypeReference<BaseReqDTO<CommonReqDTO>>() {}.getType());
- 可以簡單寫一些驗(yàn)證代碼做個(gè)簡單的驗(yàn)證,運(yùn)行后發(fā)現(xiàn)使用TypeToken創(chuàng)建Type會(huì)造成程序運(yùn)行緩慢并最終造成OOM異常,TypeToken換成TypeReference后沒有此問題出現(xiàn):
for(int i = 0;i < 1000000; i++){
JSON.parseObject("\"userId\":\"213\"",
new TypeToken<RespVipBaseDTO<OqsChoicenessProducts>>() {
}.getType());
MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean();
MemoryUsage usage = memorymbean.getHeapMemoryUsage();
if(i % 1000 == 0){
System.out.println("第:" + i + "次");
System.out.println("INIT HEAP: " + usage.getInit());
System.out.println("MAX HEAP: " + usage.getMax());
System.out.println("USE HEAP: " + usage.getUsed());
}
if(i% 50000 == 0){
System.gc();
System.out.println("第:" + i + "次");
System.out.println("INIT HEAP: " + usage.getInit());
System.out.println("MAX HEAP: " + usage.getMax());
System.out.println("USE HEAP: " + usage.getUsed());
}
}
總結(jié):
- 發(fā)現(xiàn)問題可以先看代碼最近是否有大的變動(dòng),有沒有使用不合理的地方,結(jié)合系統(tǒng)最近情況作出判斷,沒發(fā)現(xiàn)明顯問題就果斷導(dǎo)dump文件開始做分析。
- 這次排查中看代碼的時(shí)間要比看dump分析的時(shí)間多得多,要單從分析工具中找到具體問題是不可能的,或者是因?yàn)槲疫€沒掌握到更深的使用方法。
- 代碼質(zhì)量問題亟待解決









