概述
在 Java 程序开发过程中,不可避免的会出现 OOM 内存溢出的问题,本文对各种 OOM 进行详细说明。
JVM 内存
在 JVM 运行时数据区中,只有寄存器不会出现 OOM。
出现 OOM 的情况有:
- 堆内存耗尽:对象越来越多,又一直在使用,不能被垃圾回收
- 方法区内存耗尽:加载的类越来越多,很多框架都会在运行期间动态产生新的类
- 虚拟机栈累计:每个线程最多会占用1M内存,线程个数越来越多,而又长时间运行不销毁时出现
StackOverflowError 的区域:
- 虚拟机栈内部:方法调用次数过多
OOM 常见的种类
-
OutOfMemoryError–Java heap space
-
OutOfMemoryError–GC overhead limit exceeded
-
OutOfMemoryError–Metaspace/Permgen space
-
OutOfMemoryError–Unable to create new native thread
-
OutOfMemoryError–Out of swap space
-
OutOfMemoryError–Requested array size exceeds VM limit
-
OutOfMemoryError–Direct buffer memory
-
OutOfMemoryError–Kill process or sacrifice child
OutOfMemoryError–Java heap space
Java 堆用于存储对象实例,只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
/**
* Java堆内存溢出异常测试
* <p>
* -Xms20m -Xmx20m -XX:HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while (true) {
list.add(new OOMObject());
}
}
}
原因分析
- 配置的堆内存太小,或者机器内存不够
- 代码中可能存在大对象分配
- 可能存在内存泄露,导致在多次 GC 之后,还是无法找到一块足够大的内存容纳当前对象。
解决方法
- 检查是否存在大对象的分配,最有可能的是大数组分配
- 通过 jmap 命令,把堆内存 dump 下来,使用 mat 工具分析一下,检查是否存在内存泄露的问题
- 如果没有找到明显的内存泄露,使用 -Xmx 加大堆内存
- 还有一点容易被忽略,检查是否有大量的自定义的 Finalizable 对象,也有可能是框架内部提供的,考虑其存在的必要性
OutOfMemoryError–GC overhead limit exceeded
原因分析
当GC花费了程序运行总时间的98%以上,而回收不到2%的堆,则抛出该异常。
The parallel collector throws an OutOfMemoryError if too much time is being spent in garbage collection (GC): If more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, then an OutOfMemoryError is thrown. This feature is designed to prevent applications from running for an extended period of time while making little or no progress because the heap is too small. If necessary, this feature can be disabled by adding the option -XX:-UseGCOverheadLimit to the command line.
这个异常不是很容易重现。因为 GC 时也意味着堆内存不够,可能实际抛出的是更常见的java.lang.OutOfMemoryError: Java heap space。
解决方法
-
检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
-
dump内存,检查是否存在内存泄露,如果没有,加大内存。
添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
OutOfMemoryError-- Metaspace/Permgen space
永久代是 HotSot 虚拟机对方法区的具体实现,存放了被虚拟机加载的类信息、常量、静态变量、JIT编译后的代码等。
JDK8 后,元空间替换了永久代,元空间使用的是本地内存,还有其它细节变化:
- 字符串常量由永久代转移到堆中
- 和永久代相关的JVM参数已移除
原因分析
- 在 Java7 之前,频繁的错误使用 String.intern() 方法
- 运行期间生成了大量的代理类,导致方法区被撑爆,无法卸载
解决方法
- 检查是否永久代空间或者元空间设置的过小
- 检查代码中是否存在大量的反射操作
- dump 之后通过 mat 检查是否存在大量由于反射生成的代理类
- 重启 JVM
OutOfMemoryError–Unable to create new native thread
每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 native 线程时,如果没有足够的资源分配就会报此类错误。
原因分析
- 线程数超过操作系统最大线程数 ulimit 限制
- 线程数超过 kernel.pid_max(只能重启)
- native 内存不足;
解决方案
- 升级配置,为机器提供更多的内存;
- 降低 Java Heap Space 大小;
- 修复应用程序的线程泄漏问题;
- 限制线程池大小;
- 使用 -Xss 参数减少线程栈的大小;
- 调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制。
OutOfMemoryError–Out of swap space
JVM 请求的总内存大于可用物理内存的情况下,操作系统开始将内存从内存交换到硬盘驱动器,物理内存和交换空间的缺乏,分配失败
原因分析
一般来说,JVM 有自己的 GC 机制,如果出现这个异常,通常意味着是下面的原因:
操作系统配置的交换空间不足。
系统上的另一个进程正在消耗所有内存资源。
解决思路
其它服务进程可以选择性的拆分出去
最简单的解决方法时增加交换空间
例如在Linux中,使用以下命令来实现,创建并附加一个大小为640MB的新swapfile
一般来说,这个异常意味着你要升级内存或者意味这个你的一台机器不够用了,用户请求过多或者数据量很大,需要集群。
OutOfMemoryError–Requested array size exceeds VM limit
这种情况一般是由于不合理的数组分配请求导致的,在为数组分配内存之前,JVM 会执行一项检查。要分配的数组在该平台是否可以寻址(addressable),如果不能寻址(addressable)就会抛出这个错误。
JVM 限制了数组的最大长度,该错误表示程序请求创建的数组超过最大长度限制。
解决方法
检查代码中是否有创建超大数组的地方,避免创建大数组。
OutOfMemoryError–Direct buffer memory
Java 允许应用程序通过 Direct ByteBuffer 直接访问堆外内存,许多高性能程序通过 Direct ByteBuffer 结合内存映射文件(Memory Mapped File)实现高速 IO。
原因分析
Direct ByteBuffer 的默认大小为 64 MB,一旦使用超出限制,就会抛出 Directbuffer memory
错误。
解决方案
- Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查
- 检查是否直接或间接使用了 NIO,如 netty,jetty 等
- 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值
- 检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc() 失效
- 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner 的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间
- 内存容量确实不足,升级配置
OutOfMemoryError–Kill process or sacrifice child
有一种内核作业(Kernel Job)名为 Out of Memory Killer,它会在可用内存极低的情况下“杀死”(kill)某些进程。OOM Killer 会对所有进程进行打分,然后将评分较低的进程“杀死”,具体的评分规则可以参考 Surviving the Linux OOM Killer。
不同于其他的 OOM 错误, Killprocessorsacrifice child 错误不是由 JVM 层面触发的,而是由操作系统层面触发的。
原因分析
默认情况下,Linux 内核允许进程申请的内存总量大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源。然而,这种方式也会无可避免地带来一定的“超卖”风险。例如某些进程持续占用系统内存,然后导致其他进程没有可用内存。此时,系统将自动激活 OOM Killer,寻找评分低的进程,并将其“杀死”,释放内存资源。
解决方案
- 那么尽量控制堆的大小,让它不要对整个服务器造成很大的影响,所有我们通常会限制最大堆不超过整个操作系统资源的80%。
- 升级服务器配置/隔离部署,避免争用。
- OOM Killer 调优。
评论区