事務(wù)處理不當(dāng)惹骂,線上接口又雙叒內(nèi)存泄漏了!(附圖解問題全過程)

情景

項(xiàng)目上線了一個(gè)接口做瞪,先灰度一臺(tái)機(jī)器觀察調(diào)用情況析苫;
接口不斷的調(diào)用,過了一段時(shí)間穿扳,發(fā)現(xiàn)機(jī)器上的接口調(diào)用開始報(bào)OOM異常 衩侥!
當(dāng)天就是上線deadline了,刺激矛物。茫死。

q1.jpg

發(fā)現(xiàn)問題

第一步,使用jps命令獲取出問題jvm進(jìn)程的進(jìn)程ID

使用jps -l -m獲取到當(dāng)前jvm進(jìn)程的pid履羞,通過上述命令獲取到了服務(wù)的進(jìn)程號(hào):427726 (此處假設(shè)為這個(gè))

q2.png

jps命令

jps(JVM Process Status Tool):顯示指定系統(tǒng)內(nèi)所有的HotSpot虛擬機(jī)進(jìn)程
jps -l -m : 參數(shù)-l列出機(jī)器上所有jvm進(jìn)程峦萎,-m顯示出JVM啟動(dòng)時(shí)傳遞給main()的參數(shù)

第二步,使用jstat觀察jvm狀態(tài)忆首,發(fā)現(xiàn)問題

因?yàn)槭荗OM異常爱榔,所以我們首先重啟機(jī)器觀察了JVM的運(yùn)行情況;

我們使用jstat -gc pid time命令觀察GC糙及,發(fā)現(xiàn)GC在YGC后详幽,GC掉的內(nèi)存并不多,每次YGC后都有一部分內(nèi)存未回收浸锨,導(dǎo)致在多次YGC后回收不掉的內(nèi)存被挪到堆的old區(qū)唇聘,old滿了之后FGC發(fā)現(xiàn)也是回收不掉;
這里基本可以確定是內(nèi)存泄漏的問題了柱搜,下面我們有簡(jiǎn)單看了下機(jī)器的cpu迟郎、內(nèi)存、磁盤狀態(tài)

jstat命令:

jstat(JVM statistics Monitoring)是用于監(jiān)視虛擬機(jī)運(yùn)行時(shí)狀態(tài)信息的命令聪蘸,它可以顯示出虛擬機(jī)進(jìn)程中的類裝載宪肖、內(nèi)存、垃圾收集健爬、JIT編譯等運(yùn)行數(shù)據(jù)控乾。
jstat -gc pid time : -gc 監(jiān)控jvm的gc信息,pid 監(jiān)控的jvm進(jìn)程id浑劳,time每個(gè)多少毫秒刷新一次
jstat -gccause pid time : -gccause 監(jiān)控gc信息并顯示上次gc原因阱持,pid 監(jiān)控的jvm進(jìn)程id,time每個(gè)多少毫秒刷新一次
jstat -class pid time: -class 監(jiān)控jvm的類加載信息魔熏,pid 監(jiān)控的jvm進(jìn)程id衷咽,time每個(gè)多少毫秒刷新一次

在這里先簡(jiǎn)單說一下鸽扁,堆的GC:

在GC開始的時(shí)候,對(duì)象只會(huì)存在于Eden區(qū)和名為“From”的Survivor區(qū)镶骗,Survivor區(qū)“To”是空的桶现。緊接著進(jìn)行GC,Eden區(qū)中所有存活的對(duì)象都會(huì)被復(fù)制到“To”鼎姊,而在“From”區(qū)中骡和,仍存活的對(duì)象會(huì)根據(jù)他們的年齡值來決定去向。

年齡達(dá)到一定值(年齡閾值相寇,可以通過-XX:MaxTenuringThreshold來設(shè)置)的對(duì)象會(huì)被移動(dòng)到年老代中慰于,沒有達(dá)到閾值的對(duì)象會(huì)被復(fù)制到“To”區(qū)域。經(jīng)過這次GC后唤衫,Eden區(qū)和From區(qū)已經(jīng)被清空婆赠。這個(gè)時(shí)候,“From”和“To”會(huì)交換他們的角色佳励,也就是新的“To”就是上次GC前的“From”休里,新的“From”就是上次GC前的“To”。不管怎樣赃承,都會(huì)保證名為To的Survivor區(qū)域是空的妙黍,minor GC會(huì)一直重復(fù)這樣的過程。

第三步瞧剖,觀察機(jī)器狀態(tài)拭嫁,確認(rèn)問題

使用top -p pid獲取進(jìn)程的cpu和內(nèi)存使用率;查看RES 和 %CPU %MEM三個(gè)指標(biāo):

q3.png

在這里先簡(jiǎn)單說一下筒繁,top命令展示的內(nèi)容:

VIRT:virtual memory usage 虛擬內(nèi)存
1噩凹、進(jìn)程“需要的”虛擬內(nèi)存大小,包括進(jìn)程使用的庫(kù)毡咏、代碼、數(shù)據(jù)等
2逮刨、假如進(jìn)程申請(qǐng)100m的內(nèi)存呕缭,但實(shí)際只使用了10m,那么它會(huì)增長(zhǎng)100m修己,而不是實(shí)際的使用量

RES:resident memory usage 常駐內(nèi)存
1恢总、進(jìn)程當(dāng)前使用的內(nèi)存大小,但不包括swap out
2睬愤、包含其他進(jìn)程的共享
3片仿、如果申請(qǐng)100m的內(nèi)存,實(shí)際使用10m尤辱,它只增長(zhǎng)10m砂豌,與VIRT相反
4厢岂、關(guān)于庫(kù)占用內(nèi)存的情況,它只統(tǒng)計(jì)加載的庫(kù)文件所占內(nèi)存大小

SHR:shared memory 共享內(nèi)存
1阳距、除了自身進(jìn)程的共享內(nèi)存塔粒,也包括其他進(jìn)程的共享內(nèi)存
2、雖然進(jìn)程只使用了幾個(gè)共享庫(kù)的函數(shù)筐摘,但它包含了整個(gè)共享庫(kù)的大小
3卒茬、計(jì)算某個(gè)進(jìn)程所占的物理內(nèi)存大小公式:RES – SHR
4、swap out后咖熟,它將會(huì)降下來

DATA
1圃酵、數(shù)據(jù)占用的內(nèi)存。如果top沒有顯示馍管,按f鍵可以顯示出來郭赐。
2、真正的該程序要求的數(shù)據(jù)空間咽斧,是真正在運(yùn)行中要使用的堪置。

ps : 如果程序占用實(shí)存比較多,說明程序申請(qǐng)內(nèi)存多张惹,實(shí)際使用的空間也多舀锨。
如果程序占用虛存比較多,說明程序申請(qǐng)來很多空間宛逗,但是沒有使用坎匿。

發(fā)現(xiàn)機(jī)器的自身狀態(tài)不存在問題, so毋庸置疑雷激,發(fā)現(xiàn)問題了替蔬,典型的內(nèi)存泄漏。屎暇。

第四步承桥,使用jmap獲取jvm進(jìn)程dump文件

我們使用jmap -dump:format=b,file=dump_file_name pid 命令,將當(dāng)前機(jī)器的jvm的狀態(tài)dump下來或缺的一份dump文件根悼,用做下面的分析

jmap命令:

jmap(JVM Memory Map)命令用于生成heap dump文件凶异,還可以查詢finalize執(zhí)行隊(duì)列、Java堆和永久代的詳細(xì)信息挤巡,如當(dāng)前使用率剩彬、當(dāng)前使用的是哪種收集器等。
jmap -dump:format=b,file=dump_file_name pid : file=指定輸出數(shù)據(jù)文件名矿卑, pid jvm進(jìn)程號(hào)

接下來喉恋,回滾灰度的機(jī)器,開始解決問題=.=

解決問題

第一步,dump文件分析

在這里轻黑,我們分析dump文件糊肤,使用的Jprofiler軟件,就是下面這個(gè)東東:

q4.png

具體的使用方法苔悦,在這就不再贅述了轩褐,下面將dump文件導(dǎo)入到Jprofiler中:
選擇Heap Walker 中的Current Object Set,這里面顯示的是當(dāng)前的類的占用資源玖详,從占用空間從大到小排序把介;

q5.png

從上圖中,沒有觀察出什么問題蟋座,我們點(diǎn)擊Biggest Objects拗踢,查看哪個(gè)對(duì)象的占用的內(nèi)存高:

q6.png

從上圖中,我們發(fā)現(xiàn)org.janusgraph.graphdb.database.StandardJanusGraph這個(gè)對(duì)象居然占用了高達(dá)724M的內(nèi)存向臀! 看來內(nèi)存泄漏八九不離十就是這個(gè)對(duì)象的問題了巢墅!
再點(diǎn)開看看 ,如下圖券膀,可以發(fā)現(xiàn)是一個(gè)openTransactions的類型為ConcurrentHashMap的數(shù)據(jù)結(jié)構(gòu):

q7.png

第二步君纫,源碼查找定位代碼

這到底是什么對(duì)象呢,去項(xiàng)目中查找一下芹彬,打開idea-打開項(xiàng)目-雙擊shift鍵-打開全局類查找-輸入StandardJanusGraph蓄髓,如下圖:

q8.png

發(fā)現(xiàn)是我們項(xiàng)目使用的圖數(shù)據(jù)庫(kù)janusgraph的一個(gè)類,找到對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu):
類型定義:

private Set<StandardJanusGraphTx> openTransactions;

初始化為一個(gè)ConcurrentHashMap:

openTransactions = Collections.newSetFromMap(new 
ConcurrentHashMap<StandardJanusGraphTx, Boolean>(100, 
0.75f, 1));

觀察上述代碼舒帮,我們可以看到会喝,里面的存儲(chǔ)的StandardJanusGraphTx從字面意義上理解是janusgraph框架中的事務(wù)對(duì)象,下面往上追一下代碼玩郊,看看什么時(shí)候會(huì)往這個(gè)Map中賦值:

// 找到執(zhí)行openTransactions.add()的方法
    public StandardJanusGraphTx newTransaction(final TransactionConfiguration configuration) {
        if (!isOpen) ExceptionFactory.graphShutdown();
        try {
            StandardJanusGraphTx tx = new StandardJanusGraphTx(this, configuration);
            tx.setBackendTransaction(openBackendTransaction(tx));
            openTransactions.add(tx);  // 注意肢执! 此處對(duì)上述的map對(duì)象進(jìn)行了add
            return tx;
        } catch (BackendException e) {
            throw new JanusGraphException("Could not start new transaction", e);
        }
    }
 // 上述發(fā)現(xiàn),是一個(gè)newTransaction译红,創(chuàng)建事務(wù)的一個(gè)方法预茄,為確保起見,再往上跟找到調(diào)用上述方法的類:
   public JanusGraphTransaction start() {
        TransactionConfiguration immutable = new ImmutableTxCfg(isReadOnly, hasEnabledBatchLoading,
                assignIDsImmediately, preloadedData, forceIndexUsage, verifyExternalVertexExistence,
                verifyInternalVertexExistence, acquireLocks, verifyUniqueness,
                propertyPrefetching, singleThreaded, threadBound, getTimestampProvider(), userCommitTime,
                indexCacheWeight, getVertexCacheSize(), getDirtyVertexSize(),
                logIdentifier, restrictedPartitions, groupName,
                defaultSchemaMaker, customOptions);
        return graph.newTransaction(immutable);  // 注意侦厚!此處調(diào)用了上述的newTransaction方法
    }
 // 接著找上層調(diào)用反璃,發(fā)現(xiàn)了最上層的方法
    public JanusGraphTransaction newTransaction() {
        return buildTransaction().start();  // 此處調(diào)用了上述的start方法
    } 

在我們對(duì)圖數(shù)據(jù)庫(kù)中圖數(shù)據(jù)操作的過程中,采用的是手動(dòng)創(chuàng)建事務(wù)的方式,在每次查詢圖數(shù)據(jù)庫(kù)之前交洗,我們都會(huì)調(diào)用類似于dataDao.begin()代碼萨赁,
其中就是調(diào)用的public JanusGraphTransaction newTransaction()這個(gè)方法;

最后孽鸡,我們簡(jiǎn)單的看下源碼可以發(fā)現(xiàn),從上述內(nèi)存泄漏的map中去除數(shù)據(jù)的邏輯就是commit事務(wù)的接口稚虎,調(diào)用鏈如下:

    public void closeTransaction(StandardJanusGraphTx tx) {
        openTransactions.remove(tx); // 從map中刪除StandardJanusGraphTx對(duì)象
    }
    
    private void releaseTransaction() {
        isOpen = false;
        graph.closeTransaction(this); // 調(diào)用上述closeTransaction方法
        vertexCache.close();
    }
    
   public synchronized void commit() {
        Preconditions.checkArgument(isOpen(), "The transaction has already been closed");
        boolean success = false;
        if (null != config.getGroupName()) {
            MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx", "commit").inc();
        }
        try {
            if (hasModifications()) {
                graph.commit(addedRelations.getAll(), deletedRelations.values(), this);
            } else {
                txHandle.commit();  // 這個(gè)commit方法中釋放事務(wù)也是調(diào)用releaseTransaction
            }
            success = true;
        } catch (Exception e) {
            try {
                txHandle.rollback();
            } catch (BackendException e1) {
                throw new JanusGraphException("Could not rollback after a failed commit", e);
            }
            throw new JanusGraphException("Could not commit transaction due to exception during persistence", e);
        } finally {
            releaseTransaction();  // // 調(diào)用releaseTransaction
            if (null != config.getGroupName() && !success) {
                MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx", "commit.exceptions").inc();
            }
        }
    }
   

終于侧蘸,我們找到了內(nèi)存泄漏的根源所在:項(xiàng)目代碼中存在調(diào)用了事務(wù)begin但是沒有commit的代碼!

第三步裁眯,修復(fù)問題驗(yàn)證

解決問題: 找到內(nèi)存泄漏接口的代碼,并發(fā)現(xiàn)了沒有commit()的位置讳癌,try-catch-finally中添加上了commit()代碼穿稳;

提交-部署-發(fā)布-灰度一臺(tái)機(jī)器后觀察內(nèi)存泄漏的現(xiàn)象消失,GC回收正常晌坤;

內(nèi)存泄漏問題解決逢艘,項(xiàng)目如期上線~

最后

大家,有沒有遇到過內(nèi)存泄漏的情況骤菠,歡迎在評(píng)論區(qū)說出你的故事=.=

寫這篇文章耗費(fèi)的時(shí)間超出了我的預(yù)料它改,預(yù)計(jì)2個(gè)小時(shí)寫完,結(jié)果花了一下午的時(shí)間...

原創(chuàng)不易商乎,如果大家有所收獲央拖,希望大家可以點(diǎn)贊評(píng)論支持一下~

也歡迎大家關(guān)注我的簡(jiǎn)書和微信搜索公眾號(hào)[匠心Java]支持一下作者,作者定期分享工作中的所見所得~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鹉戚,一起剝皮案震驚了整個(gè)濱河市鲜戒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抹凳,老刑警劉巖遏餐,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異却桶,居然都是意外死亡境输,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門颖系,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗅剖,“玉大人,你說我怎么就攤上這事嘁扼⌒帕福” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵趁啸,是天一觀的道長(zhǎng)强缘。 經(jīng)常有香客問我,道長(zhǎng)不傅,這世上最難降的妖魔是什么旅掂? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮访娶,結(jié)果婚禮上商虐,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好秘车,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布典勇。 她就那樣靜靜地躺著,像睡著了一般叮趴。 火紅的嫁衣襯著肌膚如雪割笙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天眯亦,我揣著相機(jī)與錄音伤溉,去河邊找鬼。 笑死搔驼,一個(gè)胖子當(dāng)著我的面吹牛谈火,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舌涨,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼糯耍,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了囊嘉?” 一聲冷哼從身側(cè)響起温技,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扭粱,沒想到半個(gè)月后舵鳞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡琢蛤,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年蜓堕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片博其。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡套才,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出慕淡,到底是詐尸還是另有隱情背伴,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布峰髓,位于F島的核電站傻寂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏携兵。R本人自食惡果不足惜疾掰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望徐紧。 院中可真熱鬧个绍,春花似錦勒葱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽死遭。三九已至广恢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呀潭,已是汗流浹背钉迷。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钠署,地道東北人糠聪。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像谐鼎,于是被迫代替她去往敵國(guó)和親舰蟆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容