OOM 与内存泄漏
OutOfMemoryError(OOM)是 Java 应用常见的问题。理解 OOM 的类型和排查方法,是 Java 开发者的必备技能。
一、OOM 类型总览
| 类型 | 区域 | 错误信息 |
|---|---|---|
| 堆溢出 | Heap | java.lang.OutOfMemoryError: Java heap space |
| 栈溢出 | Stack | java.lang.StackOverflowError |
| 栈扩展失败 | Stack | java.lang.OutOfMemoryError: unable to create new native thread |
| 方法区溢出 | Metaspace | java.lang.OutOfMemoryError: Metaspace |
| 直接内存溢出 | Direct Memory | java.lang.OutOfMemoryError: Direct buffer memory |
二、堆溢出 (Java heap space)
场景
- 对象太多:创建大量对象,GC 来不及回收
- 对象太大:单个超大对象
- 内存泄漏:对象无法被回收
示例代码
// 堆溢出示例
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)
场景
- 递归过深:递归没有终止条件
- 方法调用链太长:循环依赖
示例代码
// 栈溢出示例
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栈大小影响
| 栈大小 | 递归深度(约) |
|---|---|
| 128k | 1000-2000 |
| 256k | 2000-4000 |
| 512k | 4000-8000 |
| 1m | 8000-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)
场景
- 动态生成类:CGLib、动态代理、JSP
- 加载太多类:依赖库过多
示例代码
// 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常见框架问题
| 框架 | 问题场景 |
|---|---|
| Spring | AOP 动态代理 |
| Hibernate | CGLib 字节码增强 |
| 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、线程监控 |
| JConsole | JMX 监控 |
| 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 有什么区别?
| 特性 | OOM | StackOverflow |
|---|---|---|
| 原因 | 内存不足 | 栈深度超限 |
| 恢复 | 无法恢复,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 memory | NIO 直接内存不足 | 增大直接内存 |
核心排查工具:
- jmap:导出堆转储
- MAT:分析堆转储
- Arthas:在线诊断
预防措施:
- 合理配置 JVM 参数
- 遵循代码规范
- 定期进行压力测试