题目
分析并优化一个存在内存泄漏的Java程序
信息
- 类型:问答
- 难度:⭐⭐
考点
垃圾回收原理,内存泄漏诊断,GC日志分析,弱引用应用
快速回答
该程序存在内存泄漏问题,主要原因是静态Map缓存未清理过期条目。解决方案:
- 使用
WeakHashMap替代普通HashMap实现自动清理 - 或使用带过期时间的缓存框架(如Caffeine)
- 或定期清理缓存(示例代码):
// 每10分钟清理一次 ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() -> { cache.entrySet().removeIf(entry -> System.currentTimeMillis() - entry.getValue() > 600_000); }, 10, 10, TimeUnit.MINUTES);
问题程序代码
public class MemoryLeakExample {
private static Map<String, Long> cache = new HashMap<>();
public void processUserRequest(String sessionId) {
// 模拟业务处理
cache.put(sessionId, System.currentTimeMillis());
}
public static void main(String[] args) {
MemoryLeakExample app = new MemoryLeakExample();
// 模拟持续请求
while (true) {
app.processUserRequest(UUID.randomUUID().toString());
try { Thread.sleep(10); } catch (InterruptedException e) {}
}
}
}内存泄漏原理
静态Mapcache会持续增长,因为:
- 静态变量生命周期与ClassLoader相同
- 放入Map的sessionId作为强引用不会被GC回收
- 即使业务已完成会话处理,缓存条目仍保留
诊断方法
- GC日志分析:添加JVM参数
-Xlog:gc*:file=gc.log- 观察老年代使用率持续上升
- Full GC后内存无法回收到基线水平
- 堆转储分析:
jmap -dump:live,file=heapdump.hprof <pid>- 使用MAT/Eclipse Memory Analyzer分析
- 查找HashMap$Node的retained size异常大
解决方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| WeakHashMap | 自动回收无引用键值 | 不可预测的回收时机 |
| 定时清理 | 精确控制生命周期 | 需要维护清理线程 |
| Guava/Caffeine | 内置过期策略 | 引入第三方依赖 |
最佳实践
- 对象生命周期管理:
- 避免长生命周期对象持有短生命周期对象引用
- 对缓存使用软引用(SoftReference)实现内存敏感回收
- 监控配置:
- 生产环境启用GC日志:
-Xlog:gc*,safepoint:file=gc.log:time,uptime:filecount=5,filesize=10M - 使用JMX监控内存池(MemoryPoolMXBean)
- 生产环境启用GC日志:
常见错误
- 误认为
null赋值即释放内存(需断开所有引用) - 在缓存中使用不可变的键对象(如String导致等同永久代)
- 未关闭资源导致GC无法回收(如数据库连接需配合finally/try-with-resources)
扩展知识
- 引用类型对比:
- 强引用(Strong):宁可OOM也不回收
- 软引用(Soft):内存不足时回收
- 弱引用(Weak):下次GC即回收
- 虚引用(Phantom):用于对象回收跟踪
- GC算法选择:
- 低延迟场景:ZGC/Shenandoah(暂停时间<10ms)
- 高吞吐场景:G1(JDK9+默认)