這是一個(gè)幾月前的案例,,問題比較典型,在分析和事后學(xué)習(xí)的過程中讓我對(duì)本地內(nèi)存溢出有了一定的了解,。在此和大家分享,。 先說一下背景,應(yīng)用環(huán)境是AIX5.3+WebSphere6.0.2.37,。在今年的一季度曾發(fā)生過幾次OOM故障,,當(dāng)時(shí)通過幾次內(nèi)存參數(shù)優(yōu)化,最后確定為“-Xgcprolicy:gencon –Xms512m –Xmx1280m –Xmn200m”,,此后穩(wěn)定了半年,,直到此次再發(fā)生應(yīng)用宕機(jī)。 趕到現(xiàn)場,,發(fā)現(xiàn)profiles目錄下生成有javacore和heapdump文件,,Javacore的第一行“Cause of thread dump : Dump Event "systhrow" (00040000) Detail "java/lang/OutOfMemoryError" received”表明了宕機(jī)的原因是OOM,但是令我困惑的是這段內(nèi)容: 0SECTION MEMINFO subcomponent dump routine NULL ================================= 1STHEAPFREE Bytes of Heap Space Free: 818cb70 1STHEAPALLOC Bytes of Heap Space Allocated: 20000000 在分配的512MB(十六進(jìn)制20000000)的堆空間中,,有129MB(818cb70)空閑空間,,按理說,這種情況下不該發(fā)生OutOfMemory,。就算有申請(qǐng)一個(gè)大對(duì)象,,同時(shí)JVM堆的新生代由于碎片原因沒有連續(xù)空間滿足要求,那么應(yīng)該發(fā)生堆擴(kuò)展,,所以此次內(nèi)存溢出不是堆(Heap)溢出,,GC日志的分析也支持了這一點(diǎn)。 既然Javacore無法得到有用信息,,我把目光轉(zhuǎn)向了SystemErr.log,,在對(duì)應(yīng)日期的地方,我發(fā)現(xiàn)了大量如下報(bào)錯(cuò): [8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R Exception in thread "WebContainer : 1" java.lang.RuntimeException: java.lang.OutOfMemoryError: Failed to create a thread: retVal -1073741830, errno 11 [8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R at com.ibm.io.async.ResultHandler.runEventProcessingLoop(ResultHandler.java:801) [8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R at com.ibm.io.async.ResultHandler$2.run(ResultHandler.java:881) [8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R at com.ibm.ws.util.ThreadPool$Worker.run(ThreadPool.java:1497) [8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R Caused by: java.lang.OutOfMemoryError: Failed to create a thread: retVal -1073741830, errno 11 at java.lang.Thread.startImpl(Native Method) at java.lang.Thread.start(Thread.java:980) at com.ibm.ws.util.ThreadPool.addThread(ThreadPool.java:630) at com.ibm.ws.util.ThreadPool$3.run(ThreadPool.java:1148) at com.ibm.ws.security.util.AccessController.doPrivileged(AccessController.java:63) at com.ibm.ws.util.ThreadPool.execute(ThreadPool.java:1146) at com.ibm.ws.util.ThreadPool.execute(ThreadPool.java:1040) at com.ibm.ws.runtime.WSThreadPool.execute(WSThreadPool.java:151) at com.ibm.io.async.ResultHandler.startHandler(ResultHandler.java:248) at com.ibm.io.async.ResultHandler.runEventProcessingLoop(ResultHandler.java:570) at com.ibm.io.async.ResultHandler$2.run(ResultHandler.java:881) at com.ibm.ws.util.ThreadPool$Worker.run(ThreadPool.java:1497) 在事后的學(xué)習(xí)中,,我知道“Java.lang.OutOfMemoryError: unable to create native thread” 這樣的異常是在說,,本地內(nèi)存耗盡,從而新的線程無法創(chuàng)建,。而在當(dāng)時(shí)我第一感覺是操作系統(tǒng)參數(shù)設(shè)置問題,,之前我曾寫過一篇由于nofile參數(shù)導(dǎo)致Too many open file的故障。于是我運(yùn)行如下命令 #lsattr -El sys0 -a maxuproc maxuproc 128 Maximum number of PROCESSES allowed per user True 然后運(yùn)行chgsys修改默認(rèn)的128為1024,這里我犯了一個(gè)錯(cuò)誤,,WebSphere單個(gè)Server就是一個(gè)Java進(jìn)程,,錯(cuò)誤日志里是不能創(chuàng)建一個(gè)thread,而非process,,與Oracle會(huì)創(chuàng)建多個(gè)oracle進(jìn)程不一樣,。果然兩天后又出現(xiàn)了同樣的問題。 這一次的SystemErr日志中,,除了上述的內(nèi)容,,還多了 [8/24/10 9:55:19:813 GMT+08:00] 00000036 SystemErr R Exception in thread "WebContainer : 4" java.lang.RuntimeException: java.lang.OutOfMemoryError: Unable to allocate 8192 bytes of direct memory after 5 retries [8/24/10 9:55:19:813 GMT+08:00] 00000036 SystemErr R at com.ibm.io.async.ResultHandler.runEventProcessingLoop(ResultHandler.java:801) [8/24/10 9:55:19:813 GMT+08:00] 00000036 SystemErr R at com.ibm.io.async.ResultHandler$2.run(ResultHandler.java:881) [8/24/10 9:55:19:813 GMT+08:00] 00000036 SystemErr R at com.ibm.ws.util.ThreadPool$Worker.run(ThreadPool.java:1497) [8/24/10 9:55:19:813 GMT+08:00] 00000036 SystemErr R Caused by: java.lang.OutOfMemoryError: Unable to allocate 8192 bytes of direct memory after 5 retries at java.nio.DirectByteBuffer.(DirectByteBuffer.java:197) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:303) at com.ibm.ws.buffermgmt.impl.WsByteBufferPoolManagerImpl.allocateBufferDirect(WsByteBufferPoolManagerImpl.java:656) at com.ibm.ws.buffermgmt.impl.WsByteBufferPoolManagerImpl.allocateCommon(WsByteBufferPoolManagerImpl.java:570) at com.ibm.ws.buffermgmt.impl.WsByteBufferPoolManagerImpl.allocateDirect(WsByteBufferPoolManagerImpl.java:506) at com.ibm.io.async.ResultHandler.runEventProcessingLoop(ResultHandler.java:498) at com.ibm.io.async.ResultHandler$2.run(ResultHandler.java:881) at com.ibm.ws.util.ThreadPool$Worker.run(ThreadPool.java:1497) Caused by: java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at java.nio.DirectByteBuffer.(DirectByteBuffer.java:184) … 7 more 我們可以看到是由于DirectByteBuffer無法分配導(dǎo)致的內(nèi)存溢出,而Native Method指明了這是“本地”的溢出,。通過這兩個(gè)關(guān)鍵字,,我查到了IBM的一份BUG記錄:PK31010: OUTOFMEMORYERROR DUE TO DIRECTBYTEBUFFER,但是我的版本已是最新,,無奈繼續(xù)搜尋,。 在Troubleshooting native memory issues這份文檔中,介紹了3種在WebSphere中最常見的導(dǎo)致OOM的原因,。其中第二個(gè)DirectByteBuffer use就是我們問題的癥結(jié),。 Java 1.4 中添加的新 I/O (NIO) 類引入了一種基于通道和緩沖區(qū)來執(zhí)行 I/O 的新方式。就像 Java 堆上的內(nèi)存支持 I/O 緩沖區(qū)一樣,,NIO 添加了對(duì)直接 ByteBuffer 的支持(使用java.nio.ByteBuffer.allocateDirect() 方法進(jìn)行分配),,ByteBuffer 受本機(jī)內(nèi)存而不是 Java 堆支持。直接 ByteBuffer 可以直接傳遞到本機(jī)操作系統(tǒng)庫函數(shù),,以執(zhí)行 I/O — 這使這些函數(shù)在一些場景中要快得多,,因?yàn)樗鼈兛梢员苊庠?Java 堆與本機(jī)堆之間復(fù)制數(shù)據(jù)。 對(duì)于在何處存儲(chǔ)直接 ByteBuffer 數(shù)據(jù),,很容易產(chǎn)生混淆。應(yīng)用程序仍然在 Java 堆上使用一個(gè)對(duì)象來編排 I/O 操作,,但持有該數(shù)據(jù)的緩沖區(qū)將保存在本機(jī)內(nèi)存中,,Java 堆對(duì)象僅包含對(duì)本機(jī)堆緩沖區(qū)的引用。非直接 ByteBuffer 將其數(shù)據(jù)保存在 Java 堆上的 byte[] 數(shù)組中,。下圖展示了直接與非直接 ByteBuffer 對(duì)象之間的區(qū)別: 直接與非直接 java.nio.ByteBuffer 的內(nèi)存拓?fù)浣Y(jié)構(gòu) 直接 ByteBuffer 對(duì)象會(huì)自動(dòng)清理本機(jī)緩沖區(qū),,但這個(gè)過程只能作為 Java 堆 GC 的一部分來執(zhí)行,因此它們不會(huì)自動(dòng)響應(yīng)施加在本機(jī)堆上的壓力,。GC 僅在 Java 堆被填滿,,以至于無法為堆分配請(qǐng)求提供服務(wù)時(shí)發(fā)生,或者在 Java 應(yīng)用程序中顯式請(qǐng)求它發(fā)生(不建議采用這種方式,,因?yàn)檫@可能導(dǎo)致性能問題),。 發(fā)生垃圾收集的情形可能是,本機(jī)堆被填滿,,并且一個(gè)或多個(gè)直接 ByteBuffers 適合于垃圾收集(并且可以被釋放來騰出本機(jī)堆的空間),,但 Java 堆幾乎總是空的,,所以不會(huì)發(fā)生垃圾收集。 摘自《理解JVM如何使用Windows和Linux上的本機(jī)內(nèi)存》 解決此問題的方法,,在文檔中給出的是禁止異步A/O,,通過在Web Container中設(shè)置參數(shù)來避免上節(jié)中所出現(xiàn)的由于Java堆空閑而不發(fā)生垃圾回收,導(dǎo)致本地堆撐滿的情況,。 Servers -> Application Servers -> serverName -> Web Container Settings -> Web Container -> Custom Properties: Press New: Add the following pair: Name: com.ibm.ws.webcontainer.channelwritetype Value: sync Press OK, and then save the configuration. 添加此屬性后應(yīng)用又恢復(fù)正常,,但是原先提高性能的特性反而導(dǎo)致內(nèi)存溢出,違背了初衷,,現(xiàn)在的做法只能算一個(gè)妥協(xié),。我會(huì)繼續(xù)查找此問題的解決方法,最不濟(jì)也要有個(gè)使用NIO的Best Practice,。 文檔中另兩個(gè)容易導(dǎo)致本地堆OOM的原因是: 過大的堆上限 我們知道32位機(jī)器單個(gè)進(jìn)程可以訪問的內(nèi)存地址空間為4G,,如右圖所示,但實(shí)際情況下Windows系統(tǒng)由于內(nèi)核態(tài)和用戶態(tài)的劃分,,用戶態(tài)只有2G的空間,,Linux和AIX的可用空間大一點(diǎn),但也在3G左右,。,,由于JVM實(shí)例進(jìn)程尋址是一定的,所以Heap大小和Native Area此消彼長,。而Native Area中有一部分就是供給線程的內(nèi)存空間,。 “應(yīng)用程序中的每個(gè)線程都需要內(nèi)存來存儲(chǔ)器堆棧(用于在調(diào)用函數(shù)時(shí)持有局部變量并維護(hù)狀態(tài)的內(nèi)存區(qū)域)。每個(gè) Java 線程都需要堆??臻g來運(yùn)行,。根據(jù)實(shí)現(xiàn)的不同,Java 線程可以分為本機(jī)線程和 Java 堆棧,。除了堆??臻g,每個(gè)線程還需要為線程本地存儲(chǔ)(thread-local storage)和內(nèi)部數(shù)據(jù)結(jié)構(gòu)提供一些本機(jī)內(nèi)存,。堆棧大小因 Java 實(shí)現(xiàn)和架構(gòu)的不同而不同,。一些實(shí)現(xiàn)支持為 Java 線程指定堆棧大小,其范圍通常在 256KB 到 756KB 之間,?!?/p> 1.5后一般線程堆棧大小為1M,“對(duì)于擁有數(shù)百個(gè)線程的應(yīng)用程序來說,,線程堆棧的總內(nèi)存使用量可能非常大,。如果運(yùn)行的應(yīng)用程序的線程數(shù)量比可用于處理它們的處理器數(shù)量多,效率通常很低,并且可能導(dǎo)致糟糕的性能和更高的內(nèi)存占用”(摘自《理解JVM如何使用Windows和Linux上的本機(jī)內(nèi)存》) 解決此問題的方法:設(shè)置恰當(dāng)?shù)腏VM最大堆,,32位不要超過1.5G,;設(shè)置恰當(dāng)?shù)木€程池大小(一般50~100),,當(dāng)線程使用較多時(shí),, 使用-Xss256k參數(shù)設(shè)置線程指定堆棧大小(IBM JDK還可以使用-Xmso ××k來設(shè)置Stack size for OS Threads 32-bit),;更換64位的JDK,。 第三個(gè)原因是線程池的TLS泄漏 這個(gè)現(xiàn)象我倒沒見到過,如果你的thread dump里下面三個(gè)屬性里的值有大于300,,那么就要注意了 "WebContainer : 1003" (TID:0×37D62000 "Default : 338" (TID:109934D0 "TCPChannel.DCS : 303" (TID:0×4D720000 解決的方法是設(shè)置線程池的最小最大值一致,。 |
|