我将通过一个完整的案例来展示Java内存泄漏的排查过程,包括问题发现、分析、定位和解决。

内存泄漏排查实战案例
案例背景
一个电商系统的订单处理服务,运行一段时间后出现响应变慢,最终导致OOM(OutOfMemory)。
问题发现
# 1. 发现应用告警 系统告警:应用响应时间超过5秒 内存使用率持续升高 最终抛出 OutOfMemoryError # 2. 查看错误日志 Exception in thread "http-nio-8080-exec-47" java.lang.OutOfMemoryError: Java heap space
初步诊断
通过监控平台查看内存趋势
# 使用jstat查看GC情况 jstat -gcutil <pid> 1000 10 # 输出结果分析 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 92.50 98.30 95.20 90.10 1452 12.345 567 78.901 91.246 # 观察:老年代(O)使用率98.3%,Full GC频繁(567次)
获取堆dump文件
# 方法1:使用jmap生成dump文件 jmap -dump:live,format=b,file=/tmp/heap.bin <pid> # 方法2:通过JVM参数自动生成(推荐) # 在启动参数中加入: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
分析内存泄漏
使用MAT(Memory Analyzer Tool)分析
# 打开MAT工具,加载heap.bin文件 # 执行"Leak Suspects"报告分析
MAT分析结果:
Problem Suspect 1:
"org.apache.kafka.clients.producer.KafkaProducer"
loaded by "java.net.URLClassLoader" occupies 356,234,568 (73.4%) bytes
Key Stacktrace:
- java.util.HashMap.putMapEntries
- java.util.HashSet.add
- org.apache.kafka.common.metrics.stats.Sample
深入分析堆栈
// 发现疑似泄漏代码
@Component
public class KafkaSendService {
// 问题:静态Map保存所有发送记录
private static final Map<String, MessageInfo> MESSAGE_CACHE
= new HashMap<>(); // 无大小限制的缓存
public void sendMessage(String orderId, MessageInfo message) {
// 发送消息
kafkaTemplate.send("order-topic", message);
// 问题代码:每次发送都保存到缓存,永远不清理
MESSAGE_CACHE.put(orderId, message);
}
}
定位根因
使用jhat分析
jhat -port 7000 /tmp/heap.bin # 打开浏览器访问 http://localhost:7000 # 查看shallow heap和retained heap最大的对象
使用JVisualVM分析
// 找到泄漏的集合,查看其元素类型 HashMap<String, MessageInfo> MESSAGE_CACHE - Key: 订单号 String - Value: MessageInfo (包含大对象如byte[]) // 查看引用链 MessageInfo -> byte[] payload
代码级排查
// 完整的问题代码示例
@Service
public class OrderService {
// 泄漏源1:无限增长的静态Map
private static final Map<String, Order> ORDER_CACHE = new HashMap<>();
// 泄漏源2:ThreadLocal使用不当
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
new ThreadLocal<>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
// 泄漏源3:监听器未注册
private static final List<OrderListener> LISTENERS = new ArrayList<>();
public void processOrder(Order order) {
// 1. 缓存订单,但从未移除(内存泄漏)
ORDER_CACHE.put(order.getId(), order);
// 2. ThreadLocal使用后未remove
DATE_FORMAT.get().format(new Date());
// 应该在finally中调用DATE_FORMAT.remove()
// 3. 添加监听器,但从未移除
LISTENERS.add(new OrderListener() {
@Override
public void onOrderProcessed(Order o) {
// 处理逻辑
}
});
// 4. 大对象未及时置null
byte[] bigData = new byte[10 * 1024 * 1024];
// 处理...
// bigData应置null
}
}
解决方案
修复代码
@Service
public class OrderService {
// 修复1:使用Guava Cache并设置过期策略
private final Cache<String, Order> ORDER_CACHE =
CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();
// 修复2:使用安全的DateFormat
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public void processOrder(Order order) {
// 修复1:使用cache
ORDER_CACHE.put(order.getId(), order);
// 修复2:正确使用ThreadLocal
try {
DATE_FORMAT.get().format(new Date());
} finally {
DATE_FORMAT.remove(); // 关键:使用后必须remove
}
// 修复3:使用WeakReference
List<OrderListener> listeners = new CopyOnWriteArrayList<>();
// 或者定期清理不需要的监听器
}
}
优化JVM参数
# 调整堆内存设置 JAVA_OPTS="-Xms2g -Xmx2g -XX:+UseG1GC \ -XX:MaxGCPauseMillis=100 \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/logs/heapdump.hprof \ -XX:+PrintGCDetails -XX:+PrintGCDateStamps \ -Xloggc:/logs/gc.log"
预防措施
增加监控和告警
@Component
public class MemoryMonitor {
@Scheduled(fixedDelay = 60000)
public void monitorMemory() {
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
double usedMemory = (double) heapUsage.getUsed() / heapUsage.getMax();
if (usedMemory > 0.8) {
// 发送告警
log.error("Memory usage high: {}%", usedMemory * 100);
// 自动触发dump
dumpHeap();
}
}
private void dumpHeap() {
try {
String dumpPath = "/logs/memory-dump-" +
System.currentTimeMillis() + ".hprof";
ManagementFactory.getPlatformMBeanServer()
.invoke(new ObjectName("com.sun.management:type=HotSpotDiagnostic"),
"dumpHeap",
new Object[]{dumpPath, true},
new String[]{"java.lang.String", "boolean"});
} catch (Exception e) {
log.error("Dump heap failed", e);
}
}
}
内存泄漏检测工具配置
// 使用LeakCanary(Android)或similar工具 // 配置定期清理和检查 // 使用JFR(Java Flight Recorder)监控 -XX:+UnlockCommercialFeatures -XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=recording.jfr
验证解决方案
# 1. 修复后运行24小时,检查内存趋势 jstat -gcutil <pid> 5000 # 观察输出 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 45.20 52.30 78.20 70.10 2452 20.345 12 5.901 26.246 # 老年代使用率稳定在50%左右,Full GC次数显著下降 # 2. 使用JDK自带的工具验证 jcmd <pid> GC.class_histogram | head -20
总结排查经验
排查步骤:
- 发现问题 → 监控告警/OOM
- 初步诊断 → jstat查看GC情况
- 获取分析数据 → 生成heap dump
- 分析定位 → MAT/jhat分析泄漏点
- 代码审查 → 检查静态集合、缓存、ThreadLocal等
- 修复验证 → 代码修复并持续监控
常见泄漏模式:
- 静态集合类无限增长
- 未关闭的资源(JDBC、IO流)
- ThreadLocal未清理
- 监听器未注销
- 缓存无过期策略
- 内部类持有外部类引用
这个案例展示了完整的内存泄漏排查流程,实际工作中需要结合具体场景灵活运用这些工具和方法。