漂亮的代碼,糟糕的行為—解決 Java 運行時的內存問題

來源:norwind?

我們的一個程序,假設名字為“Photo Pull”,主要解決以下問題:

從各種第三方來源上獲取聯系人照片

將照片重新調整至所需的縮略圖大小

將結果發(fā)送到S3

看起來似乎這個程序能夠理所當然的穩(wěn)定運行.但事實剛好與之相反,這是我們最容易出問題的程序之一——不是因為代碼有問題,而是Java運行時會導致問題。

漂亮的代碼, 糟糕的行為

我們遇到的第一個問題是Photo Pull運行后內存占用量持續(xù)增大。幾個小時后,這個進程將消耗掉系統的所有內存,直到被Linux OOM-killer殺掉或被Storm重啟。

常用的Java內存使用診斷工具起不了什么作用。所有我們了解到的是,非堆內存存在大量泄露,而堆內存保持在幾百MB。

然而,有一個統計學的方法來確定內存泄露的原因。導致內存泄露的組件也會使內存占用量超過任意一個閾值。如果我們在堆棧中多次跟蹤anOutOfMemoryError 錯誤,我們就有很大可能確定內存泄露的原因?,F在唯一的問題是,這個進程不能自己發(fā)現內存被耗盡,只有被Linux殺死時才知道。

這個問題的解決方法是使用ulimit命令來為該程序設置一個內存使用上限,這個上限值要顯著低于系統內存,例如:

ulimit -m $((1024*1024))

該命令是在FreeBSD系統上將程序使用內存限制為1G以內。內存使用上限設置完成后,讓程序運行一段時間,直到崩潰。

通過堆棧跟蹤,我們發(fā)現一個本地棧幀:

com.sun.imageio.plugins.jpeg.JPEGImageReader.initJPEGImageReader()

該函數是在jdk/src/share/native/sun/awt/image/jpeg/imageioJPEG.c文件中,屬于OpenJDK 6源代碼。簡單查看一下,發(fā)現如下問題:

/* ... snip to line 1450 */

/* We use our private extension JPEG error handler.

*/

jerr = malloc (sizeof(struct sun_jpeg_error_mgr));

/* ... snip to line 1476 */

/* Establish the setjmp return context for sun_jpeg_error_exit to use. */

if (setjmp(jerr->setjmp_buffer)) {

/* If we get here, the JPEG code has signaled an error. */

char buffer[JMSG_LENGTH_MAX];

(*cinfo->err->format_message) ((struct jpeg_common_struct *) cinfo,

buffer);

JNU_ThrowByName(env, "javax/imageio/IIOException", buffer);

return 0;

}

在1452行C代碼給一個錯誤的handler分配了空間,但之后一直沒有釋放,導致拋出IIOException異常.目前還不清楚cinfo是否也存在泄露。

檢查OpenJDK 7中的同一個文件,發(fā)現bug只存在于OpenJDK 6中。將代碼移植到Java 7中,運行了幾個小時后沒有發(fā)現內存泄露。

分段查找錯誤

在發(fā)現內存泄漏原因幾個小時后,程序的一個組件崩潰了,過了一會兒另外一個也崩潰了。Storm盡職盡責地重新啟動它們,但是沒有找到組件為什么被殺死的信息——沒有日志記錄,沒有內存增長,也沒有跡象表明是被Storm殺死。我們重新在本地運行程序,最終獲得如下信息:

# A fatal error has been detected by the Java Runtime Environment:

#

# SIGSEGV (0xb) at pc=0x00007fb120713242, pid=11629, tid=140394149959424

#

# JRE version: 7.0_25-b15

# Java VM: Java HotSpot(TM) 64-Bit Server VM (23.25-b01 mixed mode linux-amd64 compressed oops)

# Problematic frame:

# C [libpthread.so.0+0xe242] sem_post+0x12

由此產生的hs_err_pid*.log文件缺乏Java堆棧跟蹤,而由JVM產生的堆棧轉儲只包含最底層的框架。盡管如此,我們開始查找引起POSIX線程庫觸發(fā)段錯誤的條件.向sem_post傳遞18字節(jié)的信號量,得到如下內容:

e230: mov ? ?eax,DWORD PTR [rdi]

e232: cmp ? ?eax,0x7fffffff

e237: je ? ? e26c

e239: lea ? ?esi,[rax+0x1]

e23c: lock cmpxchg DWORD PTR [rdi],esi

e240: jne ? ?e232

e242: cmp ? ?QWORD PTR [rdi+0x8],0x0 ? ? ? ?# Here

e247: je ? ? e262

e249: mov ? ?eax,0xca

e24e: mov ? ?esi,0x1

e253: or ? ? esi,DWORD PTR [rdi+0x4]

e256: mov ? ?edx,0x1

e25b: syscall

e25d: test ? rax,rax

e260: js ? ? e265

e262: xor ? ?eax,eax

e264: ret

e265: mov ? ?eax,0x16

e26a: jmp ? ?e271

e26c: mov ? ?eax,0x4b

e271: mov ? ?rdx,QWORD PTR [rip+0x209d08]

e278: mov ? ?DWORD PTR fs:[rdx],eax

e27b: or ? ? eax,0xffffffff

e27e: ret

這一失敗本身,是相當令人吃驚的.只有早期在e230處有幾個指令訪問rdi寄存器成功,即使只有8個字節(jié)數據在內存中。但JVM將rdi寄存器放置在0x00007fb11046e000。 添加八個字節(jié)不可能跨越頁邊界。 幸運的是,JVM轉儲也給了我們一個內存映射。事實證明,這個內存地址指向到一個本地共享庫:

7fb11046d000-7fb11046e000 rw-p 00007000 ca:01 77 /lib/x86_64-linux-gnu/libnss_dns-2.15.so

7fb11046f000-7fb110473000 r--s 0008a000 ca:01 396307 /opt/jdk1.7.0/jre/lib/jsse.jar

rdi指向內存映射中有缺陷一頁的起始位置。這表明在e230執(zhí)行時存在的一些內容在e242執(zhí)行時被釋放。

但也有可能是調用者不打算使用這個地址,我們注意到rdi恰好是當前堆棧幀之上大約2^32個字節(jié)處。但是,我們仍然不知道問題出在哪里。

在重現程序崩潰10次后,我們得到了一個稍微不同的情況。再一次,我們得到一個指向POSIX線程函數的垃圾指針。 但是這一次,我們也從轉儲中得到了一個Java堆棧跟蹤。

事實證明,JRE的代碼只是委托給libccm,就像用一個嬰兒車去當成汽車使用。它會破壞POSIX線程,或者導致JVM持續(xù)的占用內存。

我們如何處理

最終的解決方案遠沒有查找問題原因本身讓人感到興奮。我們發(fā)現使用Apache Commons Imaging圖像處理類庫代替Java ImageIO庫就可以解決問題。 但是,一般來說,解決一個復雜的問題的時候,分析問題往往比修復問題花費更多的時間和精力。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容