java 内存泄漏是指程序中已分配的堆内存由于某种原因程序不再使用且无法被 gc 回收,导致占用内存越来越大最终引发 oom。排查时通常通过 jmap 导出堆快照,再用 mat 或 jvisualvm 分析 dominator tree 找出持有大量对象的 root 引用链。

如何定位导致内存泄漏的静态集合类
在 Java 中,静态变量(static)的生命周期与 JVM 进程一致,若静态集合(如 HashMap、List)只增不减,就会形成典型的内存泄漏。排查此类问题时,重点检查全局缓存或监听器注册是否缺少移除逻辑。

import java.util.HashMap;
import java.util.Map;
public class LeakyCache {
// 静态 Map 生命周期随类,若不清理则永久驻留
private static final Map<String, Object> cache = new HashMap<>();
public void addToCache(String key, Object value) {
// 错误做法:只添加不删除,导致内存无限增长
cache.put(key, value);
}
public static void main(String[] args) {
LeakyCache instance = new LeakyCache();
for (int i = 0; i < 1000000; i++) {
instance.addToCache("key-" + i, new byte[1024 * 1024]);
}
// 模拟触发 GC 后查看内存占用
System.gc();
try { Thread.sleep(5000); } catch (InterruptedException e) {}
}
}
运行上述代码并导出堆转储后,在 MAT 中可以看到 LeakyCache.cache 对象占据了绝大部分堆空间。其引用路径为 LeakyCache.class -> static field cache -> HashMap -> Entry[] -> Node。每个 Node 都持有一个大数组对象,这些对象因为被静态字段强引用而无法被垃圾回收。
立即学习“Java免费学习笔记(深入)”;
未关闭的资源连接为何会导致泄漏
数据库连接、IO 流或网络连接等资源如果在使用后没有正确调用 close() 方法,虽然对象本身可能被回收,但底层操作系统级的资源句柄(File Descriptor)会一直被占用。在 Java 层面,这通常表现为连接池耗尽或 OOME。排查时需关注 try-with-resources 的使用情况。
你是 TypeScript 项目架构专家,专注于脚手架搭建生产就绪的 Node.js 和前端应用。生成包含现代工具链(pnpm、Vite、Next.js)、类型

import java.io.FileInputStream;
import java.io.IOException;
public class ResourceLeakExample {
public void processFile(String filePath) throws IOException {
// 错误做法:未在 finally 或 try-with-resources 中关闭流
FileInputStream fis = new FileInputStream(filePath);
int data = fis.read();
while (data != -1) {
data = fis.read();
}
// 忘记调用 fis.close(),导致 FD 泄漏
}
public static void main(String[] args) {
ResourceLeakExample example = new ResourceLeakExample();
try {
for (int i = 0; i < 10000; i++) {
example.processFile("/dev/null");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行该程序后,可以通过 lsof -p <pid></pid> 命令观察到进程打开的文件描述符数量持续增加。在堆内存分析中,虽然 FileInputStream 对象可能因无强引用而被回收,但其底层的 SunNIO 内部结构可能仍持有 Native 资源。正确的做法是使用 try (FileInputStream fis = new FileInputStream(...)) 语法确保自动关闭。
内部类隐式引用外部类如何解决
非静态内部类(Non-static Inner Class)默认持有外部类的隐式引用。如果内部类实例的生命周期长于外部类实例(例如作为线程或异步回调),外部类对象将无法被回收。解决方式是将内部类改为静态内部类,或使用弱引用(WeakReference)。
在之前的 LeakyCache 示例基础上,假设我们有一个异步任务类:
import java.lang.ref.WeakReference;
public class OuterClass {
private String sensitiveData = "Secret";
// 错误做法:非静态内部类持有 OuterClass 引用
private class AsyncTask extends Thread {
public void run() {
// 即使 OuterClass 实例被置为 null,AsyncTask 仍在运行
// 且持有 OuterClass 的强引用,导致 sensitiveData 无法回收
System.out.println(sensitiveData);
}
}
// 正确做法:使用静态内部类 + WeakReference
private static class SafeAsyncTask extends Thread {
private final WeakReference<OuterClass> outerRef;
public SafeAsyncTask(OuterClass outer) {
this.outerRef = new WeakReference<>(outer);
}
@Override
public void run() {
OuterClass outer = outerRef.get();
if (outer != null) {
System.out.println(outer.sensitiveData);
}
}
}
public void startLeak() {
new AsyncTask().start(); // 泄漏
}
public void startSafe() {
new SafeAsyncTask(this).start(); // 安全
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.startSafe();
outer = null; // 此时 SafeAsyncTask 中的 weak reference 允许 GC 回收 OuterClass
System.gc();
}
}
当使用 SafeAsyncTask 时,即使外部线程仍在运行,一旦 OuterClass 没有其他强引用,GC 即可回收其内存。MAT 分析时会发现,使用弱引用的情况下,Dominator Tree 中不会存在从线程栈到 OuterClass 的强引用路径。

















