OOM 常见类型
OOM (Out Of Memory) 是指应用程序在运行时内存不足,无法分配新的内存空间,导致程序崩溃或异常终止的现象。
| 错误类型 | 触发区域 | 根本原因 | 常见场景 |
|---|---|---|---|
| Java heap space | 堆内存 | 对象太多,堆放不下 | 内存泄露、大对象、流量突增 |
| GC overhead limit exceeded | 堆内存 | GC效率低下 | 内存泄露、堆太小、代码问题(循环创建对象、字符串处理) |
| Metaspace | 元空间 | 类加载太多 | 动态代理、反射、热部署 |
| Direct buffer memory | 直接内存 | 堆外内存不足 | NIO、Netty、MMAP |
| Unable to create new native thread | 栈/系统 | 线程太多 | 线程池配置不当、递归过深 |
| Requested array size exceeds VM limit | 堆内存 | 数组过大 | 大数组创建 |
| Kill process or sacrifice child | 系统 | 系统内存不足 | 容器限制、物理内存不足 |
🔍 1. Java heap space
错误信息:
java.lang.OutOfMemoryError: Java heap space
核心原因:
对象实例占满了整个堆内存,且无法被GC回收
- 堆内存设置过小
- 内存泄漏(对象无法被 GC 回收)
- 一次性加载大量数据
- 对象生命周期过长
典型场景:
// 场景1:内存泄露 - 静态集合类持有引用
public class MemoryLeak {
private static final List<byte[]> LIST = new ArrayList<>();
public void addData() {
while (true) {
LIST.add(new byte[1024 * 1024]); // 1MB
}
}
}
// 场景2:大对象处理
public class BigObject {
public void processLargeFile() {
// 一次性读取大文件到内存
byte[] fileContent = Files.readAllBytes(Paths.get("huge_file.bin")); // 10GB文件
}
}
// 场景3:缓存失控
public class CacheOOM {
private Map<String, String> cache = new HashMap<>();
public void loadDataToCache() {
// 从数据库加载大量数据到内存
List<User> users = userDao.findAll(); // 百万条记录
for (User user : users) {
cache.put(user.getId(), user.serialize());
}
}
}
排查步骤:
# 1. 查看当前堆内存使用
jmap -heap <pid>
# 2. 生成堆转储文件
jmap -dump:format=b,file=heap.hprof <pid>
# 3. 实时监控GC
jstat -gc <pid> 1000 # 每秒打印一次
# 4. 使用jcmd
jcmd <pid> GC.heap_info
解决方案:
// 1. 合理设置JVM参数
// 生产环境示例
-Xms4g -Xmx4g // 堆内存4G,避免动态扩展
-XX:+UseG1GC // 使用G1垃圾收集器
-XX:MaxGCPauseMillis=200 // 目标暂停时间
-XX:+HeapDumpOnOutOfMemoryError // OOM时自动dump
-XX:HeapDumpPath=/path/to/dumps
// 2. 修复内存泄露代码
public class FixedMemoryLeak {
// 使用弱引用或软引用
private static final Map<String, SoftReference<BigObject>> CACHE = new WeakHashMap<>();
// 或使用LRU缓存
private static final Map<String, BigObject> safeCache =
Collections.synchronizedMap(new LinkedHashMap<String, BigObject>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > 1000; // 限制大小
}
});
}
// 3. 分批处理大数据
public class BatchProcessor {
public void processLargeData() {
int batchSize = 1000;
int offset = 0;
while (true) {
List<Data> batch = dao.findBatch(offset, batchSize);
if (batch.isEmpty()) break;
processBatch(batch);
offset += batchSize;
// 提示GC,但不是强制
if (offset % 10000 == 0) {
System.gc();
}
}
}
}
⏳ 2. GC overhead limit exceeded
错误信息:
java.lang.OutOfMemoryError: GC overhead limit exceeded
核心原因:
JVM花费了98%以上的时间进行GC,但只回收了不到2%的堆内存
- GC 花费了 98% 的时间,但只回收了不到 2% 的内存
- 通常是内存泄漏的前兆
典型场景:
// 场景1:字符串拼接在循环中
public class StringOOM {
public String buildHugeString() {
String result = "";
for (int i = 0; i < 1000000; i++) {
result += "some data "; // 每次创建新StringBuilder和String
}
return result;
}
}
// 场景2:频繁创建临时对象
public class TempObjectOOM {
public void process() {
while (true) {
// 每次循环都创建新对象,快速进入老年代
byte[] buffer = new byte[1024 * 1024]; // 1MB
// 但buffer很快失去引用,成为垃圾
}
}
}
排查方法:
# 1. 查看GC详细日志
java -Xlog:gc*,gc+heap=debug:file=gc.log -Xmx512m YourApp
# 2. 使用VisualGC或JConsole监控
# 3. 分析GC日志文件
解决方案:
// 1. 优化JVM参数
-Xmx2g -Xms2g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1ReservePercent=15
-XX:InitiatingHeapOccupancyPercent=35
// 2. 代码优化
public class OptimizedStringBuilder {
public String buildHugeString() {
StringBuilder sb = new StringBuilder(10000000); // 预分配容量
for (int i = 0; i < 1000000; i++) {
sb.append("some data ");
}
return sb.toString();
}
}
// 3. 对象复用
public class ObjectPool {
private static final ThreadLocal<ByteBuffer> bufferPool =
ThreadLocal.withInitial(() -> ByteBuffer.allocate(8192));
public void process() {
ByteBuffer buffer = bufferPool.get();
buffer.clear();
// 使用buffer
}
}
// 4. 关闭GC overhead限制(不推荐,临时方案)
-XX:-UseGCOverheadLimit
🧠 3. Metaspace (Java 8+)
错误信息:
java.lang.OutOfMemoryError: Metaspace
核心原因:
加载的类太多,元空间不足
- 类加载过多(动态代理、反射)
- 类加载器泄漏
- Metaspace 空间设置过小
典型场景:
// 场景1:动态代理大量生成类
public class DynamicProxyOOM {
public void createManyProxies() {
for (int i = 0; i < 1000000; i++) {
MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{MyInterface.class},
new MyInvocationHandler()
);
// 每个代理都会生成新类
}
}
}
// 场景2:热部署频繁
// 应用频繁重启,旧类未卸载
// 场景3:大量使用反射
public class ReflectionOOM {
public void loadManyClasses() throws Exception {
for (int i = 0; i < 10000; i++) {
Class<?> clazz = Class.forName("com.example.Class" + i);
// 每个类都加载到Metaspace
}
}
}
排查方法:
# 1. 查看元空间使用情况
jstat -gc <pid> | grep MC
# MC: 元空间容量
# MU: 元空间已使用
# 2. 查看加载的类
jcmd <pid> GC.class_stats
# 3. dump类加载信息
-XX:+TraceClassLoading -XX:+TraceClassUnloading
解决方案:
// 1. 调整Metaspace参数
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:CompressedClassSpaceSize=256m
// 2. 使用不同的ClassLoader
public class CustomClassLoader extends URLClassLoader {
public CustomClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
// 需要时创建新的ClassLoader实例
// 不需要时,整个ClassLoader可以被回收
}
// 3. 限制动态代理
public class LimitedProxyCreator {
private static final Map<String, Object> PROXY_CACHE = new ConcurrentHashMap<>();
public MyInterface getProxy(String key) {
return (MyInterface) PROXY_CACHE.computeIfAbsent(key, k ->
Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[]{MyInterface.class},
new MyInvocationHandler()
)
);
}
}
💾 4. Direct buffer memory
错误信息:
java.lang.OutOfMemoryError: Direct buffer memory
核心原因:
堆外内存(Direct Buffer)耗尽
- NIO 直接内存使用过多
- 未正确释放 DirectByteBuffer
典型场景:
// 场景1:Netty使用不当
public class NettyOOM {
public void startServer() {
// Netty默认使用堆外内存
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
// 如果不释放ByteBuf,会导致堆外内存泄露
ch.pipeline().addLast(new MyHandler());
}
});
}
}
// 场景2:大量使用ByteBuffer.allocateDirect
public class DirectBufferOOM {
public void allocateBuffers() {
List<ByteBuffer> buffers = new ArrayList<>();
while (true) {
// 每个Buffer 1MB,但不被GC管理
buffers.add(ByteBuffer.allocateDirect(1024 * 1024));
}
}
}
排查方法:
# 1. 查看直接内存使用
jcmd <pid> VM.native_memory summary scale=MB
# 2. 使用NMT(Native Memory Tracking)
-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory detail
# 3. 查看BufferPool
jcmd <pid> ManagementAgent.jmx_invoke sun.nio.ch.BufTracker getDirectBufferPoolCount
解决方案:
// 1. 限制直接内存大小
-XX:MaxDirectMemorySize=256m
// 2. Netty内存泄露检测
// 添加内存泄露检测
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator())
// 启用泄露检测
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);
// 3. 正确释放资源
public class SafeDirectBuffer {
public void useDirectBuffer() {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
try {
// 使用buffer
buffer.put("data".getBytes());
} finally {
// 重要:手动清理
if (buffer.isDirect()) {
((DirectBuffer) buffer).cleaner().clean();
}
}
}
}
// 4. 使用池化ByteBuf
public class PooledBufferExample {
private final ByteBufPool bufferPool = new ByteBufPool();
public void process() {
ByteBuf buf = bufferPool.borrowBuffer();
try {
// 使用buf
} finally {
bufferPool.returnBuffer(buf);
}
}
}
🧵 5. Unable to create new native thread
错误信息:
java.lang.OutOfMemoryError: unable to create new native thread
核心原因:
创建的线程数超过系统限制
- 线程创建过多
- 操作系统线程数限制
- 每个线程占用的栈内存过大
典型场景:
// 场景1:递归创建线程
public class RecursiveThreadOOM {
public void createThreads() {
while (true) {
new Thread(() -> {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
// 场景2:线程池配置不当
public class ThreadPoolOOM {
public void misuseExecutor() {
// 错误:使用无界队列,线程数会一直增长
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // corePoolSize
Integer.MAX_VALUE, // 最大线程数太大
60L, TimeUnit.SECONDS,
new SynchronousQueue<>() // 队列太小
);
}
}
排查方法:
# 1. 查看系统线程限制
ulimit -u
cat /proc/sys/kernel/threads-max
# 2. 查看Java进程线程数
pstree -p <pid> | wc -l
jstack <pid> | grep "java.lang.Thread.State" | wc -l
# 3. 查看每个线程的栈大小
jinfo -flag ThreadStackSize <pid>
解决方案:
// 1. 调整系统参数
// Linux系统
echo 10000 > /proc/sys/kernel/threads-max
ulimit -u 10000
// 2. 调整JVM参数
-Xss256k # 减小线程栈大小
-XX:VMThreadStackSize=256
// 3. 合理使用线程池
public class SafeThreadPool {
private final ExecutorService executor = new ThreadPoolExecutor(
10, // 核心线程
100, // 最大线程
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 有界队列
new ThreadFactoryBuilder()
.setNameFormat("worker-%d")
.build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 4. 使用虚拟线程(Java 19+)
public void useVirtualThreads() {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return "Done";
});
}
}
}
}
📈 6. Requested array size exceeds VM limit
错误信息:
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
核心原因:
尝试创建超过JVM限制的数组
最大限制:
- 32位JVM:约2^31-1 = 2,147,483,647 元素
- 64位JVM:约2^31-1(受堆大小限制)
典型场景:
// 场景:创建超大数组
public class HugeArrayOOM {
public void createHugeArray() {
// 尝试创建20亿个元素的int数组
// 20亿 * 4字节 ≈ 8GB
int[] hugeArray = new int[2_000_000_000];
}
}
解决方案:
// 1. 分批处理
public class BatchArrayProcessor {
public void processLargeData(long totalSize) {
int batchSize = 1000000; // 每批100万
int batches = (int) Math.ceil((double) totalSize / batchSize);
for (int i = 0; i < batches; i++) {
int currentSize = Math.min(batchSize, (int)(totalSize - i * batchSize));
int[] batch = new int[currentSize];
processBatch(batch);
}
}
}
// 2. 使用稀疏数组
public class SparseArrayExample {
private Map<Integer, Integer> sparseArray = new HashMap<>();
public void set(int index, int value) {
if (value != 0) { // 只存储非零值
sparseArray.put(index, value);
}
}
public int get(int index) {
return sparseArray.getOrDefault(index, 0);
}
}
// 3. 使用内存映射文件
public class MappedFileArray {
public void processLargeFile(String filePath, long arraySize) throws IOException {
try (RandomAccessFile file = new RandomAccessFile(filePath, "rw");
FileChannel channel = file.getChannel()) {
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, arraySize * 4
);
IntBuffer intBuffer = buffer.asIntBuffer();
// 像操作数组一样操作intBuffer
for (int i = 0; i < arraySize; i++) {
intBuffer.put(i, i * 2);
}
}
}
}
🔧 7. Kill process or sacrifice child
错误信息(Linux OOM Killer):
Out of memory: Kill process [pid] (java) score [score] or sacrifice child
核心原因:
系统物理内存耗尽,Linux OOM Killer终止进程
排查方法:
# 查看OOM Killer日志
dmesg | grep -i "out of memory"
dmesg | grep -i "killed process"
# 查看系统内存
free -h
cat /proc/meminfo
# 查看进程内存
ps aux --sort=-%mem | head -20
解决方案:
// 1. 调整JVM内存参数
// 不要设置过大,预留系统内存
-Xmx8g # 8GB堆内存
-Xms8g
-XX:MaxMetaspaceSize=512m
-XX:MaxDirectMemorySize=256m
-XX:ReservedCodeCacheSize=256m
// 2. 使用容器时设置内存限制
# Docker示例
docker run -m 10g --memory-reservation=8g your-java-app
# Kubernetes示例
resources:
limits:
memory: "10Gi"
requests:
memory: "8Gi"
// 3. 使用Native Memory Tracking监控
-XX:NativeMemoryTracking=detail
jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory detail.diff
// 4. 调整系统OOM Killer参数
echo 100 > /proc/sys/vm/overcommit_memory
echo 1 > /proc/sys/vm/overcommit_ratio
🛠 OOM 排查工具
1. JVM 自带工具
jps - 查看 Java 进程
# 查看所有 Java 进程
jps -l
# 查看进程及主类
jps -lv
jstat - 查看 GC 统计信息
# 查看 GC 情况,每 1000ms 输出一次
jstat -gc <pid> 1000
# 查看堆内存使用情况
jstat -gccapacity <pid>
# 查看 GC 原因
jstat -gccause <pid> 1000
jmap - 生成堆转储文件
# 生成堆转储文件
jmap -dump:format=b,file=heap.hprof <pid>
# 查看堆内存使用情况
jmap -heap <pid>
# 查看堆中对象统计信息
jmap -histo <pid> | head -n 20
jstack - 查看线程堆栈
# 查看线程堆栈
jstack <pid> > thread.txt
# 查看死锁
jstack -l <pid>
2. 可视化分析工具
MAT (Memory Analyzer Tool)
- 分析堆转储文件
- 查找内存泄漏
- 对象引用链分析
JProfiler
- 实时监控内存使用
- CPU 性能分析
- 线程分析
VisualVM
- JDK 自带的可视化工具
- 实时监控
- 堆转储分析
Arthas
- 阿里开源的 Java 诊断工具
- 无需重启应用
- 实时查看内存、线程、GC 等信息
# 启动 Arthas
java -jar arthas-boot.jar
# 查看 JVM 信息
dashboard
# 查看堆内存
memory
# 生成堆转储
heapdump /tmp/heap.hprof
🎯 实战排查流程
当发生OOM时,按此流程排查:
# 第一步:立即保存现场
# 1. 保存错误日志
# 2. 生成堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>
# 第二步:分析内存使用
# 1. 查看堆内存分布
jmap -histo:live <pid> | head -20
# 2. 查看GC情况
jstat -gcutil <pid> 1000 10
# 3. 查看线程状态
jstack <pid> > thread.dump
# 第三步:使用分析工具
# 1. Eclipse MAT
# 2. VisualVM
# 3. JProfiler
# 4. YourKit
# 第四步:复现和监控
# 1. 设置监控参数
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./java_pid<pid>.hprof
-XX:OnOutOfMemoryError="kill -3 %p"
-Xlog:gc*,gc+heap=debug:file=gc_%t.log
📊 预防策略
1. 代码层面
// 使用内存敏感的数据结构
// 使用WeakHashMap、SoftReference
// 及时关闭资源
// 使用try-with-resources
✅ 使用对象池
// 使用 Apache Commons Pool
GenericObjectPool<ExpensiveObject> pool = new GenericObjectPool<>(
new ExpensiveObjectFactory(),
new GenericObjectPoolConfig()
);
// 使用对象
ExpensiveObject obj = pool.borrowObject();
try {
// 使用对象
} finally {
pool.returnObject(obj); // 归还对象
}
✅ 及时释放资源
// 使用 try-with-resources
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
// 处理结果
}
✅ 避免创建不必要的对象
// ❌ 错误:每次都创建新对象
public String formatDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
// ✅ 正确:使用 ThreadLocal 复用对象
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String formatDate(Date date) {
return DATE_FORMAT.get().format(date);
}
// ✅ 更好:使用 Java 8 的 DateTimeFormatter(线程安全)
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
public String formatDate(LocalDate date) {
return date.format(FORMATTER);
}
2. JVM参数优化
# 基础内存设置
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小(建议与 Xms 相同,避免动态扩容)
-Xmn2g # 年轻代大小
-Xss256k # 每个线程的栈大小
# Metaspace 设置
-XX:MetaspaceSize=256m # Metaspace 初始大小
-XX:MaxMetaspaceSize=512m # Metaspace 最大大小
# GC 设置(G1 GC)
-XX:+UseG1GC # 使用 G1 垃圾收集器
-XX:MaxGCPauseMillis=200 # 最大 GC 停顿时间
-XX:G1HeapRegionSize=16m # G1 Region 大小
# OOM 时自动生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/logs/heapdump/
# GC 日志
-Xlog:gc*:file=/var/logs/gc.log:time,uptime,level,tags
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
# 其他优化
-XX:+DisableExplicitGC # 禁用 System.gc()
-XX:+UseStringDeduplication # 字符串去重(G1 GC)
# 生产环境推荐配置
-Xms4g -Xmx4g
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-XX:MaxDirectMemorySize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=35
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-Xlog:gc*,gc+heap=debug:file=gc.log
3. 监控告警
# 需要监控的指标:
# 1. 堆内存使用率 > 80% 告警
# 2. GC时间占比 > 20% 告警
# 3. 老年代增长速率
# 4. Metaspace使用率
# 5. 线程数增长
# 6. 直接内存使用
Prometheus + Grafana 监控
# prometheus.yml
scrape_configs:
- job_name: 'java-app'
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
关键指标监控
- 堆内存使用率:
jvm_memory_used_bytes / jvm_memory_max_bytes - GC 频率:
rate(jvm_gc_pause_seconds_count[5m]) - GC 耗时:
jvm_gc_pause_seconds_sum - 线程数:
jvm_threads_live_threads
告警规则
groups:
- name: jvm_alerts
rules:
# 堆内存使用率超过 85%
- alert: HighHeapMemoryUsage
expr: (jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}) > 0.85
for: 5m
annotations:
summary: "堆内存使用率过高"
# GC 耗时超过 1 秒
- alert: HighGCTime
expr: rate(jvm_gc_pause_seconds_sum[5m]) > 1
for: 5m
annotations:
summary: "GC 耗时过长"
4. 压力测试
使用 JMeter、Gatling 等工具进行压力测试:
# JMeter 压力测试
jmeter -n -t test-plan.jmx -l results.jtl
# 同时监控内存使用
jstat -gc <pid> 1000 > gc-stats.log
🎃 总结
OOM 排查核心要点
- ✅ 及时发现: 配置监控告警,第一时间发现问题
- ✅ 保留现场: 配置
-XX:+HeapDumpOnOutOfMemoryError - ✅ 分析定位: 使用 MAT 等工具分析堆转储文件
- ✅ 修复验证: 修改代码后进行压力测试验证
- ✅ 预防为主: 代码规范、资源管理、容量规划
最佳实践清单
- [ ] JVM 参数合理配置
- [ ] 开启 OOM 时自动生成堆转储
- [ ] 开启 GC 日志
- [ ] 配置内存监控告警
- [ ] 定期进行压力测试
- [ ] 代码审查关注内存使用
- [ ] 使用对象池复用对象
- [ ] 及时释放资源(try-with-resources)
- [ ] 避免大对象一次性加载
- [ ] 合理使用缓存(设置过期时间和容量限制)