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