近期公司公網(wǎng)接口被頻發(fā)攻擊刷垃圾數(shù)據(jù),某些不常用接口一晚上被刷了幾十萬次,此背景下接口項目頻繁出現(xiàn)OOM的情況搀别,主要表現(xiàn)如下圖:
如圖所示怕享,每次fgc都無法回收內(nèi)存执赡,很明顯項目代碼中有內(nèi)存泄漏的情況存在,只能重啟項目臨時救急函筋。隨后看近期代碼變動記錄也未發(fā)現(xiàn)明顯問題沙合,只得讓運維協(xié)助導出內(nèi)存dump來分析具體原因了。
分析問題前先明確下內(nèi)存異常的概念:
內(nèi)存溢出 out of memory跌帐,是指程序在申請內(nèi)存時首懈,沒有足夠的內(nèi)存空間供其使用,出現(xiàn)out of memory谨敛;
內(nèi)存泄露 memory leak究履,是指程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間脸狸,一次內(nèi)存泄露危害可以忽略最仑,但內(nèi)存泄露堆積后果很嚴重,無論多少內(nèi)存,遲早會被占光炊甲。
memory leak最終會導致out of memory盯仪!
第一次排查
第一次導出使用的命令是jmap -dump:format=b,file=heap.bin <pid>。沒有選在內(nèi)存占用最高的時候?qū)С雒鄞校晕募挥?G多大小全景,這不會影響實例占用內(nèi)存的比例情況因為對象占用是逐漸遞增成比例的。不過這個命令有個問題就是導出的內(nèi)存不是fullgc以后的內(nèi)存情況牵囤,因為內(nèi)存溢出的出現(xiàn)是fullgc后無法回收的那些對象有問題爸黄,當時心知這個命令會有問題但是當時文件已經(jīng)導出不想太麻煩運維再導一次也就沒有太在意滞伟,事實證明嫌麻煩的后果就是事情會變得更麻煩了。
-
拿到dump文件后使用jdk中自帶的jvisualvm進行分析炕贵,單看內(nèi)存占用情況如下:
內(nèi)存類信息.png -
內(nèi)存中hashMap對象實例數(shù)最多且占用內(nèi)存也是最多的梆奈,我們雙擊map對象查看詳情,一般這種實例數(shù)較多的我們很容易就能找到問題對象称开,如下圖所示亩钟,通過查看多個對象實例發(fā)現(xiàn)key是X509CertImpl的Map對象有很多,我們點擊查看垃圾回收根結點查看
最近的垃圾回收節(jié)點.png -
查看根結點后會把對象調(diào)用路徑完整的展示出來鳖轰,我們把根路徑復制出來看看具體是哪些調(diào)用清酥。如下圖點擊復制從根開始的路徑
復制從根開始的路徑.png
- 最后得到如下的一個完整路徑,發(fā)現(xiàn)是阿里云 oss的引用蕴侣,初步發(fā)現(xiàn)問題后就結合項目代碼一起比對檢查焰轻。
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)項目中上傳頭像的功能使用到了oss,代碼如下
public static boolean fileUploadToOSS(InputStream uploadFilePath, String objectKey){
URL url = null;
// 使用默認的OSS服務器地址創(chuàng)建OSSClient對象,不叫OSS_ENDPOINT代表使用杭州節(jié)點昆雀,青島節(jié)點要加上不然包異常
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)每次上傳頭像都會new一個新的OssClient狞膘,而且圖片上傳完之后還沒有手動關閉鏈接揩懒,并且圖片流也沒有進行關閉,另一個關鍵點是導出這個dump那段時間上傳頭像的接口正在被刷挽封,OssClient的對象創(chuàng)建了很多并且沒有關閉旭从,種種跡象告訴我這肯定是問題的根源。
正確的使用方式參考:https://bbs.aliyun.com/simple/t257085.html场仲,
其他人也有同樣的問題:https://blog.csdn.net/ashur619/article/details/82835662和悦,這個情況和我們極相似。
最后修改完代碼并上線渠缕,自此我天真的以為問題已經(jīng)得到了徹底解決鸽素,但是沒有過幾天同樣的問題又出現(xiàn)了,當時感覺滿腦子問號???.png
沒辦法問題來了就接著找吧 !
第二次排查
-
因為之前使用的jmap命令有問題這次加個histo:live參數(shù)亦鳞,完整命令jmap -dump:format=b,live,file=heap.bin <pid>,這個命令會執(zhí)行一次fgc馍忽,并導出gc后的內(nèi)存情況,并且下載了mat來更清晰的解析dump文件燕差。首次打開文件mat會生成自己的泄漏建議報告遭笋,我們可以打開看一下它提供的分析報告,選Leak Suspects Report點finish即可徒探,分析后生成圖表和問題報告瓦呼,problem a幾乎把1.6的內(nèi)存快占滿了,并且也準確指出了問題出現(xiàn)在哪
The memory is accumulated in one instance of com.alibaba.fastjson.util.IdentityHashMap$Entry[]
report.png -
點擊details進去后查看具體情況测暗,我們發(fā)現(xiàn)對象ParseConfig占用了93%的內(nèi)存空間央串,而ParseConfig下引用了N多個fastjson自定義的IdentityHashMap
image.png -
我們再看下內(nèi)存空間中整體的對象實例情況磨澡,到overview頁面點histogram查看實例直方圖如下:
image.png -
ParserConfig只有一個實例 ,我們結合fastjson源碼看下這個ParserConfig和IdentityHashMap的具體關系
image.png -
ParseConfig是一個單例质和,IdentityHashMap是用來存放json對象反序列化器的稳摄,并且key是一個反射類Type類型,我們再看下為什么會存放這么多的反序列化器呢饲宿,因為原則上一種類型的對象(一種Type對象)只需要一個反序列化器就可以了厦酬。下面我們看下這個deserializers里都存了哪些對象,點擊ParserConfig對象java Basics Open In Dominator Tree,查看依賴樹
image.png -
打開后如下,分析發(fā)現(xiàn)IdentityHashMap的key是一個gson的對象瘫想,而需要反序列化的類是我們項目中自定義的仗阅,我又打開了其他的IdentityHashMap發(fā)現(xiàn)里面的結構和反序列化對象都是一樣的,同對象為什么會生成如此多個反序列化器呢殿托,Map存了這么多對象說明map的key都是不一樣的霹菊,因為map的key是不能重復的剧蚣,IdentityHashMap同樣也是支竹,再查看了這些IdentityHashMap的key發(fā)現(xiàn)每個key的類型都是ParameterizedTypeImpl,但是內(nèi)存地址值都是不一樣的鸠按,說明key每次都是new的一個新ParameterizedTypeImpl對象礼搁。
image.png 再看了下IdentityHashMap的put方法,key是直接拿對象的hash值做運算并存放到buckets指定坐標上的目尖,如此我們可以確定這跟key的生成是有關系的馒吴,我們結合上面查到的RespVipBaseDTO再看下key對象到底是怎么生成的,代碼中JSON.parseObject()的時候會到ParserConfig中按key尋找對應的反序列化器瑟曲,沒有的話就會新創(chuàng)建一個新的饮戳。
RespBaseDTO<RecommendContentDTO> result =
JSON.parseObject(resultString, new TypeToken<RespVipBaseDTO<RecommendContentDTO>>() {}.getType());
- 如上代碼會用new TypeToken(){}.getType()當作key去查找合適的反序列化器,那我們看下這個key是怎么生成的。這里只貼出創(chuàng)建時的代碼洞拨,如下代碼扯罐,我們發(fā)現(xiàn)ParameterizedTypeImpl都是通過new創(chuàng)建的,說明每次使用TypeToken時都會創(chuàng)建一個新的烦衣,這也是為什么IdentityHashMap越來越多的根本原因歹河。
if (type instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType) type;
return new ParameterizedTypeImpl(p.getOwnerType(), p.getRawType(), p.getActualTypeArguments());
}
- 其實原因還是開發(fā)人員在使用json時串用了不同廠商的json工具,TypeToken是谷歌gson的類花吟,使用的JSONObject卻是阿里fastjson的秸歧,正確的使用方法應該是使用TypeReference,TypeReference.getType()時同樣會返回一個Type類型衅澈,但是這個Type類型是通過java反射獲取到的類的基本信息键菱,以此Type作為key是不會有這樣的問題的:
BaseReqDTO<CommonReqDTO> baseReqDTO = parseRequest(request, new TypeReference<BaseReqDTO<CommonReqDTO>>() {}.getType());
- 可以簡單寫一些驗證代碼做個簡單的驗證,運行后發(fā)現(xiàn)使用TypeToken創(chuàng)建Type會造成程序運行緩慢并最終造成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());
}
}
總結:
- 發(fā)現(xiàn)問題可以先看代碼最近是否有大的變動纱耻,有沒有使用不合理的地方芭梯,結合系統(tǒng)最近情況作出判斷,沒發(fā)現(xiàn)明顯問題就果斷導dump文件開始做分析弄喘。
- 這次排查中看代碼的時間要比看dump分析的時間多得多玖喘,要單從分析工具中找到具體問題是不可能的,或者是因為我還沒掌握到更深的使用方法蘑志。
- 代碼質(zhì)量問題亟待解決