JVM 内存泄漏排查从现象定位到根因分析线上问题的系统化诊断方法一、内存泄漏的隐蔽性当 OOM 只是冰山一角JVM 内存泄漏不像 CPU 飙高那样容易被监控发现。内存缓慢增长的过程可能持续数天甚至数周直到触发 OOM 才引起注意。而 OOM 发生时应用已经处于不可用状态留给排查的时间窗口极短。更棘手的是内存泄漏的表象与根因往往相距甚远。一个 HashMap 的持续增长可能是因为某个定时任务不断往里添加数据但从未清理一个 ThreadLocal 的泄漏可能是因为线程池中的线程被复用但 ThreadLocal 未被移除一个 ClassLoader 的泄漏可能是因为某个静态引用持有了一个已卸载模块的类。仅凭 OOM 日志中的堆栈信息很难直接定位到泄漏的根因。系统化的排查方法比直觉猜测更可靠。从监控指标确认泄漏存在到堆转储分析定位泄漏对象再到代码审查找到泄漏源头每一步都有明确的工具和方法。二、JVM 内存模型与泄漏类型JVM 的内存分为堆、方法区、栈、本地方法栈和直接内存。不同区域的泄漏有不同的表现和排查方法。flowchart TD A[JVM 内存区域] -- B[堆 Heap] A -- C[方法区 Metaspace] A -- D[直接内存 Direct Memory] A -- E[线程栈 Thread Stack] B -- B1[年轻代: 短生命周期对象] B -- B2[老年代: 长生命周期对象/泄漏] B2 -- F[堆泄漏类型] F -- F1[集合泄漏: Map/List 持续增长] F -- F2[缓存泄漏: 无淘汰策略的缓存] F -- F3[监听器泄漏: 未注销的事件监听] F -- F4[ThreadLocal 泄漏: 线程复用未清理] C -- G[Metaspace 泄漏类型] G -- G1[动态代理: CGLIB 生成大量类] G -- G2[ClassLoader 泄漏: 模块卸载不彻底] D -- H[直接内存泄漏类型] H -- H1[NIO ByteBuffer: 未释放] H -- H2[Netty PoolArena: 内存池泄漏] style F fill:#ffcdd2 style G fill:#fff3e0 style H fill:#fff3e02.1 内存泄漏监控指标// MemoryLeakDetector.java — 内存泄漏检测器 // 设计意图基于 JVM 运行时指标判断是否存在内存泄漏 // 通过 GC 后堆内存的持续增长趋势来识别泄漏 import java.lang.management.*; import java.util.*; import java.util.concurrent.*; public class MemoryLeakDetector { private final MemoryMXBean memoryMXBean; private final ListGarbageCollectorMXBean gcBeans; private final ScheduledExecutorService scheduler; // 记录每次 GC 后的堆内存使用量 private final ConcurrentLinkedQueueGCSnapshot snapshots new ConcurrentLinkedQueue(); private final int maxSnapshots 60; // 保留最近 60 次 GC 快照 // 上一次 GC 信息用于检测 GC 事件 private long lastGcCount 0; public MemoryLeakDetector() { this.memoryMXBean ManagementFactory.getMemoryMXBean(); this.gcBeans ManagementFactory.getGarbageCollectorMXBeanList(); this.scheduler Executors.newSingleThreadScheduledExecutor(r - { Thread t new Thread(r, memory-leak-detector); t.setDaemon(true); return t; }); } public void start(int checkIntervalSeconds) { scheduler.scheduleAtFixedRate( this::checkAndRecord, 0, checkIntervalSeconds, TimeUnit.SECONDS ); } public void stop() { scheduler.shutdown(); } private void checkAndRecord() { long currentGcCount getTotalGcCount(); // 检测到 GC 事件记录 GC 后的堆内存 if (currentGcCount lastGcCount) { lastGcCount currentGcCount; MemoryUsage heapUsage memoryMXBean.getHeapMemoryUsage(); GCSnapshot snapshot new GCSnapshot( System.currentTimeMillis(), heapUsage.getUsed(), heapUsage.getMax(), currentGcCount ); snapshots.add(snapshot); while (snapshots.size() maxSnapshots) { snapshots.poll(); } } } // 判断是否存在内存泄漏趋势 public LeakAnalysisResult analyzeLeakTrend() { if (snapshots.size() 10) { return new LeakAnalysisResult( LeakStatus.INSUFFICIENT_DATA, 数据不足需要至少 10 次 GC 快照, 0, 0 ); } GCSnapshot[] data snapshots.toArray(new GCSnapshot[0]); int mid data.length / 2; // 计算前半段和后半段的平均堆内存使用量 long firstHalfSum 0, secondHalfSum 0; for (int i 0; i mid; i) firstHalfSum data[i].usedAfterGC; for (int i mid; i data.length; i) secondHalfSum data[i].usedAfterGC; long firstHalfAvg firstHalfSum / mid; long secondHalfAvg secondHalfSum / (data.length - mid); // 计算增长率 double growthRate firstHalfAvg 0 ? (double) (secondHalfAvg - firstHalfAvg) / firstHalfAvg : 0; // 增长率超过 20% 视为疑似泄漏 if (growthRate 0.2) { return new LeakAnalysisResult( LeakStatus.SUSPECTED_LEAK, String.format(GC 后堆内存持续增长增长率 %.1f%%, growthRate * 100), firstHalfAvg, secondHalfAvg ); } return new LeakAnalysisResult( LeakStatus.NORMAL, String.format(堆内存趋势稳定增长率 %.1f%%, growthRate * 100), firstHalfAvg, secondHalfAvg ); } private long getTotalGcCount() { return gcBeans.stream() .mapToLong(GarbageCollectorMXBean::getCollectionCount) .sum(); } // 内部数据类 private record GCSnapshot( long timestamp, long usedAfterGC, long maxHeap, long gcCount ) {} public enum LeakStatus { NORMAL, INSUFFICIENT_DATA, SUSPECTED_LEAK } public record LeakAnalysisResult( LeakStatus status, String message, long firstHalfAvg, long secondHalfAvg ) {} }三、堆转储分析与泄漏定位3.1 自动化堆转储与分析// HeapDumpAnalyzer.java — 堆转储自动化分析 // 设计意图在检测到内存泄漏时自动触发堆转储 // 并分析转储文件中的大对象和支配者对象 import java.io.*; import java.nio.file.*; import java.util.*; public class HeapDumpAnalyzer { private final String dumpDir; private final long maxDumpSizeBytes; public HeapDumpAnalyzer(String dumpDir, long maxDumpSizeBytes) { this.dumpDir dumpDir; this.maxDumpSizeBytes maxDumpSizeBytes; new File(dumpDir).mkdirs(); } // 触发堆转储 public String triggerHeapDump() throws IOException { String fileName String.format(heap_%d.hprof, System.currentTimeMillis()); String filePath Paths.get(dumpDir, fileName).toString(); // 使用 jmap 命令生成堆转储 String pid getCurrentPid(); ProcessBuilder pb new ProcessBuilder( jmap, -dump:formatb,file filePath, pid ); pb.redirectErrorStream(true); Process process pb.start(); try { boolean completed process.waitFor(120, TimeUnit.SECONDS); if (!completed) { process.destroyForcibly(); throw new IOException(堆转储超时); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(堆转储被中断, e); } // 检查转储文件大小 File dumpFile new File(filePath); if (!dumpFile.exists() || dumpFile.length() 0) { throw new IOException(堆转储文件为空或不存在); } // 清理旧的转储文件避免磁盘占满 cleanOldDumps(); return filePath; } // 分析堆转储中的泄漏嫌疑对象 public ListLeakSuspect analyzeDump(String dumpPath) { // 实际实现使用 MAT (Memory Analyzer Tool) 或 JHat 的 API // 这里展示分析逻辑框架 ListLeakSuspect suspects new ArrayList(); // 步骤 1找到占用内存最大的对象 // 步骤 2分析对象的支配者树Dominator Tree // 步骤 3识别 GC Root 到泄漏对象的最短路径 return suspects; } private void cleanOldDumps() { File dir new File(dumpDir); File[] dumps dir.listFiles((d, name) - name.endsWith(.hprof)); if (dumps null || dumps.length 3) return; // 按修改时间排序保留最新的 3 个 Arrays.sort(dumps, Comparator.comparingLong(File::lastModified)); for (int i 0; i dumps.length - 3; i) { dumps[i].delete(); } } private String getCurrentPid() { String runtimeName ManagementFactory.getRuntimeMXBean().getName(); return runtimeName.split()[0]; } public static class LeakSuspect { private String className; private long retainedSize; private int objectCount; private String gcRootPath; // getter 省略 } }3.2 常见泄漏模式的代码审查清单// LeakPatternChecker.java — 常见泄漏模式的静态检查 // 设计意图在代码审查阶段识别常见的内存泄漏模式 // 避免泄漏代码进入生产环境 import java.util.*; public class LeakPatternChecker { // 模式 1静态集合持续增长 // 错误示例static MapString, Object cache new HashMap(); // 每次请求往 cache 中添加数据但从不清理 public static class StaticCollectionLeak { private static final MapString, byte[] CACHE new HashMap(); public void processData(String key, byte[] data) { // 危险静态 Map 无限增长永远不会被 GC CACHE.put(key, data); // 修复使用有界缓存 // 应替换为 Caffeine 或 Guava Cache设置最大容量和过期策略 } } // 模式 2ThreadLocal 未清理 // 错误示例线程池中的线程复用ThreadLocal 值残留 public static class ThreadLocalLeak { private static final ThreadLocalUserContext USER_CONTEXT new ThreadLocal(); private static final ExecutorService POOL Executors.newFixedThreadPool(10); public void handleRequest(Request request) { try { USER_CONTEXT.set(new UserContext(request.getUserId())); // 业务处理... doBusinessLogic(); } finally { // 必须在 finally 中清理 ThreadLocal // 遗漏这行会导致线程复用时数据泄漏 USER_CONTEXT.remove(); } } private void doBusinessLogic() {} } // 模式 3未关闭的资源 public static class ResourceLeak { public String readFile(String path) throws IOException { // 危险异常时流未关闭 // FileInputStream fis new FileInputStream(path); // byte[] data fis.readAllBytes(); // fis.close(); // 如果上一行抛异常这行不会执行 // 修复使用 try-with-resources try (FileInputStream fis new FileInputStream(path)) { return new String(fis.readAllBytes()); } } } // 模式 4监听器未注销 public static class ListenerLeak { private final ListEventListener listeners new ArrayList(); public void registerListener(EventListener listener) { listeners.add(listener); // 危险没有对应的 unregister 方法 // 当 listener 所属对象需要被 GC 时listeners 仍持有引用 } // 修复提供注销方法或使用 WeakHashMap public void unregisterListener(EventListener listener) { listeners.remove(listener); } } }四、边界分析与架构权衡堆转储对服务的影响生成堆转储时 JVM 会暂停应用Stop-The-World暂停时间与堆大小成正比。一个 8GB 堆的转储可能暂停 10-30 秒。在生产环境中这会导致所有请求超时。建议在检测到泄漏趋势时先将流量切换到备用实例再在原实例上执行堆转储。GC 后堆内存增长的误判GC 后堆内存增长不一定意味着泄漏。应用流量增长、缓存预热、批量任务执行都可能导致堆内存合理增长。需要结合业务指标QPS、活跃用户数综合判断避免误报。MAT 分析的学习成本Eclipse MAT 是最强大的堆转储分析工具但学习曲线陡峭。支配者树、泄漏嫌疑报告、GC Root 路径等概念需要深入理解 JVM 内存模型才能正确解读。团队中至少需要 1-2 名成员掌握 MAT 分析能力。直接内存泄漏的排查难度NIO 的 DirectByteBuffer 分配在堆外内存无法通过常规的堆转储分析发现。需要使用-XX:MaxDirectMemorySize限制直接内存大小配合 JMX 监控java.nio:typeBufferPool,namedirect的MemoryUsed属性来检测泄漏。五、总结JVM 内存泄漏的排查需要系统化的方法先通过 GC 后堆内存趋势判断泄漏是否存在再通过堆转储分析定位泄漏对象最后通过代码审查找到泄漏根因。核心工具包括 MemoryMXBean 监控、jmap 堆转储和 MAT 分析。落地建议在应用中集成内存泄漏检测器基于 GC 后堆内存增长趋势自动告警生产环境配置-XX:HeapDumpOnOutOfMemoryErrorOOM 时自动生成堆转储ThreadLocal 必须在 finally 中 remove静态集合必须使用有界缓存直接内存泄漏需要单独监控不能依赖堆转储分析。
JVM 内存泄漏排查:从现象定位到根因分析,线上问题的系统化诊断方法
JVM 内存泄漏排查从现象定位到根因分析线上问题的系统化诊断方法一、内存泄漏的隐蔽性当 OOM 只是冰山一角JVM 内存泄漏不像 CPU 飙高那样容易被监控发现。内存缓慢增长的过程可能持续数天甚至数周直到触发 OOM 才引起注意。而 OOM 发生时应用已经处于不可用状态留给排查的时间窗口极短。更棘手的是内存泄漏的表象与根因往往相距甚远。一个 HashMap 的持续增长可能是因为某个定时任务不断往里添加数据但从未清理一个 ThreadLocal 的泄漏可能是因为线程池中的线程被复用但 ThreadLocal 未被移除一个 ClassLoader 的泄漏可能是因为某个静态引用持有了一个已卸载模块的类。仅凭 OOM 日志中的堆栈信息很难直接定位到泄漏的根因。系统化的排查方法比直觉猜测更可靠。从监控指标确认泄漏存在到堆转储分析定位泄漏对象再到代码审查找到泄漏源头每一步都有明确的工具和方法。二、JVM 内存模型与泄漏类型JVM 的内存分为堆、方法区、栈、本地方法栈和直接内存。不同区域的泄漏有不同的表现和排查方法。flowchart TD A[JVM 内存区域] -- B[堆 Heap] A -- C[方法区 Metaspace] A -- D[直接内存 Direct Memory] A -- E[线程栈 Thread Stack] B -- B1[年轻代: 短生命周期对象] B -- B2[老年代: 长生命周期对象/泄漏] B2 -- F[堆泄漏类型] F -- F1[集合泄漏: Map/List 持续增长] F -- F2[缓存泄漏: 无淘汰策略的缓存] F -- F3[监听器泄漏: 未注销的事件监听] F -- F4[ThreadLocal 泄漏: 线程复用未清理] C -- G[Metaspace 泄漏类型] G -- G1[动态代理: CGLIB 生成大量类] G -- G2[ClassLoader 泄漏: 模块卸载不彻底] D -- H[直接内存泄漏类型] H -- H1[NIO ByteBuffer: 未释放] H -- H2[Netty PoolArena: 内存池泄漏] style F fill:#ffcdd2 style G fill:#fff3e0 style H fill:#fff3e02.1 内存泄漏监控指标// MemoryLeakDetector.java — 内存泄漏检测器 // 设计意图基于 JVM 运行时指标判断是否存在内存泄漏 // 通过 GC 后堆内存的持续增长趋势来识别泄漏 import java.lang.management.*; import java.util.*; import java.util.concurrent.*; public class MemoryLeakDetector { private final MemoryMXBean memoryMXBean; private final ListGarbageCollectorMXBean gcBeans; private final ScheduledExecutorService scheduler; // 记录每次 GC 后的堆内存使用量 private final ConcurrentLinkedQueueGCSnapshot snapshots new ConcurrentLinkedQueue(); private final int maxSnapshots 60; // 保留最近 60 次 GC 快照 // 上一次 GC 信息用于检测 GC 事件 private long lastGcCount 0; public MemoryLeakDetector() { this.memoryMXBean ManagementFactory.getMemoryMXBean(); this.gcBeans ManagementFactory.getGarbageCollectorMXBeanList(); this.scheduler Executors.newSingleThreadScheduledExecutor(r - { Thread t new Thread(r, memory-leak-detector); t.setDaemon(true); return t; }); } public void start(int checkIntervalSeconds) { scheduler.scheduleAtFixedRate( this::checkAndRecord, 0, checkIntervalSeconds, TimeUnit.SECONDS ); } public void stop() { scheduler.shutdown(); } private void checkAndRecord() { long currentGcCount getTotalGcCount(); // 检测到 GC 事件记录 GC 后的堆内存 if (currentGcCount lastGcCount) { lastGcCount currentGcCount; MemoryUsage heapUsage memoryMXBean.getHeapMemoryUsage(); GCSnapshot snapshot new GCSnapshot( System.currentTimeMillis(), heapUsage.getUsed(), heapUsage.getMax(), currentGcCount ); snapshots.add(snapshot); while (snapshots.size() maxSnapshots) { snapshots.poll(); } } } // 判断是否存在内存泄漏趋势 public LeakAnalysisResult analyzeLeakTrend() { if (snapshots.size() 10) { return new LeakAnalysisResult( LeakStatus.INSUFFICIENT_DATA, 数据不足需要至少 10 次 GC 快照, 0, 0 ); } GCSnapshot[] data snapshots.toArray(new GCSnapshot[0]); int mid data.length / 2; // 计算前半段和后半段的平均堆内存使用量 long firstHalfSum 0, secondHalfSum 0; for (int i 0; i mid; i) firstHalfSum data[i].usedAfterGC; for (int i mid; i data.length; i) secondHalfSum data[i].usedAfterGC; long firstHalfAvg firstHalfSum / mid; long secondHalfAvg secondHalfSum / (data.length - mid); // 计算增长率 double growthRate firstHalfAvg 0 ? (double) (secondHalfAvg - firstHalfAvg) / firstHalfAvg : 0; // 增长率超过 20% 视为疑似泄漏 if (growthRate 0.2) { return new LeakAnalysisResult( LeakStatus.SUSPECTED_LEAK, String.format(GC 后堆内存持续增长增长率 %.1f%%, growthRate * 100), firstHalfAvg, secondHalfAvg ); } return new LeakAnalysisResult( LeakStatus.NORMAL, String.format(堆内存趋势稳定增长率 %.1f%%, growthRate * 100), firstHalfAvg, secondHalfAvg ); } private long getTotalGcCount() { return gcBeans.stream() .mapToLong(GarbageCollectorMXBean::getCollectionCount) .sum(); } // 内部数据类 private record GCSnapshot( long timestamp, long usedAfterGC, long maxHeap, long gcCount ) {} public enum LeakStatus { NORMAL, INSUFFICIENT_DATA, SUSPECTED_LEAK } public record LeakAnalysisResult( LeakStatus status, String message, long firstHalfAvg, long secondHalfAvg ) {} }三、堆转储分析与泄漏定位3.1 自动化堆转储与分析// HeapDumpAnalyzer.java — 堆转储自动化分析 // 设计意图在检测到内存泄漏时自动触发堆转储 // 并分析转储文件中的大对象和支配者对象 import java.io.*; import java.nio.file.*; import java.util.*; public class HeapDumpAnalyzer { private final String dumpDir; private final long maxDumpSizeBytes; public HeapDumpAnalyzer(String dumpDir, long maxDumpSizeBytes) { this.dumpDir dumpDir; this.maxDumpSizeBytes maxDumpSizeBytes; new File(dumpDir).mkdirs(); } // 触发堆转储 public String triggerHeapDump() throws IOException { String fileName String.format(heap_%d.hprof, System.currentTimeMillis()); String filePath Paths.get(dumpDir, fileName).toString(); // 使用 jmap 命令生成堆转储 String pid getCurrentPid(); ProcessBuilder pb new ProcessBuilder( jmap, -dump:formatb,file filePath, pid ); pb.redirectErrorStream(true); Process process pb.start(); try { boolean completed process.waitFor(120, TimeUnit.SECONDS); if (!completed) { process.destroyForcibly(); throw new IOException(堆转储超时); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IOException(堆转储被中断, e); } // 检查转储文件大小 File dumpFile new File(filePath); if (!dumpFile.exists() || dumpFile.length() 0) { throw new IOException(堆转储文件为空或不存在); } // 清理旧的转储文件避免磁盘占满 cleanOldDumps(); return filePath; } // 分析堆转储中的泄漏嫌疑对象 public ListLeakSuspect analyzeDump(String dumpPath) { // 实际实现使用 MAT (Memory Analyzer Tool) 或 JHat 的 API // 这里展示分析逻辑框架 ListLeakSuspect suspects new ArrayList(); // 步骤 1找到占用内存最大的对象 // 步骤 2分析对象的支配者树Dominator Tree // 步骤 3识别 GC Root 到泄漏对象的最短路径 return suspects; } private void cleanOldDumps() { File dir new File(dumpDir); File[] dumps dir.listFiles((d, name) - name.endsWith(.hprof)); if (dumps null || dumps.length 3) return; // 按修改时间排序保留最新的 3 个 Arrays.sort(dumps, Comparator.comparingLong(File::lastModified)); for (int i 0; i dumps.length - 3; i) { dumps[i].delete(); } } private String getCurrentPid() { String runtimeName ManagementFactory.getRuntimeMXBean().getName(); return runtimeName.split()[0]; } public static class LeakSuspect { private String className; private long retainedSize; private int objectCount; private String gcRootPath; // getter 省略 } }3.2 常见泄漏模式的代码审查清单// LeakPatternChecker.java — 常见泄漏模式的静态检查 // 设计意图在代码审查阶段识别常见的内存泄漏模式 // 避免泄漏代码进入生产环境 import java.util.*; public class LeakPatternChecker { // 模式 1静态集合持续增长 // 错误示例static MapString, Object cache new HashMap(); // 每次请求往 cache 中添加数据但从不清理 public static class StaticCollectionLeak { private static final MapString, byte[] CACHE new HashMap(); public void processData(String key, byte[] data) { // 危险静态 Map 无限增长永远不会被 GC CACHE.put(key, data); // 修复使用有界缓存 // 应替换为 Caffeine 或 Guava Cache设置最大容量和过期策略 } } // 模式 2ThreadLocal 未清理 // 错误示例线程池中的线程复用ThreadLocal 值残留 public static class ThreadLocalLeak { private static final ThreadLocalUserContext USER_CONTEXT new ThreadLocal(); private static final ExecutorService POOL Executors.newFixedThreadPool(10); public void handleRequest(Request request) { try { USER_CONTEXT.set(new UserContext(request.getUserId())); // 业务处理... doBusinessLogic(); } finally { // 必须在 finally 中清理 ThreadLocal // 遗漏这行会导致线程复用时数据泄漏 USER_CONTEXT.remove(); } } private void doBusinessLogic() {} } // 模式 3未关闭的资源 public static class ResourceLeak { public String readFile(String path) throws IOException { // 危险异常时流未关闭 // FileInputStream fis new FileInputStream(path); // byte[] data fis.readAllBytes(); // fis.close(); // 如果上一行抛异常这行不会执行 // 修复使用 try-with-resources try (FileInputStream fis new FileInputStream(path)) { return new String(fis.readAllBytes()); } } } // 模式 4监听器未注销 public static class ListenerLeak { private final ListEventListener listeners new ArrayList(); public void registerListener(EventListener listener) { listeners.add(listener); // 危险没有对应的 unregister 方法 // 当 listener 所属对象需要被 GC 时listeners 仍持有引用 } // 修复提供注销方法或使用 WeakHashMap public void unregisterListener(EventListener listener) { listeners.remove(listener); } } }四、边界分析与架构权衡堆转储对服务的影响生成堆转储时 JVM 会暂停应用Stop-The-World暂停时间与堆大小成正比。一个 8GB 堆的转储可能暂停 10-30 秒。在生产环境中这会导致所有请求超时。建议在检测到泄漏趋势时先将流量切换到备用实例再在原实例上执行堆转储。GC 后堆内存增长的误判GC 后堆内存增长不一定意味着泄漏。应用流量增长、缓存预热、批量任务执行都可能导致堆内存合理增长。需要结合业务指标QPS、活跃用户数综合判断避免误报。MAT 分析的学习成本Eclipse MAT 是最强大的堆转储分析工具但学习曲线陡峭。支配者树、泄漏嫌疑报告、GC Root 路径等概念需要深入理解 JVM 内存模型才能正确解读。团队中至少需要 1-2 名成员掌握 MAT 分析能力。直接内存泄漏的排查难度NIO 的 DirectByteBuffer 分配在堆外内存无法通过常规的堆转储分析发现。需要使用-XX:MaxDirectMemorySize限制直接内存大小配合 JMX 监控java.nio:typeBufferPool,namedirect的MemoryUsed属性来检测泄漏。五、总结JVM 内存泄漏的排查需要系统化的方法先通过 GC 后堆内存趋势判断泄漏是否存在再通过堆转储分析定位泄漏对象最后通过代码审查找到泄漏根因。核心工具包括 MemoryMXBean 监控、jmap 堆转储和 MAT 分析。落地建议在应用中集成内存泄漏检测器基于 GC 后堆内存增长趋势自动告警生产环境配置-XX:HeapDumpOnOutOfMemoryErrorOOM 时自动生成堆转储ThreadLocal 必须在 finally 中 remove静态集合必须使用有界缓存直接内存泄漏需要单独监控不能依赖堆转储分析。