⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 风象南 「风象南」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

一、引言

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

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

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

二、内存泄漏基础知识

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

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

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

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

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

三、内存泄漏排查方法

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应用面临的常见挑战。

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

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

文章目录
  1. 1. 一、引言
  2. 2. 二、内存泄漏基础知识
  3. 3. 三、内存泄漏排查方法
    1. 3.0.1. 1. JVM启动参数配置与GC日志分析
    2. 3.0.2. 2. 使用JConsole实时监控
    3. 3.0.3. 3. VisualVM进行高级内存分析
    4. 3.0.4. 4. MAT(Memory Analyzer Tool)详细堆分析
    5. 3.0.5. 5. 使用Spring Boot Actuator监控
    6. 3.0.6. 6. 使用jstack分析线程堆栈
    7. 3.0.7. 7. 使用YourKit等商业工具进行全面分析
    8. 3.0.8. 8. 数据库连接与资源泄漏检测
    9. 3.0.9. 9. 使用BTrace进行运行时分析
    10. 3.0.10. 10. 代码审查常见内存泄漏模式
    11. 3.0.11. 11. 压力测试暴露内存问题
  • 4. 四、预防内存泄漏的最佳实践
    1. 4.0.1. 1. 集合类使用注意事项
    2. 4.0.2. 2. 资源管理规范
    3. 4.0.3. 3. 缓存使用策略
    4. 4.0.4. 4. 开发阶段内存检测
    5. 4.0.5. 5. 生产环境监控策略
  • 5. 五、总结