一、内存泄漏的那些事儿

内存泄漏就像家里漏水的水龙头,虽然每次只漏一滴,但时间长了能把整个房子淹了。Java应用里,对象本该被回收却赖在堆里不走,最终可能导致OOM(OutOfMemoryError)。举个生活化的例子:你网购了一堆商品,拆完包装后纸箱却堆满客厅不扔——这就是典型的内存泄漏。

技术栈:Java + MAT(Memory Analyzer Tool)

// 示例1:静态集合引起的内存泄漏
public class LeakyShop {
    private static List<Order> orderList = new ArrayList<>(); // 静态集合会一直持有对象引用
    
    public void addOrder(Order order) {
        orderList.add(order);  // 订单对象永远无法被GC回收
    }
}

// 正确做法:使用WeakHashMap或定期清理
public class SafeShop {
    private static Map<Order, Boolean> weakMap = new WeakHashMap<>();
    
    public void addOrder(Order order) {
        weakMap.put(order, true);  // 当订单对象无其他引用时会被自动回收
    }
}

二、工具界的"侦探三件套"

1. JVM自带工具包

jmapjstack是JDK自带的瑞士军刀。比如用jmap -histo:live <pid>可以快速查看存活对象分布:

# 生成堆转储文件
jmap -dump:live,format=b,file=heap.hprof 1234

2. VisualVM的立体侦查

图形化工具VisualVM可以实时监控堆内存曲线。当看到老年代(Old Gen)使用率只增不减时,基本可以确认泄漏。

3. MAT的深度解剖

MAT能生成泄漏嫌疑报告。比如分析java.lang.ThreadthreadLocals字段,经常是线程池未清理的罪魁祸首。

技术栈演示:MAT分析示例

// 线程局部变量泄漏案例
public class ThreadLeak {
    private static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
    
    public void doWork() {
        threadLocal.set(new byte[1024 * 1024]); // 每个线程持有一个1MB数组
        // 忘记调用threadLocal.remove()
    }
}

通过MAT的Dominator Tree视图,可以快速定位到这些"巨型"byte数组的持有者。

三、实战中的破案技巧

场景1:Spring容器中的陷阱

@Controller
public class BookingController {
    private List<Booking> bookings = new ArrayList<>(); // 实例变量被单例持有
    
    @PostMapping("/book")
    public String createBooking(Booking booking) {
        bookings.add(booking);  // 每次请求都泄漏一个Booking对象
        return "success";
    }
}

解决方案:

  1. 改用请求作用域的Bean
  2. 或使用@Scope(value = WebApplicationContext.SCOPE_REQUEST)

场景2:缓存忘记设置TTL

// 使用Guava Cache的典型错误
Cache<String, User> cache = CacheBuilder.newBuilder()
    .maximumSize(1000) // 只设置最大数量
    .build(); // 缺少.expireAfterWrite()设置

最佳实践: 必须设置双过期策略(时间和大小)

四、防漏工程指南

  1. 编码规范:对静态集合、监听器、线程池等高风险点建立Code Review清单
  2. 监控体系:在Prometheus中配置JVM内存报警规则
  3. 压测验证:用JMeter模拟长时间运行后观察内存曲线
  4. 防御性编程:资源使用遵循"谁申请谁释放"原则
// 资源关闭模板示例
public void processFile() {
    InputStream is = null;
    try {
        is = new FileInputStream("data.txt");
        // 处理逻辑...
    } finally {
        if (is != null) {
            try { is.close(); } 
            catch (IOException e) { log.error("关闭流异常", e); }
        }
    }
}

五、技术选型红黑榜

工具/方法 优点 缺点
MAT 可视化强,支持OQL查询 大堆文件分析耗内存
Arthas 无需重启,动态诊断 学习曲线陡峭
JProfiler 实时监控精准 商业收费

终极忠告

内存泄漏排查就像破案,要:

  1. 保留案发现场(保存dump文件)
  2. 收集所有证据(日志+监控数据)
  3. 使用交叉验证(至少两种工具确认)

当遇到CMS GC频繁Full GC时,不妨用这个命令组合拳:

jps -lv | grep 应用名
jmap -dump:format=b,file=leak.hprof <pid>
jstack -l <pid> > thread.txt