当前位置:首页 > 技术 > 正文内容

Java OutOfMemoryError 常见问题排查与解决方案

访客 技术 2026年8月30日 1

当 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

原因分析:

  1. 创建超大对象: 例如一次性加载过大的图片或创建巨型数组。
  2. 请求量/数据量激增: 业务高峰期(如促销活动)导致处理的数据量远超预期。
  3. 过度使用 Finalizer: 对象持有 Finalizer,导致 GC 回收延迟。
  4. 内存泄漏: 大量对象被长期持有引用,无法被 GC 回收。

解决方案:

  1. 调整堆内存上限: 通过 -Xmx 参数增加 JVM 堆内存。
  2. 检查对象合理性: 审查是否需要创建如此大的对象,是否可以分批处理(如数据库查询限制返回条数)。
  3. 应对业务高峰: 考虑增加服务器资源或实施流量控制、熔断降级策略。
  4. 解决内存泄漏: 使用内存分析工具定位并修复持有对象引用的代码。

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)内存已满。这通常是由于加载了过多或过大的类定义、常量池等信息造成的。

解决方案:

  1. 程序启动时报错: 增加永久代大小,通过 -XX:MaxPermSize 参数。
  2. 应用重新部署时报错: 检查是否未完全停止旧进程,导致类定义重复加载。重启 JVM 通常能解决。
  3. 运行时报错: 如果应用动态生成大量类,且这些类生命周期短暂,但 JVM 默认不会卸载。可尝试启用类卸载:-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC
  4. 内存分析: 使用 jmap 命令生成堆转储文件,再用 Eclipse MAT 等工具分析。

4. Metaspace (JDK 8 及以后)

错误信息: java.lang.OutOfMemoryError: Metaspace

原因分析: JDK 8 引入 Metaspace 替代了永久代。此错误表示 Metaspace 内存已满,原因与 Permgen space 类似,通常是加载了过多类定义。

解决方案:

  1. 调整 Metaspace 大小: 使用 -XX:MaxMetaspaceSize 参数。
  2. 分析类加载: 检查是否有异常的类加载或类定义。

5. Unable to create new native thread

错误信息: java.lang.OutOfMemoryError: Unable to create new native thread

原因分析: JVM 无法创建新的本地(native)线程。每个线程都需要消耗一定的操作系统资源(包括内存)。

  1. 线程数过多: 超过操作系统 ulimit 限制的最大用户进程数。
  2. 系统资源耗尽: 物理内存或虚拟内存不足以支持新线程的创建。
  3. 线程泄漏: 应用程序创建了大量线程但未正确销毁。

解决方案:

  1. 增加系统内存: 升级服务器配置。
  2. 减少线程栈大小: 使用 -Xss 参数。
  3. 限制线程池大小: 合理配置线程池。
  4. 修复线程泄漏: 检查代码中线程的创建与销毁。
  5. 调整 OS 线程限制: 使用 ulimit -u 命令增加最大用户进程数。

6. Out of swap space?

错误信息: java.lang.OutOfMemoryError: Out of swap space?

原因分析: 系统的可用虚拟内存(物理内存 + 交换空间)已被耗尽。

  1. 地址空间不足: 尤其在 32 位系统上。
  2. 物理内存耗尽。
  3. Native 内存泄漏: C/C++ 代码或其他本地库申请内存后未释放。

解决方案:

  1. 升级至 64 位系统。
  2. 增加物理内存或交换空间。
  3. 排查 Native 内存泄漏: 使用 jmap -histo:live 检查 Direct ByteBuffer 使用情况,或使用专门的 Native 内存分析工具。

7. Kill process or sacrifice child

错误信息: 进程被操作系统 OOM Killer 终止。

原因分析: 操作系统发现可用内存极低,启动 OOM Killer 机制,根据评分机制选择并"杀死"评分较低的进程以释放内存。这并非 JVM 自身抛出的 OOM,而是操作系统层面的行为。

解决方案:

  1. 增加服务器内存。
  2. 优化进程资源使用: 减少内存占用,避免资源争抢。
  3. 调整 OOM Killer 策略 (不推荐轻易修改)。

8. Requested array size exceeds VM limit

错误信息: java.lang.OutOfMemoryError: Requested array size exceeds VM limit

原因分析: 尝试创建一个大小超过 JVM 限制的数组。JVM 对数组大小有限制,通常是 Integer.MAX_VALUE - 2 左右。

解决方案:

  1. 检查代码逻辑: 确认是否确实需要如此大的数组,考虑拆分为多个小数组分批处理。

9. Direct buffer memory

错误信息: java.lang.OutOfMemoryError: Direct buffer memory

原因分析: 使用 NIO 或其他方式通过 ByteBuffer.allocateDirect() 分配的堆外内存(Direct Buffer)超出了 JVM 的限制。

解决方案:

  1. 调整堆外内存上限: 使用 -XX:MaxDirectMemorySize 参数。
  2. 排查堆外内存泄漏: 检查代码中是否有未正确释放的 Direct Buffer。
  3. 显式释放: 如果使用了 sun.misc.Cleaner,考虑手动调用其 clean() 方法。
  4. 检查 -XX:+DisableExplicitGC 参数: 该参数会使 System.gc() 失效,可能影响 Direct Buffer 的回收。
  5. 升级内存配置。

排查 Direct Buffer 问题时,可使用 Arthas 等工具拦截 ByteBuffer.allocateDirect() 方法。

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。