CPU 高通常分为两类:CPU 使用率短暂飙升(通常由某次请求或 Full GC 引起)和 CPU 持续高位运行(代码效率低或死循环)。
调优步骤:
确认现象与隔离
使用 top 或 htop 确认是用户态CPU高(us,代码逻辑问题)还是内核态CPU高(sy,系统调用频繁、IO 中断多)。
确认是单核打满(可能锁竞争或单线程计算)还是多核均衡。
定位问题线程
Linux 命令:
bash
# 1. 找到 Java 进程 PID top -c # 2. 查看进程内线程 CPU 占用 top -Hp [PID] # 3. 将 CPU 最高的线程 ID 转换为十六进制(nid) printf "%x\n" [线程ID] # 4. 导出堆栈并 grep 该 nid jstack [PID] | grep -A 50 [十六进制nid]
Arthas(阿里开源工具):dashboard 查看实时线程,thread -n 5 直接查看最忙的5个线程堆栈。
常见原因及解决方案
业务代码死循环 / 频繁 GC
现象:us 高,堆栈显示业务线程集中在某段循环代码。
解决:修复代码逻辑,增加超时控制或 sleep 释放 CPU。
频繁的 Full GC
现象:us 和 sy 可能都高,jstat -gcutil [PID] 1000 查看 FGC 次数频繁。
解决:Dump 堆内存分析大对象,调整 JVM 参数(如增大年轻代、更换 G1 或 ZGC)。
线程上下文切换频繁
现象:sy 高,vmstat 1 查看 cs(context switch)数值极高。
解决:减少锁竞争(锁粗化、读写锁),减少线程数,避免过多的阻塞等待。
正则表达式 / 序列化
现象:堆栈显示 Pattern.matcher 或 JSON.toJSONString 占大头。
解决:预编译 Pattern;优化序列化框架(避免大对象转 JSON);使用更高效的序列化协议(ProtoBuf)。
消息未入库是一个典型的分布式系统数据丢失问题。排查需要沿着数据流链路:生产者 → Broker → 消费者 → 数据库 逐一排查。
消息队列:Kafka、RocketMQ、RabbitMQ、Pulsar。
数据同步中间件:Canal(监听 MySQL Binlog 转 MQ)、DataX、Flink CDC。
网关/代理:Nginx、Spring Cloud Gateway。
1. 确认生产端:消息是否发出?
检查生产者日志是否有报错(如 timeout、No such topic、Connection refused)。
确认事务是否提交:如果是事务消息,是否执行了 commit;如果是本地事务+消息,检查事务是否回滚。
排查手段:查看生产者监控(消息发送 QPS、发送失败率)。
2. 确认 Broker 端:消息是否存储?
Kafka:kafka-run-class.sh kafka.tools.GetOffsetShell 查看主题各分区 offset 是否增长。检查是否有磁盘写满、ISR 缩容导致无法写入。
RocketMQ:查看 broker 日志,确认 putMessage 是否成功。检查刷盘策略(SYNC_FLUSH vs ASYNC_FLUSH),如果是异步刷盘且 Broker 宕机,可能丢失未落盘数据。
RabbitMQ:检查队列是否有 unacked 消息堆积,确认消息是否未确认就丢包。
3. 确认消费端:消息是否被拉取?
检查消费者组 lag(积压量)。如果 lag > 0,说明 Broker 有数据,但消费者没处理。
常见原因:
消费代码异常:消费逻辑抛异常且没有 try-catch,导致消息一直重试,陷入死循环。
负载均衡问题:新增消费者导致 rebalance,频繁重平衡期间可能停止消费。
并发处理:多线程消费时,子线程失败未做回调,导致主线程确认成功但实际入库失败。
4. 确认入库端:数据是否真的没写入?
排查数据库(DB):
检查数据库死锁日志:SHOW ENGINE INNODB STATUS,是否存在死锁回滚。
检查 SQL 语法:是否存在字段截断、非空约束冲突导致插入静默失败(代码吞掉了异常)。
分布式事务:若涉及 2PC 或 TCC,检查事务协调者日志,是否存在二阶段提交失败。
5. 链路追踪
使用分布式追踪工具(SkyWalking、Pinpoint)查看完整 Trace。如果在 MQ 环节出现断链,说明消息未进入 Broker;如果在 Consumer 环节链路完整但 DB 操作缺失,说明消费逻辑失败。
OOM(Out Of Memory)在 Java 中主要有以下几种,每一种对应不同的堆栈信息和解决方案。
| 类型 | 异常信息 | 产生原因 | 常见场景 | 排查与解决 |
|---|---|---|---|---|
| 堆内存溢出 | java.lang.OutOfMemoryError: Java heap space |
对象分配率超过 GC 回收率,或存活对象无法被回收。 | 1. 内存泄漏(集合类未清空)。 2. 大并发下创建大量临时对象。 |
MAT/JProfiler 分析 Dump 文件,查看 Shallow Heap 和 Retained Heap,定位大对象引用链。 |
| 元空间溢出 | java.lang.OutOfMemoryError: Metaspace |
加载的类太多或类加载器未卸载。 | 1. 热部署(Tomcat 重新部署后老的类加载器未回收)。 2. 大量使用动态代理(CGLIB)、JSP 或 Groovy 脚本。 |
jstat -gc 查看 M 区使用率。增加 -XX:MaxMetaspaceSize 限制,排查是否存在类加载器泄露。 |
| GC 开销超限 | java.lang.OutOfMemoryError: GC overhead limit exceeded |
98% 的时间在 GC 且回收不到 2% 的堆内存。 | 这是“堆溢出”的前兆,系统濒临崩溃。 | 通常是内存分配不合理或代码存在严重泄露,需优化代码并调整 JVM 参数。 |
| 直接内存溢出 | java.lang.OutOfMemoryError: Direct buffer memory |
NIO 操作(Netty、ByteBuffer.allocateDirect)未释放。 | 1. Netty 网络通信未设置 -XX:MaxDirectMemorySize。2. 频繁创建大容量 DirectByteBuffer。 |
jcmd [PID] VM.native_memory 查看本地内存分配。检查是否在使用完 Buffer 后调用了 cleaner 或正确释放。 |
| 无法创建本地线程 | java.lang.OutOfMemoryError: Unable to create new native thread |
进程内线程数已达系统上限(或内存不足以创建新线程栈)。 | 1. 代码中无限制创建线程(线程池未复用)。 2. 操作系统 ulimit 限制过低。 |
ulimit -u 查看用户最大进程数。pstree 查看进程树。修复线程池使用方式。 |
| 交换区溢出 | java.lang.OutOfMemoryError: Requested array size exceeds VM limit |
尝试创建长度大于 JVM 限制(通常为 Integer.MAX_VALUE - 2)的数组。 |
业务逻辑错误,试图将超大集合转数组。 | 检查代码中 new byte[] 或 List.toArray() 的逻辑,限制输入大小。 |
缓存策略的调整需要根据数据一致性要求、访问模式、缓存类型(本地缓存如 Caffeine,分布式缓存如 Redis)来综合考量。
问题:大 Value(如超过 1MB)导致 Redis 网络拥塞或频繁序列化开销大。
策略:
拆分为 Hash:将一个大的 JSON 拆分为 Hash 结构,只更新变更的 field,减少网络传输。
压缩:开启 gzip/zstd 压缩(CPU 换带宽/内存)。
热 Key 拆分:针对高并发热 Key(如秒杀商品),将 Key 加随机后缀(product_1_0 到 product_1_9),分散到多个缓存节点。
问题:缓存雪崩(大量 Key 同一时间过期)或缓存击穿(热 Key 过期瞬间大流量打穿 DB)。
策略:
过期时间打散:在基础过期时间上增加随机偏移量(如 expire = 3600 + random(0, 600))。
物理永不过期 + 逻辑异步刷新:缓存不设 TTL,由后台定时任务异步刷新缓存。读取时若发现数据即将过期(剩余 TTL < 阈值),返回旧数据的同时触发异步线程更新。
互斥锁:针对热 Key 的 rebuild 过程加锁(setnx),只允许一个线程去查 DB 回填,其他线程等待或返回旧值。
问题:Redis 内存达到 maxmemory 后触发驱逐,导致性能抖动。
策略:
allkeys-lru:适用于混合读写,最近最少使用(通用推荐)。
volatile-lru:适用于设置了过期时间的 Key(保留永久 Key 不被驱逐)。
allkeys-random:如果有全量扫描或周期性大批量访问,LRU 可能性能差,随机反而好。
volatile-ttl:适用于希望优先淘汰即将过期的数据。
问题:单靠 Redis,网络 IO 依然是瓶颈。
策略:本地缓存(Caffeine) + 分布式缓存(Redis) + 数据库。
读流程:请求 → 本地缓存(命中返回) → 分布式缓存(命中回填本地) → 数据库(回填两级缓存)。
一致性:采用 Canal + Binlog 监听 DB 变更,发送 MQ 消息,广播通知所有应用节点删除本地缓存(保证最终一致性)。对于强一致性场景,写操作时直接删除本地缓存,读操作时加分布式锁读 DB 更新 Redis。
问题:缓存穿透(查询不存在的数据,每次都穿透到 DB)。
策略:
在 Redis 前置布隆过滤器(Redisson 内置或 Guava 本地),存储所有可能存在的 Key。
当请求进来,先过布隆过滤器,如果判断“一定不存在”,直接返回空;如果判断“可能存在”,才走缓存查询。这样可以拦截绝大部分恶意穿透流量。
在互联网行业处理这类问题,通常遵循以下原则:
CPU 高:先看 GC,再看线程堆栈,最后优化算法。
消息未入库:分环节隔离(生产、Broker、消费、入库),利用监控(Lag、日志、链路追踪)确定断点。
OOM:区分区域(堆外还是堆内),结合 Dump 分析和 Native Memory Tracking。
缓存:权衡取舍(一致性 vs 性能),通过多级缓存、过期策略打散、布隆过滤器来应对高并发场景下的三大灾难(穿透、击穿、雪崩)。