最近上線一個需求,完成需求的過程對代碼進行了一次重構。應用發佈後半個小時左右,發現一個機器報警,load太高。登錄機器看CPU使用狀況,發現load已經正常,看下CPU使用狀況,發現有一個核跑滿,其餘CPU使用率很低。大概一個小時後,其餘機器陸續報警,發現一樣的問題,緊急回滾應用。java
應用運行在16G內存的虛機上,整個JVM11G內存,其中新生代3G,CMS gc,JDK7。spring
第一反應是JVM可能在進行full gc,由於只有一個線程跑滿,其餘線程被JVM暫停了。先去應用日誌看下應用運行狀況,果真日誌已經沒有任何輸出。jstat -gcutil
查看JVM內存使用狀況,發現Old區使用已經100%。數組
考慮到full gc致使RT變得超長,去ateye摘掉應用註冊的HSF服務,可是操做失敗,整個JVM已經沒有響應。數據結構
jmap -F -dump:format=b,file=atw.bin `jid`
把整個堆dump到本地,dump失敗,JVM已經僵死。多線程
jmap -histo `jid` > histo.log
保留histo內存快照成功;)併發
jstack `jid` > stack.log
JVM線程信息保存成功:)dom
現場保存完畢,重啓應用。ide
首先看下JVM線程棧信息,看看下是否有應用線程阻賽,通常狀況下,若是大量線程阻賽,每一個線程都持有必定量的內存,極可能致使內存吃緊,而這些阻塞的線程又沒有處理完請求,佔用的heap空間不能被minor gc回收掉,致使產生full gc,this
cat stack.log | grep atw | sort | uniq -c | sort -nr | head -10
結果以下(從新排版過):spa
177 at ...service.impl...searchInterProduct(AtwSearchServiceImpl.java:505) 104 at ...service.impl..searchOneWay(AtwSearchServiceImpl.java:260) 80 at ...service.impl.executor...execute(OneWayCommonAgentSearchExecutor.java:419) 70 at ...service.impl.executor...handleFlights(AbstractSearchExecutor.java:689) 47 at ...service.impl...searchOneWay(AtwSearchServiceImpl.java:257) 31 at ...service.impl.executor...getFlightInfoAndStock(AbstractSearchExecutor.java:1073) 30 at ...service.impl.executor...getFlightInfoAndStock(AbstractSearchExecutor.java:1087) 22 at ...util.stlog.FarePolicyCounter.addFail(FarePolicyCounter.java:249) 20 at ...service.impl.executor...execute(OneWayCommonAgentSearchExecutor.java:424) 20 at ...service.impl.executor...getAllFares(AbstractSearchExecutor.java:892)
HSF線程開了200個,應用相關的正在運行的線程最多的是com.taobao.trip.atw.service.impl.AtwSearchServiceImpl.searchInterProduct(AtwSearchServiceImpl.java:505)
,一共177個,小於HSF線程數,屬於正常,其餘線程數量也在正常範圍內。線程的鎖和其餘信息也未發現異常。
接下來看下histo.log:
num #instances #bytes class name ---------------------------------------------- 1: 204258 4368429800 [B 2: 6812683 926524888 com.taobao.trip.atw.domain.AtwInterFareDO 3: 22639343 724458976 java.util.HashMap$Entry 4: 22304135 538457776 [S 5: 21614962 518759088 java.lang.Long 6: 13867918 443773376 com.taobao.trip.atw.util.LongReferenceConcurrentHashMap$HashEntry 7: 6812439 326997072 com.taobao.trip.atw.domain.AtwInterFareSegmentDO 8: 421442 211696296 [J 9: 557827 199825568 [Ljava.util.HashMap$Entry; 10: 6812439 163498536 [Lcom.taobao.trip.atw.domain.AtwInterFareSegmentDO;
發現最大的內存對象是byte數組,204258個實例大約佔用4G堆空間(整個堆11G),平均每一個20k。
初步猜想可能本次上線代碼還有new byte[]
的地方,即查看代碼,發現本次新增功能沒有這樣的代碼。並且整個應用的代碼也無可疑地方產生了byte數組。
繼續分析多是依賴的二方或者三方jar包引發,從新申請分支,提發佈單,查看發佈包變化:
經過對比發現,本次發佈涉及jar包變動很小,並且無三方包變動,只有內部包發生變化。對變化的包進行分析沒有找到new byte[]
地方。
繼續分析histo.log,找到一臺線上正常機器,生成histo,用故障機器數據減去正常值,獲得差值以下(top 10):
class | instances | bytes |
---|---|---|
[B | 47404 | 4275481936 |
java.util.HashMap$Entry | 19442009 | 622144288 |
java.lang.Long | 19711584 | 473078016 |
[Ljava.util.HashMap$Entry; | 239216 | 164734560 |
com.taobao.at.dal.common.dataobject.AtwMessage | 359668 | 20141408 |
java.util.HashMap | 215770 | 10356960 |
java.util.concurrent.LinkedBlockingQueue$Node | 421036 | 10104864 |
com.taobao.trip.atw.metaq.service.common.LocalMessageReactor$1 | 359769 | 8634456 |
com.alibaba.rocketmq.common.message.MessageExt | 65151 | 6775704 |
除了byte[] 外,java.util.HashMap$Entry
比正常機器多2kw,查看代碼也沒有明顯證據能解釋HashMap和byte[]同時增大的場景。
至此,分析思路阻塞,須要找到新的線索。
經過上面的分析,已經找到現象:應用出現了full gc,並且伴隨大量byte[]和java.util.HashMap$Entry不能回收。
然而,full gc最直接的產物gc.log尚未被挖掘。根據full gc時間點,發現新線索(從新排版過):)
==WARNNING== allocating large array--thread_id[0x00007f71211b0800]--thread_name[owc--425027705]--array_size[2132509912 bytes]--array_length[2132509891 elememts] prio=10 tid=0x00007f71211b0800 nid=0x3f43e runnable at com.alibaba.dubbo.common.io.Bytes.copyOf(Bytes.java:59) at com.alibaba.dubbo.common.io...write(UnsafeByteArrayOutputStream.java:64) at com.taobao.hsf.com.caucho.hessian.io...flushBuffer(Hessian2Output.java:1553) at com.taobao.hsf.com.caucho.hessian.io...printString(Hessian2Output.java:1466) at com.taobao.hsf.com.caucho.hessian.io...writeString(Hessian2Output.java:987) at com.taobao.hsf.com.caucho.hessian.io...writeObject(BasicSerializer.java:149) at com.taobao.hsf.com.caucho.hessian.io...writeObject(Hessian2Output.java:421) at com.taobao.hsf.com.caucho.hessian.io...writeObject(MapSerializer.java:99) at com.taobao.hsf.com.caucho.hessian.io...writeObject(Hessian2Output.java:421) at com.taobao.hsf.com.caucho.hessian.io...serialize(UnsafeSerializer.java:293) at com.taobao.hsf.com.caucho.hessian.io...writeInstance(UnsafeSerializer.java:212) at com.taobao.hsf.com.caucho.hessian.io...writeObject(UnsafeSerializer.java:171) at com.taobao.hsf.com.caucho.hessian.io.H..writeObject(Hessian2Output.java:421) at com.taobao.hsf.remoting.serialize...encode(Hessian2Encoder.java:23) at com.taobao.hsf.remoting.server.output...writeHSFResponse(RpcOutput.java:47) at com.taobao.hsf.remoting.provider...handleRequest(ProviderProcessor.java:202) at com.taobao.hsf.remoting.server...handleRequest(RPCServerHandler.java:47) at com.taobao.hsf.remoting.server..r.handleRequest(RPCServerHandler.java:25) ...
阿里定製的JVM增長了許多本身的新特性,其中一個就是在full gc不能回收的狀況下,會把當前分配最大內存的線程信息和分配的內存信息打印出來!
==WARNNING== allocating large array–thread_id[0x00007f71211b0800]–thread_name[owc–425027705]–array_size[2132509912 bytes]–array_length[2132509891 elememts]
線程owc--425027705
,這是一個應用本身處理HSF請求的線程,它在分配一個巨大的數據組!經過gc日誌的堆棧信息發現當前這個線程正在處理byte[]的拷貝:
at com.alibaba.dubbo.common.io.Bytes.copyOf(Bytes.java:59)
這個拷貝過程通常是,應用處理好HSF請求後,把處理結果序列化成byte[],而後經過網路傳輸到調用機器上。
至此找到了byte[]產生的緣由,還有java.util.HashMap$Entry
未解決。
根據線程名字owc--425027705
去JVM的線程日誌查找信息,發現owc--425027705
是處理請求的主線程,下面有四個子線程都在處理這樣的堆棧:
"owc--425027705-344" daemon prio=10 tid=0x00007f710278f800 nid=0x3f414 runnable [0x0000000051906000] java.lang.Thread.State: RUNNABLE at java.util.HashMap.getEntry(HashMap.java:469) at java.util.HashMap.get(HashMap.java:421) at com.taobao.trip.atw.result.impl.DefaultPriceMergerOW.processHeightQuality(DefaultPriceMergerOW.java:327) at com.taobao.trip.atw.result.impl.DefaultPriceMergerOW.extendedProductProcess(DefaultPriceMergerOW.java:179) at com.taobao.trip.atw.result.impl.DefaultPriceMergerOW.mergeOneWay(DefaultPriceMergerOW.java:137) at com.taobao.trip.atw.result.PriceMergerProxy.mergeOneWay(PriceMergerProxy.java:184) ...
子線程都在從HashMap中get數據!因爲以前遇到過HashMap多線程操做致使成環形數據結構,繼而get操做成死循環的教訓,這裏判定是HashMap問題!
簡短的說,多線程下對HashMap的put操做,會致使內部的Entry鏈表造成環形數據結構。 首先,put操做會檢查容量是否充足,若是不足,會resize內部數組。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
問題就在於resize內部會遍歷Entry的鏈表:
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
這樣的代碼在多線程狀況下,會出現環。
對於成環的Map,get遍歷Entry鏈表時會致使死循環:
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
爲何會出現多個線程同時操做一個HashMap?
主線程收到請求後,會分配4個子線程去計算結果,而後由主線程去完成對結果的合併。若是子線程處理失敗或者超時,那麼這個子線程的結果會被丟棄,不會被合併。
從日誌上看,4個子線程的處理都已經超時,可是因爲HashMap併發操做形成死循環,4個子線程仍然在運行,主線程丟棄了子線程的結果,那數量量應該很是小纔對,爲什麼會產生如此大的byte[]?
追根溯源,從主線程分配任務找到了端倪。4個子線程處理計算的結果對象都是從主線程拷貝過來的:
BeanUtils.copyProperties(main, rsp);
此次修改在結果對象上增長了一個HashMap:
private Map<Long,Map<ItemGroupType,ItemDO>> agentItemGroup; public Map<Long, Map<ItemGroupType, ItemDO>> getAgentItemGroup() { if (agentItemGroup == null) { agentItemGroup = new HashMap<Long, Map<ItemGroupType, ItemDO>>(); } return agentItemGroup; } public void setAgentItemGroup(Map<Long, Map<ItemGroupType, ItemDO>> agentItemGroup) { this.agentItemGroup = agentItemGroup; }
agentItemGroup
的get方法會判斷是否null,若是是的話,會生成一個新的map。
在org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object)
方法中,對象屬性的賦值會調用get/set方法,(參考:org.springframework.beans.BeanUtils#copyProperties(java.lang.Object, java.lang.Object, java.lang.Class, java.lang.String[]))這樣就致使4個子線程用的map跟主線程是同一個map,並且就算子線程的結果被放棄了,主線程的map已經被搞壞。
com.taobao.hsf.com.caucho.hessian.io.MapSerializer.writeObject(MapSerializer.java:99)
HSF在對Map的序列化時候,對遍歷Map,進行序列化:
public void writeObject(Object obj, AbstractHessianOutput out) throws IOException { if(!out.addRef(obj)) { Map map = (Map)obj; Class cl = obj.getClass(); if(!cl.equals(HashMap.class) && this._isSendJavaType && obj instanceof Serializable) { out.writeMapBegin(obj.getClass().getName()); } else { out.writeMapBegin((String)null); } Iterator iter = map.entrySet().iterator(); while(iter.hasNext()) { Entry entry = (Entry)iter.next(); out.writeObject(entry.getKey()); out.writeObject(entry.getValue()); } out.writeMapEnd(); } }
因爲主線程的map已經成環形數據結構,遍歷的迭代器會死循環執行。
至此,full gc現象所有排查完畢,解決方案,一行代碼到搞定:
BeanUtils.copyProperties(main, rsp); rsp.setAgentItemGroup(new HashMap<Long, Map<ItemGroupType, ItemDO>>());
併發環境下被HashMap坑不止一次,不少時候,寫代碼沒有考慮併發場景,熟知寫的代碼已是在併發環境運行了。這樣就釀成大錯,其實後來想一想,HashMap也能夠作一下改進,get中若是循環超過size次了,拋出個異常,也不會致使死循環和full gc了 :)
但這並不能根治問題,寫代碼仍是要多想一想,增強reivew!