当前位置: 首页 > article >正文

Java 内存溢出(OOM)问题的排查与解决

在 Java 开发中,内存溢出(OutOfMemoryError,简称 OOM)是一个常见且棘手的问题。相比于数组越界、空指针等业务异常,OOM 问题通常更难定位和解决。本文将通过一次线上内存溢出问题的排查过程,分享从问题表现到最终解决的完整思路,希望能为遇到类似问题的开发者提供参考。

1 内存溢出与内存泄露

在 Java 中,与内存相关的问题主要有两种:内存溢出内存泄露

  • 内存溢出(Out Of Memory):指应用程序申请内存时,JVM 没有足够的内存空间。可以形象地理解为“去蹲坑发现坑位满了”。
  • 内存泄露(Memory Leak):指应用程序申请了内存但没有释放,导致内存空间浪费。可以形象地理解为“有人占着茅坑不拉屎”。

1.1 内存溢出

在 JVM 的内存区域中,除了程序计数器,其他内存区域都有可能发生内存溢出。Java 堆是存储对象实例的区域,只要不断创建对象,并确保这些对象与 GC Roots 之间存在可达路径,避免被垃圾回收机制清除,就一定会发生内存溢出。

例如,以下代码会不断创建对象,最终导致内存溢出:

public class OOM {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        while (true) {
            list.add(new Object());
        }
    }
}

运行该程序时,可以通过设置 JVM 参数 -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError 来限制堆内存大小为 20M,并在发生 OOM 时生成内存快照。
在这里插入图片描述

1.2 内存泄露

内存泄露是指程序中动态分配的堆内存由于某种原因未能释放,导致系统内存浪费,进而可能引发程序运行速度减慢甚至系统崩溃。简单来说,内存泄露是由于应该被垃圾回收的对象未能被回收,导致内存占用不断增加,最终可能导致内存溢出。

例如,以下代码中,数据库连接未关闭,导致内存泄露:

public class MemoryLeak {
    public static void main(String[] args) {
        try {
            Connection conn = null;
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("url", "", "");
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("....");
        } catch (Exception e) {
            // 异常日志
        } finally {
            // 1. 关闭结果集 Statement
            // 2. 关闭声明的对象 ResultSet
            // 3. 关闭连接 Connection
        }
    }
}

如果连接未关闭,GC 将无法回收相关对象(如 ConnectionStatementResultSet 等),从而导致内存泄露。

换句话说,内存泄露不是内存溢出,但会加快内存溢出的发生。

2 内存溢出的表现

在生产环境中,内存溢出问题通常随着业务量的增长而频繁出现。例如,某应用程序从 Kafka 消费数据并进行批量持久化操作,随着 Kafka 消息量的增加,OOM 问题出现的频率也越来越高。虽然重启可以暂时解决问题,但这并非长久之计。

3 内存泄露的排查

为了排查内存泄露问题,首先需要分析运维收集的内存数据和 GC 日志。通过 jstat 工具可以发现,老年代的内存使用率即使在发生 Full GC 后仍然居高不下,且随着时间的推移逐渐增加。这表明应用程序中存在大量无法回收的对象。
在这里插入图片描述

4 内存泄露的定位

由于生产环境的内存快照文件较大(几十 GB),使用 MAT(Memory Analyzer Tool)进行分析耗时较长。因此,我们尝试在本地复现问题。通过将本地应用的最大堆内存设置为 150M,并模拟 Kafka 数据消费,使用 VisualVM 监控内存和 GC 情况。

经过多次尝试,发现只有在模拟生产环境的数据量(每次从 Kafka 取出几百条数据)时,才能复现内存溢出问题。通过 VisualVM 的 HeapDump 功能,发现 com.lmax.disruptor.RingBuffer 类型的对象占用了近 50% 的内存。
在这里插入图片描述

5 内存泄露的解决

通过代码审查,发现从 Kafka 取出的数据直接放入 Disruptor 环形队列中,而队列的大小配置为 1024 * 1024,导致内存中积累了大量的对象。通过将队列大小调整为较小的值(如 2),问题得到解决。
在这里插入图片描述

Disruptor 是一个高性能的异步处理框架,它的核心思想是:通过无锁的方式来实现高性能的并发处理,其性能是高于 JDK 的 BlockingQueue 的。

6 总结

虽然最终只是修改了一行代码(或配置),但整个排查过程非常有意义。通过这次经历,我们可以更好地理解 JVM 内存管理的机制,并掌握排查内存溢出和内存泄露问题的基本方法。同时,也提醒我们在使用高性能框架(如 Disruptor)时,必须谨慎配置参数,避免因不当使用而导致内存问题。

7 思维导图

在这里插入图片描述

8 参考链接

一次内存溢出的排查优化实战,彻底干掉臭名昭著的 OOM


http://www.kler.cn/a/467597.html

相关文章:

  • 前端学习DAY31(子元素溢出父元素)
  • 04-spring-理-ApplicationContext的实现
  • ESP32-C3 AT WiFi AP 启 TCP Server 被动接收模式 + BLE 共存
  • qt鼠标右键菜单
  • Echart实现3D饼图示例
  • GitHub 基础使用指南
  • 《攀爬者》
  • 探讨面向未来的框架新技术:逻辑驱动和自适应框架的突破
  • k8s集群,CRI-Docker部署条件及方法
  • Spring Cloud微服务多模块架构:父子工程搭建实践
  • 提示词教程:零样本提示
  • ArkTs-@Builder引用传递问题
  • 【MongoDB详解】
  • 旧服务改造及微服务架构演进
  • 如何在不丢失数据的情况下从 IOS 14 回滚到 IOS 13
  • 现代光学基础6
  • ruckus R510升级到Unleashe后不能访问
  • 端到端性能体验稳定性优化常见方案
  • webpack01
  • Elasticsearch 文档批处理 混合处理 批量操作
  • (四)基于STM32通过Event Recoder实现时间测量功能
  • Android中创建ViewModel的几种方法
  • 体验谷歌最新Gemini 2.0 Flash原生多模态音视频对话桌面分享功能
  • 数据结构C语言描述7(图文结合)--哈希、哈希冲突、开放地址法、链地址法等实现
  • 阿里云效自动化部署 Docker镜像
  • 电子电气架构 --- 整车整车网络管理浅析