2023年11月29日发(作者:)

⼜见OutOfMemory——⼀次内存溢出故障诊断全过程聚沙成塔-⼩哈的记事薄

这是⼀个⼏⽉前的案例,问题⽐较典型,在分析和事后学习的过程中让我对本地内存溢出有了⼀定的了解。在此和⼤家分享。

先说⼀下背景,应⽤环境是AIX5.3+WebSphere6.0.2.37。在今年的⼀季度曾发⽣过⼏次OOM故障,当时通过⼏次内存参数优化,最后

确定为“-Xgcprolicy:gencon –Xms512m –Xmx1280m –Xmn200m”,此后稳定了半年,直到此次再发⽣应⽤宕机。

赶到现场,发现profiles⽬录下⽣成有javacoreheapdump⽂件,Javacore的第⼀⾏“Cause of thread dump : Dump Event "systhrow"

(00040000) Detail "java/lang/OutOfMemoryError" received”表明了宕机的原因是OOM,但是令我困惑的是这段内容:

0SECTION MEMINFO subcomponent dump routine

NULL =================================

1STHEAPFREE Bytes of Heap Space Free: 818cb70

1STHEAPALLOC Bytes of Heap Space Allocated: 20000000

在分配的512MB(⼗六进制20000000)的堆空间中,有129MB818cb70)空闲空间,按理说,这种情况下不该发⽣OutOfMemory

就算有申请⼀个⼤对象,同时JVM堆的新⽣代由于碎⽚原因没有连续空间满⾜要求,那么应该发⽣堆扩展,所以此次内存溢出不是堆

Heap)溢出,GC⽇志的分析也⽀持了这⼀点。

既然Javacore⽆法得到有⽤信息,我把⽬光转向了,在对应⽇期的地⽅,我发现了⼤量如下报错:

[8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R Exception in thread "WebContainer : 1"

eException: emoryError: Failed to create a thread: retVal -1073741830, errno 11

[8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R at

ntProcessingLoop(:801)

[8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R at

Handler$(:881)

[8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R at

Pool$(:1497)

[8/26/10 14:12:57:860 GMT+08:00] 0000002f SystemErr R Caused by: emoryError: Failed to create a

thread: retVal -1073741830, errno 11

at mpl(Native Method)

at (:980)

at ead(:630)

at Pool$(:1148)

at ileged(:63)

at e(:1146)

at e(:1040)

at e(:151)

at andler(:248)

at ntProcessingLoop(:570)

at Handler$(:881)

at Pool$(:1497)

在事后的学习中,我知道“emoryError: unable to create native thread” 这样的异常是在说,本地内存耗尽,从⽽新的线

程⽆法创建。⽽在当时我第⼀感觉是操作系统参数设置问题,之前我曾写过⼀篇由于nofile参数导致Too many open file的故障。于是我

运⾏如下命令

#lsattr -El sys0 -a maxuproc

maxuproc 128 Maximum number of PROCESSES allowed per user True

然后运⾏chgsys修改默认的1281024,这⾥我犯了⼀个错误,WebSphere单个Server就是⼀个Java进程,错误⽇志⾥是不能创

建⼀个thread,⽽⾮process,与Oracle会创建多个oracle进程不⼀样。果然两天后⼜出现了同样的问题。

这⼀次的SystemErr⽇志中,除了上述的内容,还多了

[8/24/10 9:55:19:813 GMT+08:00] 00000036 SystemErr R Exception in thread "WebContainer : 4"

eException: emoryError: 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

ntProcessingLoop(:801)

[8/24/10 9:55:19:813 GMT+08:00] 00000036 SystemErr R at

Handler$(:881)

[8/24/10 9:55:19:813 GMT+08:00] 00000036 SystemErr R at

Pool$(:1497)

[8/24/10 9:55:19:813 GMT+08:00] 00000036 SystemErr R Caused by: emoryError: Unable to allocate

8192 bytes of direct memory after 5 retries

at ByteBuffer.(:197)

at ByteBuffer.(:197)

at teDirect(:303)

at

teBufferDirect(:656)

at

teCommon(:570)

at teDirect(:506)

at ntProcessingLoop(:498)

at Handler$(:881)

at Pool$(:1497)

Caused by: emoryError

at teMemory(Native Method)

at ByteBuffer.(:184)

… 7 more

我们可以看到是由于DirectByteBuffer⽆法分配导致的内存溢出,⽽Native Method指明了这是本地的溢出。通过这两个关键字,我查到

IBM的⼀份BUG记录:PK31010: OUTOFMEMORYERROR DUE TO DIRECTBYTEBUFFER,但是我的版本已是最新,⽆奈继续搜

寻。

Troubleshooting native memory issues这份⽂档中,介绍了3种在WebSphere中最常见的导致OOM的原因。其中第⼆

DirectByteBuffer use就是我们问题的症结。

Java 1.4 中添加的新 I/O NIO 类引⼊了⼀种基于通道和缓冲区来执⾏ I/O 的新⽅式。就像 Java 堆上的内存⽀持 I/O 缓冲

区⼀样,NIO 添加了对直接 ByteBuffer 的⽀持(使⽤teDirect() ⽅法进⾏分配),ByteBuffer

本机内存⽽不是 Java 堆⽀持。直接 ByteBuffer 可以直接传递到本机操作系统库函数,以执⾏ I/O — 这使这些函数在⼀些场

景中要快得多,因为它们可以避免在 Java 堆与本机堆之间复制数据。

对于在何处存储直接 ByteBuffer 数据,很容易产⽣混淆。应⽤程序仍然在 Java 堆上使⽤⼀个对象来编排 I/O 操作,但持

有该数据的缓冲区将保存在本机内存中,Java 堆对象仅包含对本机堆缓冲区的引⽤。⾮直接 ByteBuffer 将其数据保存在

Java 堆上的 byte[] 数组中。下图展⽰了直接与⾮直接 ByteBuffer 对象之间的区别:

直接与⾮直接 ffer 的内存拓扑结构

直接 ByteBuffer 对象会⾃动清理本机缓冲区,但这个过程只能作为 Java GC 的⼀部分来执⾏,因此它们不会⾃动响应

施加在本机堆上的压⼒。GC 仅在 Java 堆被填满,以⾄于⽆法为堆分配请求提供服务时发⽣,或者在 Java 应⽤程序中显式

请求它发⽣(不建议采⽤这种⽅式,因为这可能导致性能问题)。

发⽣垃圾收集的情形可能是,本机堆被填满,并且⼀个或多个直接 ByteBuffers 适合于垃圾收集(并且可以被释放来腾出

本机堆的空间),但 Java 堆⼏乎总是空的,所以不会发⽣垃圾收集。

摘⾃《理解JVM如何使⽤WindowsLinux上的本机内存》

解决此问题的⽅法,在⽂档中给出的是禁⽌异步A/O,通过在Web Container中设置参数来避免上节中所出现的由于Java堆空闲⽽不发

⽣垃圾回收,导致本地堆撑满的情况。

⽣垃圾回收,导致本地堆撑满的情况。

Servers -> Application Servers -> serverName -> Web Container Settings -> Web Container -> Custom Properties:

Press New:

Add the following pair:

Name: lwritetype

Value: sync

Press OK, and then save the configuration.

添加此属性后应⽤⼜恢复正常,但是原先提⾼性能的特性反⽽导致内存溢出,违背了初衷,现在的做法只能算⼀个妥协。我会继续查找

此问题的解决⽅法,最不济也要有个使⽤NIOBest Practice

⽂档中另两个容易导致本地堆OOM的原因是:

过⼤的堆上限

我们知道32位机器单个进程可以访问的内存地址空间为4G,如右图所⽰,但实际情况下Windows

统由于内核态和⽤户态的划分,⽤户态只有2G的空间,LinuxAIX的可⽤空间⼤⼀点,但也在3G

右。,由于JVM实例进程寻址是⼀定的,所以Heap⼤⼩和Native Area此消彼长。⽽Native Area中有

⼀部分就是供给线程的内存空间。

应⽤程序中的每个线程都需要内存来存储器堆栈(⽤于在调⽤函数时持有局部变量并维护状态的内

存区域)。每个 Java 线程都需要堆栈空间来运⾏。根据实现的不同,Java 线程可以分为本机线程和

Java 堆栈。除了堆栈空间,每个线程还需要为线程本地存储(thread-local storage)和内部数据结

构提供⼀些本机内存。堆栈⼤⼩因 Java 实现和架构的不同⽽不同。⼀些实现⽀持为 Java 线程指定

堆栈⼤⼩,其范围通常在 256KB 756KB 之间。

1.5后⼀般线程堆栈⼤⼩为1M对于拥有数百个线程的应⽤程序来说,线程堆栈的总内存使⽤量可能⾮常⼤。如果运⾏的应⽤程序的线

程数量⽐可⽤于处理它们的处理器数量多,效率通常很低,并且可能导致糟糕的性能和更⾼的内存占⽤(摘⾃《理解JVM如何使⽤

WindowsLinux上的本机内存》)

解决此问题的⽅法:设置恰当的JVM最⼤堆,32位不要超过1.5G;设置恰当的线程池⼤⼩(⼀般50100),当线程使⽤较多时, 使

-Xss256k参数设置线程指定堆栈⼤⼩(IBM JDK还可以使⽤-Xmso ××k来设置Stack size for OS Threads 32-bit);更换64位的JDK

第三个原因是线程池的TLS泄漏

这个现象我倒没见到过,如果你的thread dump⾥下⾯三个属性⾥的值有⼤于300,那么就要注意了

"WebContainer : 1003" (TID:0×37D62000

"Default : 338" (TID:109934D0

" : 303" (TID:0×4D720000

解决的⽅法是设置线程池的最⼩最⼤值⼀致。