news 2026/10/22 18:52:37

Java OOM 四大经典场景排查实战:从堆转储到 CPU 飙升根治(含完整命令 + 修复方案)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java OOM 四大经典场景排查实战:从堆转储到 CPU 飙升根治(含完整命令 + 修复方案)

凌晨 3 点,监控告警疯狂响起 ——OutOfMemoryError、CPU 95%、服务不可用。
别慌,本文给你一套可复制的 OOM 排查 SOP + 4 个真实生产案例,涵盖堆/GC/元空间/线程四种 OOM,每种都讲清楚「为什么会触发」+「怎么排查」+「怎么根治」+「怎么避免」。


一、OOM 排查通用 SOP(先看流程图,再看案例)

无论哪种 OOM,排查都遵循这套思路:看错误类型 → 抓现场(heap dump + thread dump) → 系统层验证(CPU/线程数/内存) → 修复。

1. OOM 排查全景图

OutOfMemoryError 异常抛出 │ ▼ ┌─────────────────────────┐ │ Step1: 识别 OOM 类型 │ 错误信息关键词 → 4 大类型之一 └────────┬────────────────┘ ▼ ┌─────────────────────────┐ │ Step2: 抓 heap dump │ -XX:+HeapDumpOnOutOfMemoryError │ 抓 thread dump │ jstack │ 看 GC 情况 │ jstat -gcutil └────────┬────────────────┘ ▼ ┌─────────────────────────┐ │ Step3: 系统层排查 │ top / vmstat / free │ (CPU 飙升关联) │ 找出 OOM 与 CPU 飙升的因果 └────────┬────────────────┘ ▼ ┌─────────────────────────┐ │ Step4: 分析 dump 文件 │ MAT / JProfiler / VisualVM └────────┬────────────────┘ ▼ ┌─────────────────────────┐ │ Step5: 止血 + 根治 │ 扩容 / 重启 / 改代码 / 限流 └─────────────────────────┘

2. OOM 错误类型识别(一图看懂)

┌──────────────────────────────────────────────────────────────┐ │ OutOfMemoryError │ ├───────────────────────────────────────────────────────┬──────┤ │ │ │ │ ┌─────────────────────┐ ┌──────────────────────┐ │ │ │ │ Java heap space │ │ GC overhead limit │ │ GC │ │ │ 堆内存溢出 │ │ exceeded │ │ 类 │ │ └─────────────────────┘ └──────────────────────┘ │ │ │ ┌─────────────────────┐ ┌──────────────────────┐ │ │ │ │ Metaspace / PermGen │ │ Unable to create new │ │ 非 │ │ │ 元空间溢出 │ │ native thread │ │ GC │ │ └─────────────────────┘ └──────────────────────┘ │ 类 │ │ ┌─────────────────────┐ ┌──────────────────────┐ │ │ │ │ Direct buffer │ │ StackOverflowError │ │ │ │ │ memory 堆外内存 │ │ 栈溢出 │ │ │ │ └─────────────────────┘ └──────────────────────┘ │ │ └───────────────────────────────────────────────────────┴──────┘

3. 通用排查命令速查表

# === 必备 JVM 参数(出问题前就配上)=== -XX:+HeapDumpOnOutOfMemoryError # OOM 时自动 dump -XX:HeapDumpPath=/data/dump/ # dump 路径 -XX:+PrintGCDetails -Xloggc:/tmp/gc.log # GC 日志 -XX:+UnlockDiagnosticVMOptions -XX:NativeMemoryTracking=summary # NMT 跟踪 native # === 抓 heap dump 三种方式 === # 方式 1: OOM 时自动 dump(最常用,推荐) # 方式 2: 主动 dump jmap -dump:format=b,file=/tmp/heap.hprof <PID> # 方式 3: jcmd(推荐,更安全) jcmd <PID> GC.heap_dump /tmp/heap.hprof # === 看堆内存分布 === jmap -heap <PID> # 堆配置 + 各代使用情况 jmap -histo:live <PID> | head 30 # 按实例数 / 内存排序的类 # === 看 GC 频率 === jstat -gcutil <PID> 1000 5 # 每 1s 输出 1 次,共 5 次 # === 抓线程栈 === jstack <PID> > /tmp/ts.log jstack <PID> | grep BLOCKED # 看锁竞争 # === CPU 飙升排查(OOM 经常伴随 CPU 飙升)=== top -Hp <PID> # 找出 CPU 最高的线程 printf "%x\n" <TID> # 转 16 进制 jstack <PID> | grep "nid=<hex>" # 找到那个线程在干嘛 # === 系统层 === free -h # 系统内存 ulimit -u # 用户最大线程数 cat /proc/sys/kernel/threads-max

4. 一键 OOM 排查脚本

#!/bin/bash # oom_check.sh - OOM 一键排查脚本 PID=$1 [ -z "$PID" ] && { echo "Usage: $0 <PID>"; exit 1; } echo "============ Step 1: 进程基本信息 ============" ps -p $PID -o pid,user,%cpu,%mem,rss,vsz,etime,cmd echo "============ Step 2: 堆内存配置 ============" jmap -heap $PID 2>/dev/null | grep -E "Heap Configuration|Max Heap|New Generation|Old Generation" echo "============ Step 3: GC 情况 ============" jstat -gcutil $PID 1000 3 echo "============ Step 4: 线程数 ============" echo "线程总数: $(ls /proc/$PID/task 2>/dev/null | wc -l)" echo "系统线程限制: $(cat /proc/sys/kernel/threads-max)" echo "============ Step 5: CPU Top 10 线程 ============" top -bn1 -Hp $PID | head -17 echo "============ Step 6: 系统内存 ============" free -h echo "============ Step 7: 抓 heap dump ============" jcmd $PID GC.heap_dump /tmp/heap_$(date +%Y%m%d_%H%M%S).hprof echo "Dump 已保存到 /tmp/heap_*.hprof,可用 MAT 分析" echo "============ Step 8: 抓线程栈 ============" jstack $PID > /tmp/jstack_$(date +%Y%m%d_%H%M%S).log echo "线程栈已保存到 /tmp/jstack_*.log"

二、案例 1:Java heap space —— 堆内存溢出

2.1 案例背景

某电商商品中心服务,运行 6 小时后突然抛java.lang.OutOfMemoryError: Java heap space,伴随CPU 从 40% 飙升到 90%(FGC 频率暴涨导致)。

2.2 Bug 代码

// ProductService.java - 商品推荐服务 @Service public class ProductRecommendService { // ❌ 静态 Map 永不清理,存储导致内存泄漏 private static final Map<Long, List<ProductDTO>> PRODUCT_CACHE = new HashMap<>(); public List<ProductDTO> recommendProducts(Long userId) { // ❌ 每次调用都往 Map 里塞,且没有清理机制 String cacheKey = "user:" + userId; if (PRODUCT_CACHE.containsKey(cacheKey)) { return PRODUCT_CACHE.get(cacheKey); } List<ProductDTO> products = productMapper.findByUserId(userId); // ❌ 大对象直接放静态 Map,无 TTL、无容量限制 PRODUCT_CACHE.put(cacheKey, products); return products; } }

2.3 OOM 错误日志

2026-09-15 03:24:18 ERROR [http-nio-8080-exec-125] o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space] java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.ArrayList.grow(ArrayList.java:267) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:241) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:233) at java.util.ArrayList.<init>(ArrayList.java:157) at com.acme.service.ProductRecommendService.recommendProducts(ProductRecommendService.java:42) at com.acme.controller.ProductController.recommend(ProductController.java:58) ...

2.4 排查过程(OOM 排查 + CPU 飙升排查双管齐下)

Step 1 - OOM 类型识别:

错误关键字:Java heap space → 确认是「堆内存溢出」,对象占用超过 -Xmx 限制

Step 2 - jmap 看堆分布:

$ jmap -heap 23841 Attaching to process ID 23841, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.272-b10 using parallel threads in the new generation. using thread-local object allocation. Concurrent Mark-Sweep GC Heap Configuration: MinHeapFreeRatio = 40 MaxHeapFreeRatio = 70 MaxHeapSize = 4294967296 (4096.0 MB) ← 堆上限 4G NewSize = 2147483648 (2048.0 MB) MaxNewSize = 2147483648 (2048.0 MB) OldSize = 5439488 (5.1875 MB) MetaspaceSize = 21807104 (20.796875 MB) Heap Usage: PS Young Generation Eden Space: capacity = 2013265920 (1920.0 MB) used = 2013265920 (1920.0 MB) ← Eden 100% 占满 ... PS Old Generation capacity = 2147483648 (2048.0 MB) used = 2113929216 (2016.0 MB) ← Old 98.5% 占满

关键信号:Old Gen 已用 2016M / 2048M,接近上限,对象晋升到老年代后无法回收。

Step 3 - jmap -histo 看谁占内存:

$ jmap -histo:live 23841 | head -20 num #instances #bytes class name ---------------------------------------------- 1: 1523891 385299512 com.acme.dto.ProductDTO ← 占 367MB 2: 523891 75463512 java.util.HashMap$Node 3: 523891 33529024 java.util.ArrayList 4: 523891 25146768 [Ljava.lang.Object; ...

关键信号:ProductDTO 占了 150 万个实例、367MB,疑似内存泄漏。

Step 4 - jstat -gcutil 看 GC 频率(这是排查 CPU 飙升的关键):

$ jstat -gcutil 23841 1000 5 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 8.45 99.87 95.12 89.34 12050 35.23 1289 156.78 201.99 0.00 0.00 15.32 99.91 95.12 89.34 12060 35.28 1298 158.12 203.40 0.00 0.00 20.15 99.93 95.12 89.34 12070 35.33 1308 159.45 204.78

关键信号:

  • O(Old Gen)持续 > 99%
  • FGC 频率:每 10 秒一次(正常应为几小时一次)
  • FGCT(Full GC 累计耗时):159 秒

Step 5 - top 验证 CPU 飙升(CPU 飙升排查的入口):

$ top -c PID USER PR NI VIRT RES SHR S %CPU %MEM COMMAND 23841 app 20 0 8.2g 4.0g 12m R 92.8 25.3 java -jar product-service.jar ← CPU 92.8% $ top -Hp 23841 PID USER PR VIRT RES SHR S %CPU TIME+ COMMAND 24098 app 20 8.2g 4.0g 12m R 12.5 2:35.15 java ← GC 线程,CPU 12.5% 24099 app 20 8.2g 4.0g 12m R 12.3 2:34.89 java ← GC 线程,CPU 12.3% 24100 app 20 8.2g 4.0g 12m R 12.1 2:34.67 java ← GC 线程 ... (12 个 GC 线程,每个占 12% CPU,加起来 92% 以上)

关键信号:12 个 GC 线程,每个占 12%+ CPU,合计占用 CPU 90%+→CPU 飙升是 OOM 触发的频繁 Full GC 导致的!

Step 6 - jstack 看 GC 线程在干嘛:

$ printf "%x\n" 24098 5e32 $ jstack 23841 | grep -A 8 "nid=0x5e32" "GC-thread-8" #24098 os_prio=0 tid=0x00007f1234 nid=0x5e32 runnable java.lang.Thread.State: RUNNABLE at java.lang.ref.ReferenceQueue.enqueue(ReferenceQueue.java:154) at java.lang.ref.ReferenceQueue.enqueuePendingReference(ReferenceQueue.java:175) ... at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:127)

2.5 排查思路总结

┌─────────────────────────────────────────────────────┐ │ 1. OOM 类型:Java heap space │ │ ↓ │ │ 2. 堆使用情况:Old Gen 99%,FGC 频率每 10 秒一次 │ │ ↓ │ │ 3. 占用内存大头:ProductDTO 152 万实例,367 MB │ │ ↓ │ │ 4. 代码定位:静态 Map 无清理 → 内存泄漏 │ │ ↓ │ │ 5. CPU 飙升根因:频繁 Full GC 导致 12 个 GC 线程高占用│ └─────────────────────────────────────────────────────┘

2.6 应急止血

# 方案 1: 临时扩容堆内存(应急) # 重启时把 -Xmx 从 4G 调到 8G java -Xmx8g -jar product-service.jar # 方案 2: arthas 动态清理(如果服务重要不能重启) # 进入 arthas 后: ognl '@com.acme.service.ProductRecommendService@PRODUCT_CACHE.clear()' # 然后观察 FGC 频率是否降下来 # 方案 3: 限流降级 # 用 sentinel/resilience4j 限流推荐接口

2.7 修复方案

// ✅ 修复 1: 用 Caffeine 替代静态 Map(带 TTL + 最大容量) @Service public class ProductRecommendService { private final Cache<String, List<ProductDTO>> cache = Caffeine.newBuilder() .maximumSize(100_000) // 最多 10w 个 key .expireAfterWrite(Duration.ofMinutes(10)) // 写后 10 分钟过期 .expireAfterAccess(Duration.ofMinutes(5)) // 访问后 5 分钟过期 .recordStats() .build(); public List<ProductDTO> recommendProducts(Long userId) { String cacheKey = "user:" + userId; return cache.get(cacheKey, k -> productMapper.findByUserId(userId)); } } // ✅ 修复 2: 大对象分页加载,避免一次性加载 public List<ProductDTO> recommendProducts(Long userId) { int pageSize = 100; List<ProductDTO> result = new ArrayList<>(); for (int page = 0; page < 10; page++) { Page<ProductDTO> p = productMapper.findPage(userId, page, pageSize); result.addAll(p.getRecords()); if (!p.hasNext()) break; } return result; } // ✅ 修复 3: JVM 参数优化 // -Xms4g -Xmx4g # 堆大小固定,避免动态调整 // -XX:+UseG1GC # G1 替代 CMS // -XX:MaxGCPauseMillis=200 # 最大停顿 200ms // -XX:+HeapDumpOnOutOfMemoryError # OOM 自动 dump // -XX:HeapDumpPath=/data/dump/

三、案例 2:GC overhead limit exceeded —— GC 开销超限

3.1 案例背景

某订单服务在压测时,跑 2 小时后开始抛java.lang.OutOfMemoryError: GC overhead limit exceeded,CPU 持续 95%+,但堆内存看起来没满。

3.2 错误特征(区别于 heap space)

异常信息: java.lang.OutOfMemoryError: GC overhead limit exceeded at java.util.HashMap.resize(HashMap.java:704) at java.util.HashMap.putVal(HashMap.java:663) at com.acme.service.OrderService.batchInsert(OrderService.java:78) ⚠ 注意:JVM 抛出该异常时,堆内存通常**不是 100% 占满**,而是 98%+ 时间在做 GC,但回收不到 2% 空间。

3.3 Bug 代码

// OrderService.java public void batchInsert(List<Order> orders) { // ❌ 一次性插入 100 万条订单,且每条都用 Map 包装 for (Order order : orders) { Map<String, Object> paramMap = new HashMap<>(); // ❌ 大循环内分配 paramMap.put("userId", order.getUserId()); paramMap.put("amount", order.getAmount()); paramMap.put("status", order.getStatus()); // ... 50 个字段 orderMapper.insertOrder(paramMap); // ❌ 每次插入都是一次 GC 压力 } }

3.4 排查过程

Step 1 - jstat 看 GC 详情(关键诊断步骤):

$ jstat -gcutil 15891 1000 5 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 98.20 87.32 95.12 89.34 12050 35.23 58291 9456.78 9492.01 0.00 0.00 99.50 87.45 95.12 89.34 12060 35.28 58412 9468.12 9503.40

关键信号:

  • FGC 次数:58291 次(5 分钟内执行了 5w+ 次 Full GC)
  • Old Gen 只占 87%(没满,但 GC 回收效率极低)
  • FGCT 累计 9456 秒≈ 2.6 小时纯 GC

Step 2 - 看 GC 日志:

$ tail -50 gc.log [2026-09-15 10:23:18.456+0800] 15891.234s: Full GC (Ergonomics) ... [PSYoungGen: 1952M->0M(1920M)] ← 新生代被清空 [ParOldGen: 1789M->1786M(2048M)] ← 老年代几乎没回收(只回收 3M) 1789M->1786M(3968M), [Metaspace: 95M->95M], 0.1284125 secs] [Times: user=0.50 sys=0.00, real=0.13 secs]

关键信号:Full GC 回收老年代 1789M → 1786M,只回收 3MB(0.16%),效率极差!

Step 3 - top 验证 CPU:

$ top -c PID USER PR NI VIRT RES SHR S %CPU %MEM COMMAND 15891 app 20 0 8.2g 3.9g 12m R 98.8 24.3 java -jar order-service.jar

CPU 飙到 98.8%,全部都在做 GC。

3.5 排查思路

  1. 看 OOM 类型:GC overhead limit exceeded是 JVM 保护性机制(默认开启-XX:+UseGCOverheadLimit)
  2. 看 GC 日志:回收前后内存变化,回收效率 < 2% 就会触发
  3. 看 GC 频率:FGC 持续高频(每分钟数十次)
  4. 代码定位:内存碎片化 / 大量朝生夕死对象 / 内存泄漏初期

3.6 应急止血

# 方案 1: 主动触发 Full GC(先释放一点内存) jmap -histo:live 15891 | head -20 # 触发 live 标记并 Full GC # 方案 2: 临时关闭 GC overhead limit 检查(应急,可能导致真 OOM) java -XX:-UseGCOverheadLimit -jar order-service.jar # 方案 3: 限流,停止新订单写入

3.7 修复方案

// ✅ 修复 1: 批量插入优化 public void batchInsert(List<Order> orders) { int batchSize = 500; // ✅ 使用 mybatis batch 模式 SqlSession sqlSession = SessionFactory.openSession(ExecutorType.BATCH, false); try { OrderMapper mapper = sqlSession.getMapper(OrderMapper.class); for (int i = 0; i < orders.size(); i++) { mapper.insertOrder(orders.get(i)); // 直接传对象 if ((i + 1) % batchSize == 0) { sqlSession.flushStatements(); // 每 500 条提交一次 } } sqlSession.flushStatements(); sqlSession.commit(); } finally { sqlSession.close(); } } // ✅ 修复 2: 对象复用,避免朝生夕死 private final ThreadLocal<Map<String, Object>> paramCache = ThreadLocal.withInitial(HashMap::new); public void batchInsert(List<Order> orders) { for (Order order : orders) { Map<String, Object> paramMap = paramCache.get(); // ✅ 复用 paramMap.clear(); paramMap.put("userId", order.getUserId()); // ... } } // ✅ 修复 3: 切换 G1 GC // -XX:+UseG1GC // -XX:MaxGCPauseMillis=200 // -XX:InitiatingHeapOccupancyPercent=45 # 提前启动混合 GC

四、案例 3:Metaspace / PermGen —— 元空间溢出

4.1 案例背景

某网关服务(基于 Spring Cloud Gateway + Groovy 动态脚本)运行 1 周后,抛java.lang.OutOfMemoryError: Metaspace,CPU 飙升到 85%。

4.2 Bug 代码

// GroovyScriptEngine.java - 动态执行用户上传的脚本 @Service public class DynamicScriptService { private final GroovyClassLoader classLoader = new GroovyClassLoader(); public Object executeScript(String scriptContent, Map<String, Object> params) { try { // ❌ 每次执行都生成新的 Class 对象到 Metaspace Class<?> scriptClass = classLoader.parseClass(scriptContent); // ❌ 上一个 Class 没清理,永久驻留 Script script = (Script) scriptClass.getDeclaredConstructor().newInstance(); Binding binding = new Binding(params); script.setBinding(binding); return script.run(); } catch (Exception e) { log.error("脚本执行失败", e); return null; } } }

4.3 OOM 错误日志

2026-09-22 14:32:18 ERROR [] o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dynamicScriptService': nested exception is java.lang.OutOfMemoryError: Metaspace at sun.reflect.GeneratedMethodAccessor123.invoke(<generated>) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.acme.gateway.DynamicScriptService.executeScript(DynamicScriptService.java:42)

4.4 排查过程

Step 1 - jcmd 看 Metaspace:

$ jcmd 18901 VM.metaspace 18901: Total Usage - Non-Class-Terminating: Class Space: 536870912 (512 MB) ← Metaspace 类空间 512MB Non-Class Space: 20971520 (20 MB) ... $ jstat -gcmetacapacity 18901 1000 3 MCMN MCMX MC CCS CCSC MCR CCR 0.0 1073741824.0 536870912.0 10485760.0 983040.0 18901.0 18850.0

关键信号:MC(已用)= 512MB,接近 MCMX(上限 1GB)。

Step 2 - jcmd 看 Class 加载统计:

$ jcmd 18901 VM.classloader_stats 18901: ClassLoaderDataKLASS instance total = 18392, loaded = 18392, unloaded = 0 ← 卸载数为 0,Class 全部驻留!

关键信号:loaded=18392,unloaded=0,所有 Class 都没被卸载,典型的 Class 泄漏。

Step 3 - top + jstack 排查 CPU 飙升:

$ top -Hp 18901 PID USER PR VIRT RES SHR S %CPU TIME+ COMMAND 19001 app 20 8.2g 4.5g 12m R 8.5 1:23.45 java ← JIT 编译线程 19002 app 20 8.2g 4.5g 12m R 8.3 1:23.012 java ... (8 个 JIT 线程,每个 8% CPU) 19012 app 20 8.2g 4.5g 12m R 5.2 0:54.32 java ← 应用线程

关键信号:JIT 编译线程 + 应用线程持续编译新 Class,CPU 飙升 85%。

4.5 排查思路

┌────────────────────────────────────────────┐ │ 1. OOM 类型:Metaspace │ │ ↓ │ │ 2. Class 加载情况:loaded=18392, unloaded=0│ │ ↓ │ │ 3. 定位:动态生成 Class 但没清理 │ │ ↓ │ │ 4. 常见原因:CGLib 代理、JSP 热部署、 │ │ Groovy 脚本、Lambda 大量生成 │ └────────────────────────────────────────────┘

4.6 应急止血

# 方案 1: 临时扩容 Metaspace(重启) java -XX:MaxMetaspaceSize=2g -jar gateway.jar # 方案 2: 主动触发 Class 卸载(Full GC + 解除引用) jcmd 18901 GC.run # 触发 Full GC # 但只有 GroovyClassLoader 实例被回收后,Class 才能被卸载 # 方案 3: arthas 清理 ClassLoader # 进入 arthas 后执行: classloader --load # 查看 ClassLoader # 找到对应的 ClassLoader,解除引用,让 ClassLoader 被 GC 回收

4.7 修复方案

// ✅ 修复 1: 复用 GroovyClassLoader,及时清理 Class 缓存 @Service public class DynamicScriptService { // ✅ 每个脚本有 TTL,定期清理 private final LoadingCache<String, Script> scriptCache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterAccess(Duration.ofMinutes(10)) .build(); public Object executeScript(String scriptContent, Map<String, Object> params) { Script script = scriptCache.get(scriptContent, key -> { GroovyClassLoader loader = new GroovyClassLoader(); // ✅ 每次新建 loader Class<?> clazz = loader.parseClass(key); Script s = (Script) clazz.getDeclaredConstructor().newInstance(); // 关键:保留 loader 引用,否则 Class 会被卸载 s.setProperty("__loader__", loader); return s; }); // ... } } // ✅ 修复 2: JVM 参数调优 // -XX:MetaspaceSize=256m # 初始大小 // -XX:MaxMetaspaceSize=512m # 上限(防止 OOM 后占满 native) // -XX:+UseG1GC -XX:MaxGCPauseMillis=200 // -XX:+CMSClassUnloadingEnabled # CMS 开启类卸载(如果用 CMS) // ✅ 修复 3: 监控告警 // 提前监控 Metaspace 使用率,> 70% 告警 // Micrometer + Prometheus @Bean public MeterBinder metaspaceMetrics() { return registry -> Gauge.builder("jvm.metaspace.used", Runtime.getRuntime(), r -> ManagementFactory.getMemoryPoolMXBeans().stream() .filter(p -> p.getName().equals("Metaspace")) .mapToLong(p -> p.getUsage().getUsed()) .sum()) .register(registry); }

五、案例 4:Unable to create new native thread —— 无法创建新线程

5.1 案例背景

某数据导入服务(多线程并行处理)压测时,抛java.lang.OutOfMemoryError: Unable to create new native thread,系统 loadavg 高达 64,CPU 上下文切换暴涨。

5.2 OOM 错误日志

2026-09-25 16:42:18 ERROR [task-pool-17823] o.s.s.q.ThreadUtils - Error creating thread java.lang.OutOfMemoryError: Unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:717) at com.acme.importer.DataImporter.parallelImport(DataImporter.java:78)

5.3 Bug 代码

// DataImporter.java public void parallelImport(List<DataFile> files) { // ❌ 每个文件都 new 一个 Thread for (DataFile file : files) { Thread thread = new Thread(() -> { importSingleFile(file); // ❌ 处理完线程未终止,但对象一直引用 }); thread.start(); } }

5.4 排查过程

Step 1 - OOM 类型识别:

错误关键字:Unable to create new native thread → 不是 Java 堆问题,是 native 层线程资源耗尽

Step 2 - 看线程数:

$ ls /proc/20891/task | wc -l 8234 ← 进程内线程数 8234,远超正常(一般 < 500)

Step 3 - 看系统限制:

$ cat /proc/sys/kernel/threads-max 32602 ← 系统最大线程数 32602 $ ulimit -u 32768 ← 用户级最大进程/线程数 $ free -h total used free shared buff/cache available Mem: 15Gi 12Gi 380Mi 120Mi 2.3Gi 1.2Gi

关键信号:线程数 8234 / 32602 = 25%已用,每个线程栈默认 1MB → native 内存占用 8GB+

Step 4 - top + vmstat 看 CPU 上下文切换(CPU 飙升排查):

$ top -c PID USER PR NI VIRT RES SHR S %CPU %MEM COMMAND 20891 app 20 0 12.2g 8.5g 12m R 78.5 56.3 java -jar>$ jstack 20891 | grep "java.lang.Thread.State" | sort | uniq -c 5234 java.lang.Thread.State: RUNNABLE # 5000+ 线程可运行 3000 java.lang.Thread.State: TIMED_WAITING ...

5.5 排查思路

┌──────────────────────────────────────────────────────┐ │ 1. OOM 类型:Unable to create new native thread │ │ ↓ │ │ 2. 进程线程数 / 系统线程数限制 │ │ ↓ │ │ 3. 每个线程栈大小:默认 1MB → 8000 线程 ≈ 8GB native │ │ ↓ │ │ 4. CPU 飙升原因:线程上下文切换太多 + 锁竞争 │ └──────────────────────────────────────────────────────┘

5.6 应急止血

# 方案 1: 临时调小线程栈大小(重启) java -Xss256k -jar>// ✅ 修复 1: 用线程池替代 new Thread() @Service public class DataImporter { @Autowired private ThreadPoolExecutor importExecutor; // 注入配置的线程池 public void parallelImport(List<DataFile> files) { List<CompletableFuture<Void>> futures = files.stream() .map(file -> CompletableFuture.runAsync( () -> importSingleFile(file), importExecutor)) .collect(Collectors.toList()); CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); } } // ✅ 修复 2: 线程池合理配置 @Bean public ThreadPoolExecutor importExecutor() { // ✅ CPU 密集型:核心数 + 1 int cpuCores = Runtime.getRuntime().availableProcessors(); return new ThreadPoolExecutor( cpuCores + 1, // 核心线程 cpuCores * 2, // 最大线程 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10000), // 有界队列 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 ); } // ✅ 修复 3: JVM 参数优化 // -Xss512k # 线程栈大小(默认 1MB,可降到 512KB) // -XX:+UseG1GC -XX:MaxGCPauseMillis=200

六、OOM 排查 Checklist(直接拿去做 Runbook)

OOM 类型排查命令关键指标常见原因
Java heap spacejmap -heapjmap -histo:liveOld Gen > 95%内存泄漏、大对象、缓存无界
GC overhead limit exceededjstat -gcutilGC 日志FGC 频率 > 1次/分钟朝生夕死、内存碎片
Metaspace / PermGenjcmd PID VM.metaspaceMC 接近 MCMX动态 Class 未清理、CGLib
Unable to create new native threadls /proc/PID/taskulimit -u线程数 > 5000线程泄漏、new Thread()
Direct buffer memoryjcmd PID VM.native_memoryDirectMemory > -XX:MaxDirectMemorySizeNIO DirectByteBuffer 泄漏
StackOverflowErrorjstack栈深度异常递归无终止条件

七、写在最后

OOM 问题没有银弹,但有套路:

  1. 看错误类型:6 大 OOM 类型对应不同的排查路径
  2. 抓现场:heap dump + thread dump + GC 日志(出问题前就配置好-XX:+HeapDumpOnOutOfMemoryError)
  3. 系统层验证:CPU / 线程数 / 内存,OOM 经常伴随 CPU 飙升(FGC 频繁 / 上下文切换大)
  4. 根治三板斧:限流止血、扩容应急、改代码根治
  5. 加监控告警:OOM 堆使用率、Metaspace、线程数,提前预警

把上面 4 个案例 + SOP 收藏到你的 Runbook,下次遇到 OOM 直接照抄作业即可。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 18:52:30

IntelliJ IDEA插件实战:一键生成Builder模式代码的全过程解析

写Builder代码这事&#xff0c;听起来简单&#xff0c;真做起来能烦死人。尤其是项目里实体类一多&#xff0c;字段二三十个起步&#xff0c;每个类都得配一个静态内部Builder&#xff0c;方法一个接一个写&#xff0c;命名稍有出入&#xff0c;调用方就得跟着改。后来我干脆抽…

作者头像 李华
网站建设 2026/10/22 11:37:45

MXNet图像数据生成:从目录结构到record文件的完整工程实践

1. 为什么“mxnet 图像训练数据生成”不是个简单配置问题&#xff0c;而是一整套数据工程闭环你打开MXNet文档&#xff0c;翻到mx.io.ImageRecordIter或gluon.data.vision.transforms那几页&#xff0c;照着示例改了两行路径、调了三个参数&#xff0c;跑起来报错&#xff1a;O…

作者头像 李华