知识模块
☕ Java 知识模块
四、JVM 深入理解
OOM 与内存泄漏

OOM 与内存泄漏

OutOfMemoryError(OOM)是 Java 应用常见的问题。理解 OOM 的类型和排查方法,是 Java 开发者的必备技能。

一、OOM 类型总览

类型区域错误信息
堆溢出Heapjava.lang.OutOfMemoryError: Java heap space
栈溢出Stackjava.lang.StackOverflowError
栈扩展失败Stackjava.lang.OutOfMemoryError: unable to create new native thread
方法区溢出Metaspacejava.lang.OutOfMemoryError: Metaspace
直接内存溢出Direct Memoryjava.lang.OutOfMemoryError: Direct buffer memory

二、堆溢出 (Java heap space)

场景

  1. 对象太多:创建大量对象,GC 来不及回收
  2. 对象太大:单个超大对象
  3. 内存泄漏:对象无法被回收

示例代码

// 堆溢出示例
public class HeapOOM {
    static class OOMObject {}
    
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true) {
            list.add(new OOMObject());  // 无限创建对象
        }
    }
}
 
// 运行参数:-Xms20m -Xmx20m
// 结果:java.lang.OutOfMemoryError: Java heap space

排查步骤

# 1. 添加 JVM 参数,OOM 时生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof
 
# 2. 分析堆转储
jhat dump.hprof              # 启动 HTTP 服务器分析
# 或使用 MAT、VisualVM 等工具
 
# 3. 查看内存使用情况
jmap -heap <pid>             # 查看堆配置和使用情况
jmap -histo:live <pid>       # 查看存活对象统计

解决方案

原因解决方案
内存不足增大堆大小 -Xmx
内存泄漏找到泄漏点并修复
对象生命周期过长优化代码,及时释放引用

三、栈溢出 (StackOverflowError)

场景

  1. 递归过深:递归没有终止条件
  2. 方法调用链太长:循环依赖

示例代码

// 栈溢出示例
public class StackOverflow {
    private int depth = 0;
    
    public void recursive() {
        depth++;
        recursive();  // 无限递归
    }
    
    public static void main(String[] args) {
        StackOverflow sf = new StackOverflow();
        try {
            sf.recursive();
        } catch (StackOverflowError e) {
            System.out.println("递归深度: " + sf.depth);
        }
    }
}
 
// 运行参数:-Xss128k
// 结果:java.lang.StackOverflowError

栈大小影响

栈大小递归深度(约)
128k1000-2000
256k2000-4000
512k4000-8000
1m8000-16000

解决方案

  • 增大栈大小:-Xss512k
  • 优化递归:改为迭代
  • 检查代码:修复无限递归

四、无法创建线程 (unable to create new native thread)

场景

创建太多线程,系统资源耗尽。

// 线程创建过多
public class ThreadOOM {
    public static void main(String[] args) {
        while (true) {
            new Thread(() -> {
                try {
                    Thread.sleep(1000000);
                } catch (InterruptedException e) {}
            }).start();
        }
    }
}
 
// 结果:java.lang.OutOfMemoryError: unable to create new native thread

原因分析

每个 Java 线程需要占用本地内存:

  • 栈空间(-Xss)
  • 线程本地存储
  • 操作系统资源

解决方案

// 使用线程池替代无限制创建线程
ExecutorService executor = Executors.newFixedThreadPool(100);
// 或自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10, 100, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

五、方法区溢出 (Metaspace)

场景

  1. 动态生成类:CGLib、动态代理、JSP
  2. 加载太多类:依赖库过多

示例代码

// CGLib 生成大量类
public class MetaspaceOOM {
    static class OOMObject {}
    
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> {
                return proxy.invokeSuper(obj, args1);
            });
            enhancer.create();  // 每次生成新类
        }
    }
}
 
// 运行参数:-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
// 结果:java.lang.OutOfMemoryError: Metaspace

常见框架问题

框架问题场景
SpringAOP 动态代理
HibernateCGLib 字节码增强
JSP每个 JSP 编译为一个类
Groovy动态脚本生成类

解决方案

# 增大元空间
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
 
# 限制类加载(可选)
-XX:+DisableExplicitGC  # 可能导致元空间无法释放

六、直接内存溢出 (Direct buffer memory)

场景

NIO 使用直接内存(DirectByteBuffer),不受堆大小限制。

// 直接内存溢出
public class DirectMemoryOOM {
    public static void main(String[] args) {
        List<ByteBuffer> list = new ArrayList<>();
        while (true) {
            list.add(ByteBuffer.allocateDirect(1024 * 1024));  // 1MB
        }
    }
}
 
// 运行参数:-XX:MaxDirectMemorySize=10m
// 结果:java.lang.OutOfMemoryError: Direct buffer memory

直接内存特点

特性说明
分配方式ByteBuffer.allocateDirect()
回收方式System.gc() 或 Cleaner
性能零拷贝,访问更快
限制默认与堆大小相同

解决方案

// 1. 限制直接内存大小
-XX:MaxDirectMemorySize=256m
 
// 2. 手动释放(NIO 不会自动释放)
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// 使用完毕后
((DirectBuffer) buffer).cleaner().clean();
 
// 3. 使用池化技术
// Netty 的 PoolArena 管理 DirectBuffer

七、内存泄漏

什么是内存泄漏?

对象不再使用,但无法被 GC 回收。

正常情况:
对象使用完毕 → 引用失效 → GC 回收

内存泄漏:
对象使用完毕 → 引用仍存在 → 无法回收 → 堆逐渐被填满 → OOM

常见泄漏场景

1. 静态集合

// 静态集合持有对象引用
public class StaticCollectionLeak {
    static List<Object> list = new ArrayList<>();
    
    public void add(Object obj) {
        list.add(obj);  // 对象永远不会被回收
    }
}
 
// 解决方案:使用弱引用或及时清理
static List<WeakReference<Object>> list = new ArrayList<>();

2. 未关闭的资源

// 连接未关闭
public void query() {
    Connection conn = null;
    try {
        conn = DriverManager.getConnection(url);
        // ... 使用连接
        // 忘记关闭
    } finally {
        // conn.close();  // 需要关闭
    }
}
 
// 解决方案:使用 try-with-resources
try (Connection conn = DriverManager.getConnection(url)) {
    // ... 使用连接
}  // 自动关闭

3. 监听器未注销

// 监听器泄漏
public class ListenerLeak {
    private List<EventListener> listeners = new ArrayList<>();
    
    public void addListener(EventListener listener) {
        listeners.add(listener);
    }
    
    // 缺少 removeListener 方法
}
 
// 解决方案:使用 WeakHashMap 或提供注销方法

4. ThreadLocal

// ThreadLocal 泄漏
public class ThreadLocalLeak {
    static ThreadLocal<byte[]> local = new ThreadLocal<>();
    
    public void process() {
        local.set(new byte[1024 * 1024]);  // 1MB
        // 忘记 remove
    }
}
 
// 解决方案:使用完毕后 remove
try {
    local.set(value);
    // ...
} finally {
    local.remove();
}

5. 缓存无界

// 缓存无限增长
public class CacheLeak {
    static Map<String, Object> cache = new HashMap<>();
    
    public void put(String key, Object value) {
        cache.put(key, value);  // 无限增长
    }
}
 
// 解决方案:使用有界缓存
// 1. LinkedHashMap + LRU
Map<String, Object> cache = new LinkedHashMap<String, Object>(100, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > 100;  // 最多 100 个
    }
};
 
// 2. Guava Cache
Cache<String, Object> cache = CacheBuilder.newBuilder()
    .maximumSize(100)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build();
 
// 3. Caffeine Cache
Cache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(100)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build();

八、排查工具

1. 命令行工具

# jps - 查看 Java 进程
jps -l
 
# jstat - 查看 GC 统计
jstat -gc <pid> 1000  # 每秒输出一次
 
# jmap - 堆内存分析
jmap -heap <pid>              # 堆配置和使用
jmap -histo:live <pid>        # 存活对象统计
jmap -dump:format=b,file=dump.hprof <pid>  # 导出堆转储
 
# jstack - 线程栈分析
jstack <pid> > thread.txt     # 导出线程栈
 
# jinfo - JVM 参数查看
jinfo -flags <pid>            # 查看 JVM 参数

2. 可视化工具

工具用途
VisualVM内存、CPU、线程监控
JConsoleJMX 监控
MAT堆转储分析
Arthas在线诊断
JProfiler性能分析

3. MAT 使用技巧

1. 打开 dump.hprof 文件
2. 查看 Leak Suspects Report(泄漏嫌疑报告)
3. Dominator Tree(支配树)查看大对象
4. Histogram(直方图)查看对象数量
5. Path to GC Roots(GC 根路径)查找引用链

4. Arthas 快速诊断

# 安装并启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
 
# 常用命令
dashboard          # 总览面板
thread             # 线程列表
thread -n 5        # CPU 使用最高的 5 个线程
memory             # 内存信息
heapdump           # 导出堆转储
classloader        # 类加载器信息

九、排查流程

OOM 排查流程图

OOM 发生

┌───────────────────────┐
│ 1. 确认 OOM 类型       │
│    - 堆?栈?方法区?   │
└───────────┬───────────┘

┌───────────────────────┐
│ 2. 导出内存快照        │
│    jmap -dump         │
└───────────┬───────────┘

┌───────────────────────┐
│ 3. 分析快照           │
│    MAT / VisualVM     │
└───────────┬───────────┘

┌───────────────────────┐
│ 4. 找到占用内存的对象   │
│    - 最大对象          │
│    - 对象数量最多      │
└───────────┬───────────┘

┌───────────────────────┐
│ 5. 查找 GC Root       │
│    谁持有这些对象?    │
└───────────┬───────────┘

┌───────────────────────┐
│ 6. 定位代码位置        │
│    找到泄漏点          │
└───────────┬───────────┘

┌───────────────────────┐
│ 7. 修复并验证          │
└───────────────────────┘

十、最佳实践

JVM 参数配置

# 堆内存
-Xms2g              # 初始堆大小
-Xmx2g              # 最大堆大小(与 -Xms 相同避免动态扩容)
 
# 元空间
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
 
# 直接内存
-XX:MaxDirectMemorySize=256m
 
# GC 日志(JDK8)
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log
 
# GC 日志(JDK9+)
-Xlog:gc*:file=/path/to/gc.log:time,uptime:filecount=5,filesize=10m
 
# OOM 时生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dump.hprof

代码规范

// 1. 使用 try-with-resources
try (InputStream is = new FileInputStream(file)) {
    // ...
}
 
// 2. 及时清理 ThreadLocal
finally { threadLocal.remove(); }
 
// 3. 使用有界缓存
Cache<String, Object> cache = Caffeine.newBuilder()
    .maximumSize(1000)
    .build();
 
// 4. 使用线程池
ExecutorService executor = new ThreadPoolExecutor(
    10, 100, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)
);
 
// 5. 避免静态集合无限增长
// 定期清理或使用弱引用

常见面试题

Q1: OOM 和 StackOverflow 有什么区别?

特性OOMStackOverflow
原因内存不足栈深度超限
恢复无法恢复,JVM 可能崩溃可以捕获并处理
常见场景对象太多/内存泄漏递归太深

Q2: 内存泄漏和内存溢出有什么区别?

特性内存泄漏内存溢出
定义对象无法回收内存不足
关系泄漏可能导致溢出溢出不一定是泄漏
解决找到泄漏点修复增大内存或减少对象

Q3: 如何排查 OOM?

1. 分析错误信息,确定 OOM 类型
2. 导出堆转储(jmap 或自动生成)
3. 使用 MAT 分析大对象
4. 查找 GC Root 定位引用链
5. 找到代码位置并修复

Q4: 如何避免内存泄漏?

  • 使用 try-with-resources 自动关闭资源
  • 及时清理 ThreadLocal
  • 使用有界缓存
  • 避免静态集合无限增长
  • 及时注销监听器

小结

OOM 类型原因解决方案
Java heap space对象太多/内存泄漏增大堆、修复泄漏
StackOverflowError递归太深增大栈、改迭代
unable to create thread线程太多使用线程池
Metaspace类太多增大元空间
Direct buffer memoryNIO 直接内存不足增大直接内存

核心排查工具

  • jmap:导出堆转储
  • MAT:分析堆转储
  • Arthas:在线诊断

预防措施

  • 合理配置 JVM 参数
  • 遵循代码规范
  • 定期进行压力测试