内存泄漏排查案例

wen java案例 2

我将通过一个完整的案例来展示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

总结排查经验

排查步骤:

  1. 发现问题 → 监控告警/OOM
  2. 初步诊断 → jstat查看GC情况
  3. 获取分析数据 → 生成heap dump
  4. 分析定位 → MAT/jhat分析泄漏点
  5. 代码审查 → 检查静态集合、缓存、ThreadLocal等
  6. 修复验证 → 代码修复并持续监控

常见泄漏模式:

  • 静态集合类无限增长
  • 未关闭的资源(JDBC、IO流)
  • ThreadLocal未清理
  • 监听器未注销
  • 缓存无过期策略
  • 内部类持有外部类引用

这个案例展示了完整的内存泄漏排查流程,实际工作中需要结合具体场景灵活运用这些工具和方法。

抱歉,评论功能暂时关闭!