📊 OOM 常见类型总览
| 错误类型 | 触发区域 | 根本原因 | 常见场景 |
|---|---|---|---|
| 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回收
典型场景:
java
// 场景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());
}
}
}
排查步骤:
bash
# 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
解决方案:
java
// 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%
- 连续5次GC都满足上述条件
典型场景:
java
// 场景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很快失去引用,成为垃圾
}
}
}
排查方法:
bash
# 1. 查看GC详细日志
java -Xlog:gc*,gc+heap=debug:file=gc.log -Xmx512m YourApp
# 2. 使用VisualGC或JConsole监控
# 3. 分析GC日志文件
解决方案:
java
// 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
核心原因:
加载的类太多,元空间不足
典型场景:
java
// 场景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
}
}
}
排查方法:
bash
# 1. 查看元空间使用情况
jstat -gc <pid> | grep MC
# MC: 元空间容量
# MU: 元空间已使用
# 2. 查看加载的类
jcmd <pid> GC.class_stats
# 3. dump类加载信息
-XX:+TraceClassLoading -XX:+TraceClassUnloading
解决方案:
java
// 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)耗尽
典型场景:
java
// 场景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));
}
}
}
排查方法:
bash
# 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
解决方案:
java
// 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
核心原因:
创建的线程数超过系统限制
典型场景:
java
// 场景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<>() // 队列太小
);
}
}
排查方法:
bash
# 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>
解决方案:
java
// 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(受堆大小限制)
典型场景:
java
// 场景:创建超大数组
public class HugeArrayOOM {
public void createHugeArray() {
// 尝试创建20亿个元素的int数组
// 20亿 * 4字节 ≈ 8GB
int[] hugeArray = new int[2_000_000_000];
}
}
解决方案:
java
// 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终止进程
排查方法:
bash
# 查看OOM Killer日志
dmesg | grep -i "out of memory"
dmesg | grep -i "killed process"
# 查看系统内存
free -h
cat /proc/meminfo
# 查看进程内存
ps aux --sort=-%mem | head -20
解决方案:
java
// 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时,按此流程排查:
bash
# 第一步:立即保存现场
# 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. 代码层面
java
// 使用内存敏感的数据结构
// 使用WeakHashMap、SoftReference
// 及时关闭资源
// 使用try-with-resources
2. JVM参数优化
bash
# 生产环境推荐配置
-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. 监控告警
yaml
# 需要监控的指标:
# 1. 堆内存使用率 > 80% 告警
# 2. GC时间占比 > 20% 告警
# 3. 老年代增长速率
# 4. Metaspace使用率
# 5. 线程数增长
# 6. 直接内存使用
💎 总结
| OOM类型 | 关键特征 | 优先排查点 |
|---|---|---|
| heap space | 对象太多,GC无法回收 | 大对象、内存泄露、缓存 |
| GC overhead | GC时间长,回收效率低 | 循环创建对象、字符串处理 |
| Metaspace | 类加载过多 | 动态代理、反射、热部署 |
| Direct buffer | 堆外内存不足 | Netty、NIO、MMAP |
| unable to create thread | 线程数超标 | 线程池配置、递归调用 |
| array size | 数组过大 | 大数组创建 |
| OOM Killer | 系统内存耗尽 | 容器限制、物理内存 |
anyway,预防优于治疗。在生产环境部署前,必须进行压力测试和内存分析,建立完善的监控体系,才能避免OOM对业务造成严重影响。