讓人頭疼的WAS內(nèi)存溢出,看銀行運(yùn)維人員如何優(yōu)雅的解決
1 引言
WAS(IBM WebSphere Application Server)是IBM發(fā)布的一款成熟的企業(yè)級Web中間件產(chǎn)品,憑借其可靠性與穩(wěn)定性,一直是國內(nèi)大型商業(yè)銀行Web服務(wù)的主流選擇??稍俜€(wěn)定也會(huì)出問題,在日常的生產(chǎn)運(yùn)維中,WAS應(yīng)用問題的排查確實(shí)讓筆者這種銀行運(yùn)維人員頭疼。一方面廠商提供技術(shù)支持的時(shí)效性與準(zhǔn)確性有待改善,另一方面像IBM其他產(chǎn)品一樣,網(wǎng)上開放的可參考和借鑒的資料太少,發(fā)生WAS問題時(shí)著實(shí)讓人無從下手。不過不要緊,魯迅先生曾經(jīng)說過,“走的人多了,自然就有路了”,筆者作為具有多年WAS運(yùn)維經(jīng)驗(yàn)的老鳥,下面就把自己在應(yīng)對WAS內(nèi)存溢出方面的知識總結(jié)一下,為大家介紹一下如何優(yōu)雅的應(yīng)對WAS內(nèi)存溢出。
2 IBM JAVA內(nèi)存管理
要應(yīng)對WAS內(nèi)存溢出,必須對IBM對JAVA內(nèi)存的管理有所了解,下面,筆者就簡單介紹一下IBM是如何管理JAVA內(nèi)存的。不同于大家經(jīng)常使用的Oracle Java,WAS使用的JAVA是內(nèi)置于WAS內(nèi)部的IBM JAVA,與Oracle Java在JVM、配置參數(shù)等方面有著顯著不同。
IBM JAVA 同樣包含JDK、JRE、JVM三層,其關(guān)系如圖所示:
圖1 JDK、JRE、JVM關(guān)系
圖2 JVM 運(yùn)行時(shí)內(nèi)存區(qū)域
程序計(jì)數(shù)器區(qū)域
Java虛擬機(jī)支持多線程運(yùn)行,所以對于每個(gè)線程,都需要一個(gè)指示其運(yùn)行程序位置的指針,這個(gè)指針指向當(dāng)前程序運(yùn)行方法的地址。
Java虛擬機(jī)棧
每一個(gè)Java線程都擁有一個(gè)私有的Java虛擬機(jī)棧。像其他傳統(tǒng)語言一樣,Java虛擬機(jī)棧保存了程序調(diào)用時(shí)的局部變量和部分結(jié)果(稱之為Frame)。
方法區(qū)、運(yùn)行時(shí)常量池
方法區(qū)存放運(yùn)行時(shí)常量池、字段以及方法(包括構(gòu)造方法、特殊方法)代碼。在IBM Java 8版本中,所有加載的類都存放在稱之為Metaspace的空間中,Metaspace使用操作系統(tǒng)本地內(nèi)存空間。
本地方法區(qū)域
為了支持操作系統(tǒng)本地方法(如C語言)調(diào)用,虛擬機(jī)中在本地方法區(qū)域中存儲本地方法調(diào)用的棧信息。
堆空間
堆是JVM運(yùn)行時(shí)內(nèi)存中最大的區(qū)域,也是和程序開發(fā)密切相關(guān)區(qū)域,所有的對象實(shí)例(包括基本類型)、數(shù)組都存放在這個(gè)區(qū)域。和傳統(tǒng)的C、C++語言不同,Java語言不需要開發(fā)人員顯式地進(jìn)行內(nèi)存的申請和釋放,而是由JVM的Allocator(內(nèi)存分配器)和Garbage Collection(內(nèi)存垃圾回收器,簡稱GC)負(fù)責(zé)管理內(nèi)存。我們最常見的內(nèi)存溢出“java.lang.OutOfMemoryError : Java heap space”也主要和該區(qū)域有關(guān)。下面我們將著重闡述IBM J9 VM堆空間相關(guān)模型和垃圾回收策略。
堆空間內(nèi)存結(jié)構(gòu)和垃圾回收策略(GC)
J9 VM支持多種不同的GC策略,不同的GC策略對應(yīng)不同的Heap內(nèi)存模型及分配回收算法,不同的GC策略適應(yīng)于不同的業(yè)務(wù)場景,對于大多數(shù)系統(tǒng)(特別是交易類系統(tǒng))來說,可使用“Generational Concurrent Garbage Collector”策略(簡稱gencon,參數(shù):-Xgcpolicy:gencon可以指定使用該策略),這也是J9 VM的默認(rèn)GC策略,本文主要詳細(xì)介紹該策略。
“Generational Concurrent Garbage Collector”策略特別適合存在非常多短生命周期對象的應(yīng)用,即對象申請完之后,很快就不被使用,可以被GC回收。而一般的交易類系統(tǒng),都符合這種場景。
在該策略下,Heap內(nèi)存被劃分成新區(qū)域(Nursery)、老區(qū)域(Tenured)。所有對象創(chuàng)建后都被分配到Nursery區(qū)域,之后如果該對象一直標(biāo)記為可用,則會(huì)被自動(dòng)到Tenured區(qū)域。
圖3 J9 VM 默認(rèn)堆空間內(nèi)存模型
圖4 Local GC過程
上文提到,在一般場景下,大部分對象創(chuàng)建后,很快就不被使用、存活的對象較少,所以Local GC移動(dòng)的數(shù)據(jù)也很少,而且Local GC后,可以得到很大的Allocate空間,這樣就減小了GC時(shí)間。在JVM中,GC意味著所有運(yùn)行中線程都要停下來(Pause)等待GC結(jié)束,GC完成后,才可以繼續(xù)運(yùn)行,所以Local GC可以減少因GC帶來的系統(tǒng)吞吐量下降的影響。
發(fā)生堆空間分配失敗或者調(diào)用System.gc()方法后,觸發(fā)Global GC過程。Global GC通過標(biāo)記、清除、壓縮過程來盡可能釋放JVM內(nèi)存空間。Global GC需要獲得整個(gè)JVM的排他控制權(quán),所以當(dāng)進(jìn)行Global GC時(shí),所有應(yīng)用線程也將暫停。當(dāng)Global GC結(jié)束后,應(yīng)用線程將恢復(fù)執(zhí)行。
3 常見的WAS內(nèi)存溢出原因
上面我們介紹了IBM Java內(nèi)存管理的模型和策略。理解上述模型后,我們可以清楚的知道為何會(huì)發(fā)生內(nèi)存溢出:
(1)JVM內(nèi)部或者JVM間接使用的操作系統(tǒng)內(nèi)存分配失敗后觸發(fā)內(nèi)存溢出報(bào)錯(cuò)。JVM內(nèi)存區(qū)域中,除了程序計(jì)數(shù)器區(qū)域外,Java虛擬機(jī)棧、堆空間、方法區(qū)、運(yùn)行時(shí)常量池、本地方法棧都可能會(huì)發(fā)生內(nèi)存溢出報(bào)錯(cuò)。
(2)對于堆空間,當(dāng)堆空間已經(jīng)盡可能擴(kuò)展,并且JVM花費(fèi)了95%以上的時(shí)間在GC時(shí),也會(huì)觸發(fā)內(nèi)存溢出報(bào)錯(cuò)。
以上兩點(diǎn)是內(nèi)存溢出的基本要點(diǎn),但實(shí)際生產(chǎn)系統(tǒng)由于運(yùn)行環(huán)境往往較為復(fù)雜,在處理實(shí)際問題時(shí),我們還應(yīng)結(jié)合環(huán)境配置和業(yè)務(wù)場景來分析。通過總結(jié)實(shí)際運(yùn)維過程中經(jīng)驗(yàn),可以將內(nèi)存溢出原因分為如下幾類:
(1)堆內(nèi)存大小上限配置過低
由于Java程序所能使用的堆空間上限完全取決于JVM啟動(dòng)時(shí)的參數(shù)配置,當(dāng)堆空間上限參數(shù)設(shè)置過低,即使操作系統(tǒng)物理內(nèi)存空閑較多,應(yīng)用程序也無法使用。所以在問題排查時(shí),我們首先應(yīng)該明確系統(tǒng)配置的堆空間上限(由Xmx參數(shù)指定),一般不能使用堆大小上限默認(rèn)值。
(2)程序內(nèi)存泄漏導(dǎo)致內(nèi)存持續(xù)增長
如果程序存在內(nèi)存泄漏,即使已經(jīng)不再使用的內(nèi)存仍將無法被GC回收釋放,JVM內(nèi)存將持續(xù)增長(而且,由于內(nèi)存使用率逐漸升高,將會(huì)更加頻繁的觸發(fā)GC,反復(fù)GC又會(huì)引發(fā)CPU過高),最終導(dǎo)致堆內(nèi)存空間滿而引發(fā)內(nèi)存溢出。
(3)數(shù)據(jù)查詢交易返回記錄數(shù)過多或者程序申請使用大內(nèi)存對象
當(dāng)程序過度地使用內(nèi)存大對象或數(shù)組,導(dǎo)致無法申請足夠的內(nèi)存空間而引發(fā)內(nèi)存溢出。例如,在實(shí)際生產(chǎn)中,可能存在應(yīng)用程序讀取整表數(shù)據(jù)或情況(數(shù)據(jù)條數(shù)在幾萬條以上),極易引發(fā)內(nèi)存溢出。
(4)物理內(nèi)存過低或因其他進(jìn)程消耗過多內(nèi)存引發(fā)內(nèi)存溢出
即使我們設(shè)定了合理的JVM內(nèi)存空間大小上限,但也有可能因?yàn)楸镜夭僮飨到y(tǒng)本身可用內(nèi)存過低、無法實(shí)現(xiàn)內(nèi)存空間的動(dòng)態(tài)擴(kuò)充,進(jìn)而導(dǎo)致內(nèi)存溢出;也可能因?yàn)樵谕粋€(gè)操作系統(tǒng)上運(yùn)行的其他JVM或者本地進(jìn)程使用過多的內(nèi)存導(dǎo)致內(nèi)存溢出;由于JVM的部分區(qū)域(如Metaspace、DirectMemory等)直接使用的是操作系統(tǒng)內(nèi)存,所以當(dāng)操作系統(tǒng)內(nèi)存過低,但創(chuàng)建本地線程過多、加載類過多時(shí)也有可能發(fā)生內(nèi)存溢出異常;當(dāng)程序過度使用DirectMemory也會(huì)引發(fā)內(nèi)存溢出。
(5)交易量突然增大
如果我們將JVM堆內(nèi)存上限設(shè)為M,每支交易處理需要使用的堆內(nèi)存是N,那么當(dāng)同時(shí)處理的交易量X突然增多N*X>M時(shí),就容易觸發(fā)內(nèi)存溢出。
4 如何優(yōu)雅的應(yīng)對WAS內(nèi)存溢出
當(dāng)發(fā)生內(nèi)存溢出后,首先要做的是恢復(fù)生產(chǎn),恢復(fù)因內(nèi)存溢出而宕機(jī)的Server?;謴?fù)生產(chǎn)后,可按照下面步驟進(jìn)行內(nèi)存溢出原因分析。
收集環(huán)境信息
內(nèi)存溢出分析首先要做的就是收集環(huán)境信息和日志信息。
收集日志文件
表 1 收集日志文件表
分析應(yīng)用日志
查看SystemOut.log日志java.lang.OutOfMemoryError的提示信息,確定內(nèi)存溢出發(fā)生在JVM的哪個(gè)區(qū)域之后,查看SystemOut.log、SystemErr.log中應(yīng)用交易日志,分析是否可疑的異常交易。
分析堆內(nèi)存使用趨勢
一般內(nèi)存分析,第一步先查看JVM內(nèi)存使用情況,即通過“IBM Pattern Modeling and Analysis Tool for Java Garbage Collector”工具,打開native_stderr.log文件,查看JVM堆空間內(nèi)存使用曲線:
對于大對象或數(shù)組使用導(dǎo)致內(nèi)存溢出的曲線一般如下圖所示,存在曲線突然升高的情況:
圖5 大對象內(nèi)存溢出堆空間趨勢圖
內(nèi)存泄漏導(dǎo)致內(nèi)存溢出的曲線一般如下圖所示,曲線緩慢上升(紅色曲線):
圖6 內(nèi)存泄漏程序堆空間趨勢圖
找到堆空間可疑內(nèi)存溢出點(diǎn)
分析線程現(xiàn)場信息
使用“IBM Thread and Monitor Dump Analyzer for Java”工具,分析javacore文件。檢查內(nèi)存溢出時(shí)正在執(zhí)行的交易、正在執(zhí)行的方法。
非堆空間內(nèi)存溢出
如果出現(xiàn)“java.lang.OutOfMemoryError: 本機(jī)內(nèi)存耗盡”內(nèi)存溢出報(bào)錯(cuò),則需要考慮DirectByteBuffer內(nèi)存區(qū)域引發(fā)內(nèi)存溢出。
5 如何在具體場景應(yīng)用
圖7:發(fā)生問題時(shí)某臺WAS服務(wù)器的內(nèi)存監(jiān)控情況
第三步,首先我們來查看日志文件,下面分別是SystemOut.log和SystemErr.log的部分內(nèi)容。果然,在問題時(shí)點(diǎn)附近的錯(cuò)誤日志中看到了OutOfMemoryError,同時(shí)在應(yīng)用日志中看到了一些正在執(zhí)行的sql,那么到底是哪個(gè)程序在作怪,又是為什么產(chǎn)生了內(nèi)存溢出呢。
圖8:問題時(shí)點(diǎn)的應(yīng)用日志
圖9:問題時(shí)點(diǎn)的錯(cuò)誤日志
第四步,看來僅從日志是無法定位具體問題的,筆者接下來要運(yùn)用工具來解決問題了。筆者先后用IBM HeapAnalyzer和IBM Thread and Monitor Dump Analyzer for Java工具,分別對Heapdump文件及Javacore文件進(jìn)行了具體的分析。對Heapdump文件的解析結(jié)果顯示,某個(gè)List居然存在68萬多個(gè)對象,占用了近50%的內(nèi)存空間。對Javacore文件的分析結(jié)果顯示,發(fā)生溢出時(shí)某支交易線程一直處于等待狀態(tài)。
圖10:Heapdump文件的分析結(jié)果
圖11:Heapdump文件的分析結(jié)果
6 如何預(yù)防或解決內(nèi)存溢出問題
7 最后