Java OutOfMemoryError 常见问题排查与解决方案
当 Java 虚拟机 (JVM) 内存资源严重不足时,会抛出 java.lang.OutOfMemoryError 异常。本文将梳理几种常见的 OOM 类型,分析其根本原因,并提供相应的排查与解决思路。
OOM 产生的根本原因
JVM 进程受到操作系统对单个进程最大内存占用的限制。当应用程序申请的内存资源超过这个限制时,即使系统还有剩余内存,也会触发 OOM 错误。这与整个设备的可用内存量关系不大。
Android 应用内存构成
Android 应用的内存主要由两部分组成:
- Dalvik/ART 堆 (Java Heap): 用于分配 Java 对象。
- Native 内存: 通过 C/C++ 代码申请的内存,例如 Bitmap 在 Android 3.0 之前的分配方式(之后大部分也通过 Dalvik 堆管理)。
这两部分内存的总和不得超过操作系统为单个进程或虚拟机设定的内存上限。
可以通过 /system/build.prop 文件中的参数了解堆内存大小限制,例如:
dalvik.vm.heapstartsize = 5m
dalvik.vm.heapgrowthlimit = 48m
dalvik.vm.heapsize = 256m
其中 heapgrowthlimit 参数通常会限制程序运行时实际可用的堆内存大小。
GC (垃圾回收) 与 OOM
JVM 拥有自动垃圾回收机制,用于回收不再被引用的对象所占用的内存。然而,GC 主要回收的是"无主"对象或被弱引用的对象。对于被强引用的对象,即使不再被主动使用,GC 也不会轻易回收。
// 强引用,bt 即使设为 null,GC 仍可能等待一段时间才回收
Bitmap bt = BitmapFactory.decodeResource(this.getResources(), R.drawable.splash);
bt = null;
// 软引用,GC 在内存不足时会回收
SoftReference<Bitmap> softRef = new SoftReference<Bitmap>(bt);
bt = null; // 此时 softRef 仍然持有对 Bitmap 的引用,但 GC 在内存压力大时可能回收
当内存申请速度远超 GC 回收速度,或者存在大量无法被 GC 回收的对象(如内存泄漏),就可能导致 OOM。
常见的 OOM 类型与解决方案
1. Java heap space
错误信息: java.lang.OutOfMemoryError: Java heap space
原因分析:
- 创建超大对象: 例如一次性加载过大的图片或创建巨型数组。
- 请求量/数据量激增: 业务高峰期(如促销活动)导致处理的数据量远超预期。
- 过度使用 Finalizer: 对象持有 Finalizer,导致 GC 回收延迟。
- 内存泄漏: 大量对象被长期持有引用,无法被 GC 回收。
解决方案:
- 调整堆内存上限: 通过
-Xmx参数增加 JVM 堆内存。 - 检查对象合理性: 审查是否需要创建如此大的对象,是否可以分批处理(如数据库查询限制返回条数)。
- 应对业务高峰: 考虑增加服务器资源或实施流量控制、熔断降级策略。
- 解决内存泄漏: 使用内存分析工具定位并修复持有对象引用的代码。
2. GC overhead limit exceeded
错误信息: java.lang.OutOfMemoryError: GC overhead limit exceeded
原因分析: JVM 花费超过 98% 的时间进行 GC,但仅回收不到 2% 的内存,且这种情况连续发生多次。表明 JVM 几乎耗尽所有可用内存,GC 已无能为力。
解决方案: 与 Java heap space 类似,通常是内存泄漏或堆内存不足的迹象。
3. Permgen space (JDK 7 及以前)
错误信息: java.lang.OutOfMemoryError: PermGen space
原因分析: JVM 的永久代(Permanent Generation)内存已满。这通常是由于加载了过多或过大的类定义、常量池等信息造成的。
解决方案:
- 程序启动时报错: 增加永久代大小,通过
-XX:MaxPermSize参数。 - 应用重新部署时报错: 检查是否未完全停止旧进程,导致类定义重复加载。重启 JVM 通常能解决。
- 运行时报错: 如果应用动态生成大量类,且这些类生命周期短暂,但 JVM 默认不会卸载。可尝试启用类卸载:
-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC。 - 内存分析: 使用
jmap命令生成堆转储文件,再用 Eclipse MAT 等工具分析。
4. Metaspace (JDK 8 及以后)
错误信息: java.lang.OutOfMemoryError: Metaspace
原因分析: JDK 8 引入 Metaspace 替代了永久代。此错误表示 Metaspace 内存已满,原因与 Permgen space 类似,通常是加载了过多类定义。
解决方案:
- 调整 Metaspace 大小: 使用
-XX:MaxMetaspaceSize参数。 - 分析类加载: 检查是否有异常的类加载或类定义。
5. Unable to create new native thread
错误信息: java.lang.OutOfMemoryError: Unable to create new native thread
原因分析: JVM 无法创建新的本地(native)线程。每个线程都需要消耗一定的操作系统资源(包括内存)。
- 线程数过多: 超过操作系统
ulimit限制的最大用户进程数。 - 系统资源耗尽: 物理内存或虚拟内存不足以支持新线程的创建。
- 线程泄漏: 应用程序创建了大量线程但未正确销毁。
解决方案:
- 增加系统内存: 升级服务器配置。
- 减少线程栈大小: 使用
-Xss参数。 - 限制线程池大小: 合理配置线程池。
- 修复线程泄漏: 检查代码中线程的创建与销毁。
- 调整 OS 线程限制: 使用
ulimit -u命令增加最大用户进程数。
6. Out of swap space?
错误信息: java.lang.OutOfMemoryError: Out of swap space?
原因分析: 系统的可用虚拟内存(物理内存 + 交换空间)已被耗尽。
- 地址空间不足: 尤其在 32 位系统上。
- 物理内存耗尽。
- Native 内存泄漏: C/C++ 代码或其他本地库申请内存后未释放。
解决方案:
- 升级至 64 位系统。
- 增加物理内存或交换空间。
- 排查 Native 内存泄漏: 使用
jmap -histo:live检查 Direct ByteBuffer 使用情况,或使用专门的 Native 内存分析工具。
7. Kill process or sacrifice child
错误信息: 进程被操作系统 OOM Killer 终止。
原因分析: 操作系统发现可用内存极低,启动 OOM Killer 机制,根据评分机制选择并"杀死"评分较低的进程以释放内存。这并非 JVM 自身抛出的 OOM,而是操作系统层面的行为。
解决方案:
- 增加服务器内存。
- 优化进程资源使用: 减少内存占用,避免资源争抢。
- 调整 OOM Killer 策略 (不推荐轻易修改)。
8. Requested array size exceeds VM limit
错误信息: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
原因分析: 尝试创建一个大小超过 JVM 限制的数组。JVM 对数组大小有限制,通常是 Integer.MAX_VALUE - 2 左右。
解决方案:
- 检查代码逻辑: 确认是否确实需要如此大的数组,考虑拆分为多个小数组分批处理。
9. Direct buffer memory
错误信息: java.lang.OutOfMemoryError: Direct buffer memory
原因分析: 使用 NIO 或其他方式通过 ByteBuffer.allocateDirect() 分配的堆外内存(Direct Buffer)超出了 JVM 的限制。
解决方案:
- 调整堆外内存上限: 使用
-XX:MaxDirectMemorySize参数。 - 排查堆外内存泄漏: 检查代码中是否有未正确释放的 Direct Buffer。
- 显式释放: 如果使用了
sun.misc.Cleaner,考虑手动调用其clean()方法。 - 检查
-XX:+DisableExplicitGC参数: 该参数会使System.gc()失效,可能影响 Direct Buffer 的回收。 - 升级内存配置。
排查 Direct Buffer 问题时,可使用 Arthas 等工具拦截 ByteBuffer.allocateDirect() 方法。