告别OOM!SpringBoot内存泄漏的11个排查方法

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

👉这是一个或许对你有用的开源项目

国产Star破10w的开源项目,前端包括管理后台、微信小程序,后端支持单体、微服务架构

RBAC权限、数据权限、SaaS多租户、商城、支付、工作流、大屏报表、ERP、CRMAI大模型、IoT物联网等功能:

  • 多模块:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • 微服务:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK17/21+SpringBoot3、JDK8/11+Spring Boot2双版本 

来源:风象南


一、引言

内存泄漏是项目开发中常见且棘手的问题,它会导致应用性能下降、响应变慢,严重时甚至会引发OutOfMemoryError异常导致应用崩溃。

与传统的Java应用相比,SpringBoot应用因其丰富的组件生态和依赖注入的特性,内存泄漏问题可能更加隐蔽和复杂。

本文将介绍多种实用的方法来排查应用中的内存泄漏问题。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

二、内存泄漏基础知识

在深入排查方法之前,先简单回顾一下内存泄漏的基本概念:

内存泄漏(Memory Leak) :程序分配的内存由于某种原因无法被释放,导致这部分内存一直被占用,无法被GC回收。

在Java中,内存泄漏通常表现为对象被引用但实际上不再需要,从而无法被垃圾回收器回收。

SpringBoot应用中常见的内存泄漏原因包括:

静态集合类引用 :如静态的Map、List持有对象引用单例bean中的集合类引用 :Spring的单例bean生命周期与应用一致未关闭的资源 :数据库连接、文件流等不当的缓存使用 :无界缓存或缓存过期策略设置不当线程池管理不当 :任务队列无限增长JNI调用未释放的本地内存类加载器泄漏 :如WebappClassLoader在热部署时未释放

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

三、内存泄漏排查方法

1. JVM启动参数配置与GC日志分析

通过配置适当的JVM参数,可以记录详细的GC日志,帮助分析内存使用情况。

实施步骤 :

  1. 添加以下JVM参数启用GC日志:

-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-Xloggc:/path/to/gc.log
  1. 在SpringBoot应用中,可以在application.properties中配置:

spring.jvm.args=-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log
  1. 使用GCViewer等工具分析GC日志,关注以下指标:

  • Full GC频率异常增高

  • GC后内存回收效果不明显

  • 老年代内存持续增长

示例GC日志片段分析 :

2023-08-10T14:15:30.245+0800: [GC (Allocation Failure) [PSYoungGen: 786432K->9437K(917504K)] 786432K->9445K(3014656K), 0.0088311 secs]
2023-08-10T14:16:30.377+0800: [GC (Allocation Failure) [PSYoungGen: 795869K->8941K(917504K)] 795877K->23757K(3014656K), 0.0102321 secs]
2023-08-10T14:17:30.502+0800: [GC (Allocation Failure) [PSYoungGen: 795373K->10022K(917504K)] 810189K->54038K(3014656K), 0.0143901 secs]

注意观察GC后内存占用持续增长的趋势,这可能表明存在内存泄漏。

2. 使用JConsole实时监控

JConsole是JDK自带的图形化监控工具,可以实时监控JVM内存、线程和类加载情况。

实施步骤 :

  1. 启动SpringBoot应用时添加JMX参数:

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9010
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
  1. 运行JConsole:jconsole命令或从JDK的bin目录启动

  2. 连接到目标应用,观察"内存"选项卡,特别关注以下区域:

  • 堆内存使用趋势(持续上升表明可能存在问题)

  • 永久代/元空间使用情况

  • GC活动频率

  1. 在"MBeans"选项卡中,可以查看Spring相关的Bean信息

3. VisualVM进行高级内存分析

VisualVM是一个功能更强大的分析工具,可以生成堆转储并分析内存使用情况。

实施步骤 :

  1. 下载并启动VisualVM(JDK 8之前自带,之后需单独下载)

  2. 连接到目标应用,在"应用程序"视图中选择你的应用

  3. 在"监视"选项卡观察内存使用趋势

  4. 使用"堆转储"按钮创建堆转储文件

  5. 在"类"视图中,按实例数量排序,查找异常增多的对象

  6. 检查可疑对象的引用链,找出引用源

分析技巧 :

  • 对比多个时间点的堆转储,观察哪些对象数量异常增长

  • 使用OQL(对象查询语言)进行高级查询

SELECT s FROM java.util.HashMap s WHERE s.size > 1000
4. MAT(Memory Analyzer Tool)详细堆分析

Eclipse Memory Analyzer是专门用于分析Java堆转储文件的工具,能够找出潜在的内存泄漏。

实施步骤 :

  1. 获取堆转储文件(可以使用VisualVM或jmap命令):

jmap -dump:format=b,file=heap.hprof <PID>
  1. 使用MAT打开堆转储文件

  2. 运行"Leak Suspects Report",自动分析可能的内存泄漏

  3. 使用"Dominator Tree"查看占用内存最多的对象

  4. 检查可疑对象的GC Roots和引用链

分析关键点 :

  • 关注"Retained Heap"列,它表示对象及其引用的所有对象占用的总内存

  • 使用"Path to GC Roots"查找阻止对象被回收的引用路径

  • 检查集合类(如HashMap、ArrayList)中的元素

5. 使用Spring Boot Actuator监控

Spring Boot Actuator提供了丰富的监控端点,可以用来监控应用内存使用情况。

实施步骤 :

  1. 添加Actuator依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. application.properties中开启相关端点:

management.endpoints.web.exposure.include=health,metrics,heapdump
management.endpoint.health.show-details=always
  1. 访问指标端点查看内存使用情况:

  • /actuator/metrics/jvm.memory.used - 查看内存使用

  • /actuator/metrics/jvm.gc.memory.promoted - 查看提升到老年代的内存

  • /actuator/heapdump - 下载堆转储文件

  1. 可以集成Prometheus和Grafana进行长期监控和告警

示例代码 - 自定义内存监控端点 :

@Component
@Endpoint(id = "memory-status")
public class MemoryStatusEndpoint {
    
    @ReadOperation
    public Map<String, Object> memoryStatus() {
        Map<String, Object> status = new HashMap<>();
        Runtime runtime = Runtime.getRuntime();
        
        long totalMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        long maxMemory = runtime.maxMemory();
        long usedMemory = totalMemory - freeMemory;
        
        status.put("total", bytesToMB(totalMemory));
        status.put("free", bytesToMB(freeMemory));
        status.put("used", bytesToMB(usedMemory));
        status.put("max", bytesToMB(maxMemory));
        status.put("usagePercentage", usedMemory * 100.0 / maxMemory);
        
        return status;
    }
    
    private double bytesToMB(long bytes) {
        return bytes / (1024.0 * 1024.0);
    }
}
6. 使用jstack分析线程堆栈

线程相关问题也可能导致内存泄漏,如线程池使用不当或线程持有大对象引用。

实施步骤 :

  1. 使用jstack命令获取线程转储:

jstack <PID> > thread_dump.txt
  1. 分析线程状态,关注以下点:

  • 大量BLOCKED状态的线程(可能表明有死锁)

  • 线程数量异常增多(可能有线程泄漏)

  • 线程堆栈深度异常(可能有递归或循环依赖)

  1. 结合jmap查看每个线程的内存占用:

jmap -histo:live <PID> | head -20

分析示例 :

"http-nio-8080-exec-10" #43 daemon prio=5 os_prio=0 tid=0x00007f8b5c34e800 nid=0x7f82 waiting on condition [0x00007f8a3bf7c000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
        at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)
7. 使用YourKit等商业工具进行全面分析

YourKit、JProfiler等商业工具提供了更全面的内存分析功能。

实施步骤 :

  1. 安装YourKit Java Profiler并配置应用连接

  2. 使用"内存"视图实时监控内存使用情况

  3. 创建多个堆快照进行对比分析

  4. 使用"对象计数"功能查看不同类型对象的数量变化

  5. 设置对象创建跟踪,找出创建大量对象的代码

特别功能 :

  • 内存泄漏检测器自动分析可能的泄漏

  • 可以捕获具体的内存分配点(allocation points)

  • 支持查看保留的内存分布

8. 数据库连接与资源泄漏检测

数据库连接、文件句柄等资源未正确关闭是常见的泄漏源。

实施步骤 :

  1. 使用数据库连接池监控功能,如HikariCP的指标:

spring.datasource.hikari.register-mbeans=true
  1. 通过JMX查看连接池状态:

  • 活跃连接数

  • 等待连接数

  • 总连接数

  1. 代码审查,确保所有资源都在try-with-resources块中使用:

// 正确方式
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement("SELECT * FROM users");
     ResultSet rs = ps.executeQuery()) {
    // 处理结果集
} catch (SQLException e) {
    logger.error("Database error", e);
}

// 错误方式 - 可能导致连接泄漏
Connection conn = null;
try {
    conn = dataSource.getConnection();
    // ...如果这里抛出异常,连接可能不会关闭
} finally {
    // 可能遗漏关闭或异常处理不当
}
  1. 使用lsof命令检查进程打开的文件句柄数:

lsof -p <PID> | wc -l
9. 使用BTrace进行运行时分析

BTrace是一个强大的Java运行时跟踪工具,可以在不重启应用的情况下动态分析对象创建和方法调用。

实施步骤 :

  1. 下载安装BTrace

  2. 编写BTrace脚本跟踪可疑方法:

import org.openjdk.btrace.core.annotations.*;
import static org.openjdk.btrace.core.BTraceUtils.*;

@BTrace
public class MemoryLeakTracer {
    @OnMethod(
        clazz="com.example.service.CacheService",
        method="addToCache"
    )
    public static void traceAdd(@Self Object self, @ProbeClassName String pcn, @ProbeMethodName String pmn, Object key, Object value) {
        println("Adding to cache: " + str(key));
        println("Cache size: " + get(field(classOf("com.example.service.CacheService"), "cache", self), "size"));
    }
}
  1. 将脚本附加到运行中的应用:

btrace <PID> MemoryLeakTracer.java
  1. 分析输出,寻找异常增长的集合或频繁创建的大对象

10. 代码审查常见内存泄漏模式

系统地审查代码中的常见内存泄漏模式可以有效预防问题。

需关注的模式 :

1. 静态集合 :

public class EventCollector {
    // 危险:无界静态集合
    private static final List<Event> ALL_EVENTS = new ArrayList<>();
    
    public void recordEvent(Event event) {
        ALL_EVENTS.add(event);  // 不断增长,从不清理
    }
}

2. 未关闭的资源 :

public byte[] readFile(String path) throws IOException {
    FileInputStream fis = new FileInputStream(path);
    // 错误:未使用try-with-resources,可能导致文件句柄泄漏
    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    int data;
    while ((data = fis.read()) != -1) {
        buffer.write(data);
    }
    // fis未关闭!
    return buffer.toByteArray();
}

3. 内部类引用 :

public class Outer {
    private final byte[] largeArray = new byte[10 * 1024 * 1024];
    
    public Runnable createTask() {
        // 非静态内部类持有外部类引用,可能导致largeArray无法释放
        return new Runnable() {
            @Override
            public void run() {
                System.out.println("Task running");
            }
        };
    }
}

4. 缓存使用不当 :

@Service
public class ProductService {
    // 不限大小的缓存,没有过期策略
    private final Map<String, Product> productCache = new HashMap<>();
    
    public Product getProduct(String id) {
        if (!productCache.containsKey(id)) {
            Product product = repository.findById(id);
            productCache.put(id, product);  // 持续增长
        }
        return productCache.get(id);
    }
}

5. 线程池配置不当 :

// 无界队列可能导致内存溢出
ExecutorService executor = new ThreadPoolExecutor(
    10, 10, 0L, TimeUnit.MILLISECONDS,
    new LinkedBlockingQueue<Runnable>()  // 无界队列
);
11. 压力测试暴露内存问题

通过压力测试可以更快地暴露内存泄漏问题。

实施步骤 :

  1. 使用JMeter或Gatling创建测试脚本,模拟真实业务场景

  2. 设置循环执行测试用例,持续观察内存使用趋势

  3. 监控GC活动和内存分配情况

  4. 增加负载直到发现异常内存增长

  5. 获取堆转储进行分析

压测注意事项 :

  • 逐步增加并发用户数,避免立即施加高负载

  • 测试周期应足够长,某些内存泄漏可能需要长时间积累才显现

  • 关注不同业务场景的内存使用差异

  • 每次测试前重启应用,确保基线一致

四、预防内存泄漏的最佳实践

1. 集合类使用注意事项
  • 优先使用有界集合,如ArrayBlockingQueue而非无界的LinkedBlockingQueue

  • 使用WeakHashMap存储可缓存但不必须的对象

  • 定期检查和清理长期存活的集合

2. 资源管理规范
  • 始终使用try-with-resources关闭IO资源

  • 实现AutoCloseable接口并在@PreDestroy方法中清理资源

  • 使用连接池监控功能,设置合理的最大连接数和超时时间

3. 缓存使用策略
  • 使用专业缓存框架如Caffeine或Ehcache,而非自定义Map

  • 设置适当的缓存大小上限和过期策略

  • 考虑使用弱引用或软引用缓存非关键数据

4. 开发阶段内存检测
  • 在开发和测试环境使用较小的堆内存,更快暴露问题

  • 编写单元测试验证资源释放

  • 使用FindBugs或SpotBugs等静态分析工具检测潜在问题

5. 生产环境监控策略
  • 配置内存使用告警

  • 定期采集和分析GC日志

  • 自动化生成周期性堆转储并分析

五、总结

内存泄漏问题是Java应用尤其是长期运行的SpringBoot应用面临的常见挑战。

在实际应用中,通常需要结合多种方法进行综合分析,才能准确找出问题根源。

同时,完善的监控体系也能帮助我们及早发现并解决潜在问题,确保应用的长期稳定运行。


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值