前言
本文档面向线上 java 应用运维与开发人员,提供内存溢出(oom)和 cpu 占用过高问题的定位与解决思路。内含多个实战案例,初级程序员可按步骤复现、定位并修复。
一、内存异常(oom)排查与解决
1.1 常见内存异常类型
| 类型 | 说明 | 常见原因 |
|---|---|---|
java.lang.outofmemoryerror: java heap space | 堆内存不足 | 对象过多、大对象、内存泄漏 |
java.lang.outofmemoryerror: gc overhead limit exceeded | gc 回收时间过长 | 堆几乎满,gc 频繁且效果差 |
java.lang.outofmemoryerror: metaspace | 元空间不足 | 类加载过多、动态代理类膨胀 |
java.lang.outofmemoryerror: direct buffer memory | 直接内存不足 | nio、netty 等使用堆外内存超限 |
java.lang.outofmemoryerror: unable to create new native thread | 无法创建线程 | 线程数过多、栈内存不足 |
1.2 启动前必须配置的 jvm 参数
# 堆内存溢出时自动 dump,便于事后分析 -xx:+heapdumponoutofmemoryerror -xx:heapdumppath=/path/to/dumps/ # 可选:限制 metaspace(避免无限增长) -xx:maxmetaspacesize=256m
1.3 堆内存溢出(heap space)排查步骤
step 1:拿到 heap dump 文件
若已配置
heapdumponoutofmemoryerror,oom 后会自动生成.hprof文件若未配置,可手动触发:
jmap -dump:live,format=b,file=heap.hprof <pid>
step 2:分析 heap dump
推荐工具:mat(eclipse memory analyzer)、jprofiler、visualvm
打开
.hprof文件查看 dominator tree / leak suspects,定位占用内存最大的对象
关注:大集合、缓存、未关闭的连接、长生命周期的引用
step 3:常见根因与对应处理
| 根因 | 处理思路 |
|---|---|
| 缓存未限制大小 | 使用带容量和淘汰策略的缓存(caffeine、guava cache 设置 maxsize) |
| 大集合无限增长 | 控制集合上限、定期清理、改为流式处理 |
| 大对象/大数组 | 分批处理、压缩、使用流式 api |
| 连接/资源未关闭 | 使用 try-with-resources、finally 关闭、连接池超时检测 |
| 内存泄漏(对象被长期引用) | 检查静态集合、监听器未移除、threadlocal 未清理 |
1.4 metaspace 溢出排查
# 查看 metaspace 使用情况 jstat -gc <pid>
常见原因:大量动态类生成(反射、代理、groovy 等)、类加载器泄漏
处理:限制
maxmetaspacesize、减少动态类生成、排查类加载器泄漏
1.5 direct buffer / 直接内存溢出
常见于 netty、nio、直接 bytebuffer
检查
-xx:maxdirectmemorysize配置排查:bytebuf、directbytebuffer 是否正确 release
1.6 无法创建线程(unable to create new native thread)
# 查看线程数 jstack <pid> | grep "java.lang.thread.state" | wc -l
常见原因:线程池配置过大、线程泄漏、每个线程栈过大
处理:合理配置线程池、减小
-xss、排查线程泄漏
二、cpu 过高排查与解决
2.1 快速定位占用 cpu 高的线程
step 1:找到高 cpu 的 java 进程
# linux top -c # 或 ps -eo pid,pcpu,pmem,cmd --sort=-pcpu | head -20
step 2:找到进程内的高 cpu 线程
# 查看进程内各线程 cpu top -hp <pid> # 或 ps -elo pid,tid,pcpu,comm | grep <pid>
step 3:将线程 id 转为十六进制
# 假设 tid=12345 printf "%x\n" 12345 # 输出:3039
step 4:抓取线程栈并过滤
jstack <pid> > jstack.txt # 在 jstack.txt 中搜索 nid=0x3039(十六进制)
step 5:多次采样确认
# 连续抓 3 次,间隔 2 秒,对比是否同一处代码 for i in 1 2 3; do jstack <pid> >> jstack_$i.txt; sleep 2; done
2.2 常见 cpu 过高根因
| 根因 | 表现 | 处理思路 |
|---|---|---|
| 死循环 / 无限递归 | 栈中同一方法反复出现 | 修复逻辑、加退出条件、限制递归深度 |
| 正则回溯 | 栈中有 java.util.regex | 简化正则、避免灾难性回溯 |
| 大对象 / json 序列化 | 栈中有 jackson、fastjson 等 | 优化数据结构、减少序列化体积 |
| gc 过于频繁 | 栈中有 gc 相关线程 | 调大堆、优化 gc 参数、减少短生命周期对象 |
| 加密 / 计算密集 | 栈中有加密、压缩、复杂计算 | 加缓存、异步、降频、换算法 |
| 大量线程竞争 | 栈中有 park、wait、锁 | 减少锁粒度、优化并发设计 |
2.3 使用 arthas 快速排查(推荐)
# 启动 arthas java -jar arthas-boot.jar # 查看最忙的线程 thread # 查看指定线程栈 thread <id> # 查看 cpu 使用率 top n 线程 thread -n 5
三、实战案例:从现象到修复
以下案例均以「现象 → 排查步骤 → 根因分析 → 修复方案」的形式呈现,方便初级程序员按图索骥。
案例 1:hashmap 缓存无限增长导致 oom
现象
java.lang.outofmemoryerror: java heap space
at java.util.hashmap.resize(hashmap.java:...)
at com.example.orderservice.cacheorder(orderservice.java:42)问题代码(简化)
// 错误:使用静态 hashmap 做缓存,没有大小限制,也没有过期
public class orderservice {
private static final map<string, order> cache = new hashmap<>();
public order getorder(string orderid) {
if (cache.containskey(orderid)) {
return cache.get(orderid);
}
order order = queryfromdb(orderid);
cache.put(orderid, order); // 无限往里塞,迟早 oom
return order;
}
}排查步骤
拿到 oom 时的 heap dump:
jmap -dump:live,format=b,file=heap.hprof <pid>用 mat 打开,点击 leak suspects 或 dominator tree
找到占用最大的对象,例如:
hashmap$node[]或hashmap,查看其被谁引用通常会看到
orderservice.cache引用了大量order对象
修复方案
// 使用 caffeine 或 guava cache,限制大小和过期时间
public class orderservice {
private static final cache<string, order> cache = caffeine.newbuilder()
.maximumsize(10_000) // 最多 1 万条
.expireafterwrite(30, timeunit.minutes)
.build();
public order getorder(string orderid) {
return cache.get(orderid, this::queryfromdb);
}
}
// maven: com.github.ben-manes.caffeine:caffeine案例 2:一次性加载大 list 导致 oom
现象
java.lang.outofmemoryerror: java heap space
at java.util.arrays.copyof(arrays.java:...)
at com.example.reportservice.exportall(reportservice.java:28)问题代码(简化)
// 错误:一次性从 db 查出百万条记录到内存
public void exportall() {
list<order> all = ordermapper.selectall(); // 假设 100 万条
for (order o : all) {
writetoexcel(o);
}
}排查步骤
用 mat 打开 heap dump,dominator tree 中会看到巨大的
arraylist或object[]查看其元素类型,往往是业务实体(如
order)在代码中搜索
selectall、findall等全量查询方法
修复方案
// 改为分页或流式查询
public void exportall() {
int pagesize = 1000;
int offset = 0;
list<order> batch;
while (!(batch = ordermapper.selectbypage(offset, pagesize)).isempty()) {
for (order o : batch) {
writetoexcel(o);
}
offset += pagesize;
}
}
// 或使用 mybatis 流式查询:@select 配合 resulthandler案例 3:死循环导致 cpu 100%
现象
监控显示某 java 进程 cpu 持续 90%+,甚至 100%
接口响应变慢或超时
问题代码(简化)
// 错误:某个条件永远不满足,导致死循环
public void processqueue() {
while (true) {
task task = queue.poll();
if (task == null) {
// 忘记 break 或 sleep,空转死循环!
continue;
}
dotask(task);
}
}排查步骤(按顺序执行)
# 1. 找到高 cpu 进程 top -c # 记下 pid,例如 12345 # 2. 看该进程内哪些线程 cpu 高 top -hp 12345 # 记下 tid,例如 12346 # 3. 转为十六进制 # linux/mac: printf "%x\n" 12346 # windows powershell: [convert]::tostring(12346, 16) # 输出:303a # 4. 抓线程栈 jstack 12345 > jstack.txt # 5. 在 jstack.txt 中搜索 nid=0x303a
你会看到的栈(示例)
"http-nio-8080-exec-1" #32 daemon prio=5 ...
java.lang.thread.state: runnable
at com.example.queueprocessor.processqueue(queueprocessor.java:18)
at com.example.queueprocessor$$lambda$1.run(...)说明:processqueue 第 18 行附近在疯狂运行。
修复方案
// 队列为空时应等待,而不是空转
public void processqueue() {
while (true) {
task task = queue.poll();
if (task == null) {
locksupport.parknanos(timeunit.milliseconds.tonanos(100));
continue;
}
dotask(task);
}
}案例 4:正则灾难性回溯导致 cpu 飙升
现象
某个包含用户输入的接口被调用后,cpu 突然飙高
接口长时间不返回
问题代码(简化)
// 危险正则:嵌套量词 + 回溯,可能指数级耗时
public boolean validateemail(string input) {
// 这个正则在恶意输入下会灾难性回溯
return input.matches("([a-z]+\\.)*[a-z]+@[a-z]+\\.com");
}
// 恶意输入示例:"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"排查步骤
用
top -hp+jstack定位高 cpu 线程栈中会看到类似:
java.lang.thread.state: runnable
at java.util.regex.pattern$loop.match(pattern.java:...)
at java.util.regex.pattern$grouptail.match(pattern.java:...)
at java.util.regex.pattern$branchconn.match(pattern.java:...)
...说明:卡在正则匹配上。
修复方案
// 简化正则,避免嵌套量词;或对输入长度做限制
public boolean validateemail(string input) {
if (input == null || input.length() > 100) return false;
return input.matches("[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}");
}案例 5:线程池任务里又有死循环
现象
线程池满,新任务一直排队
cpu 高,但
jstack里很多线程状态是runnable,且栈顶都在同一业务方法
问题代码(简化)
@scheduled(fixedrate = 1000)
public void scheduledtask() {
executor.submit(() -> {
for (int i = 0; i < list.size(); i++) {
// 某次循环里 list 被清空,size 变成 0,但 i 已经很大...
// 或者条件写错,i 永远不会满足退出条件
process(list.get(i));
}
});
}排查步骤
jstack <pid>抓栈搜索业务类名(如
scheduledtask、process)看多根线程是否都卡在同一行,且状态为
runnable
修复思路
检查循环的退出条件是否正确
避免在循环内修改
list(如并发清空)加日志或断点确认
i和list.size()的变化
案例 6:arthas 一键定位 cpu 问题(推荐)
当你不熟悉 top、jstack 时,可用 arthas 快速定位。
步骤
# 1. 下载并启动 arthas curl -o https://arthas.aliyun.com/arthas-boot.jar java -jar arthas-boot.jar # 2. 选择要 attach 的 java 进程(按数字回车) # 3. 看 cpu 最高的线程 thread -n 3 # 输出示例: # threads total: 45, new: 0, runnable: 10, blocked: 0, waiting: 30, timed_waiting: 5 # "http-nio-8080-exec-2" id=32 cpuusage=87.23% # at com.example.badservice.heavycompute(badservice.java:66) # ...
直接看到 badservice.java:66 在吃 cpu,去查该行代码即可。
案例速查表
| 现象 | 优先怀疑 | 快速验证 |
|---|---|---|
| oom: heap space | 大集合、无界缓存 | mat 看 dominator tree 最大的对象 |
| oom: metaspace | 动态类过多 | jstat -gc 看 mc/mu |
| cpu 100% | 死循环、正则、大计算 | jstack 或 arthas thread -n 5 |
| 接口卡住 | 死锁、阻塞 io | jstack 看 blocked / waiting |
四、常用命令速查
| 场景 | 命令 |
|---|---|
| 查看 jvm 进程 | jps -l 或 ps -ef \| grep java |
| 堆内存概览 | jmap -heap <pid> |
| 生成 heap dump | jmap -dump:live,format=b,file=heap.hprof <pid> |
| 线程栈 | jstack <pid> > jstack.txt |
| gc 统计 | jstat -gc <pid> 1000 10 |
| 查看 jvm 参数 | jinfo -flags <pid> |
五、预防建议
上线前:配置
heapdumponoutofmemoryerror、合理设置堆和 metaspace监控:接入 jvm 监控(堆、gc、线程数、cpu)
压测:做压测和长时间稳定性测试,提前暴露问题
限流与熔断:防止突发流量导致 oom 或 cpu 打满
代码:避免大对象、注意资源释放、合理使用缓存
文档更新:可根据实际环境补充具体参数和工具版本。
到此这篇关于java内存异常与cpu过高线上排查完整步骤的文章就介绍到这了,更多相关java内存异常与cpu过高内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论