前言
在服务器运行过程中,会遇到各种各样的内存溢出异常OutOfMemory,今天介绍一种容易被忽视的异常内存情况:堆外内存。
现象
- 发现服务器不定时地会抛出内存溢出异常;
- 尝试把堆开到最大(32位系统最多到1,6G),没有任何效果,反而异常频繁;
- 加入-XX:+HeapDumpOnOutOfMemoryError,没有任何反应,内存溢出时不会产生dump文件;
- jstat工具监测,GC并不频繁;
- 查看系统日志如下:
[org.eclipse.jetty.util.log] handle failed java.lang.OutOfMemoryError: null at sun.misc.Unsafe.allocateMemory(Natave Method) at java.nio.DirecByteBuffer.<init>(DirectByteBuffer.java:99) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)\ at org.eclipse.jetty.io.nio.DirectNIOBuffer.<init> ......
分析
此时,看到上面的异常现象后,可以得知异常的原因是因为堆外内存不够引起。32位系统下,给Java堆分配的内存越大,堆外内存越小,因为总量是固定的,最大是2G。
异常关键原因:
垃圾收集时,虚拟机会对Direct Memory进行回收,但是不会主动回收。只能等到老年代满了后,发送Full GC时,才会对Direct Memory进行回收,这是一个“顺便清理”的过程。所以,当Direct Memory不够用时,自然会抛出内存溢出异常。
扩展
除了Java堆和永久代之外,下面这几个区域也会占用较多的内存,这里所有内存的总和受到操作系统进程最大内存的限制。
- Direct Memory
可通过-XX:MaxDirectMemorySize调整大小。
内存不足时,抛出OurtOfMemoryError或者OutOfMemoryError:Direct buffer memory。- 线程堆栈
可通过-Xss调整大小。
内存不足时,抛出StackOverflowError(纵向无法分配,即无法分配新的栈帧);或者OutOfMemoryError:unable to create new thread(横向无法分配,即无法建立新的线程)。- Socket缓存区
每个Socket连接都会有相应的两个缓存区,即Receive和Send,大小分别约37KB和25KB。链接数量多的话,这块内存区域的开销也比较大。如果无法分配,则可能会抛出IOException:Too many open files异常。- JNI代码
如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中。- 虚拟机和GC
虚拟机、GC的代码执行,也要消耗一定的内存。