网站首页 > java教程 正文
1 问题现象
使用docker stats发现机器上的有一个容器占用的cpu特别高,接近400%
但系统几乎没负荷,只有一两个设备和用户。据同事说,这个现象持续很久了,从他接手项目就是这样。
到现在至少有一年多了,一直没人知道是什么原因,只知道是历史遗留问题。
正好周末我有空,就花了点时间研究一下。
2 使用arthas分析cpu占用情况
1 下载arthas
分析java的cpu使用问题,必须使用arthas。arthas可以到github下载:https://github.com/alibaba/arthas/releases
2 进入容器分析
先将下载下来的arthas-bin.zip解压,然后拷贝到容器里面
docker cp arthas-boot.jar f2dfdf703594:/arthas-boot.jar
docker exec -it f2dfdf703594 /bin/bash # 这里的f2dfdf703594就是前面cpu使用率过高的容器名称
cd /
3 切换到java进程所在用户启动arthas
容器里面的java进程是使用xxxxx_user用户启动的,如果直接使用arthas访问会报错
INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 1 iot-xxxxx.jar
[2]: 91 -- main class information unavailable
1
[INFO] arthas home: /root/.arthas/lib/4.0.5/arthas
[INFO] Try to attach process 1
Picked up JAVA_TOOL_OPTIONS:
com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file: target process not responding or HotSpot VM not loaded
at sun.tools.attach.LinuxVirtualMachine.<init>(LinuxVirtualMachine.java:106)
at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:78)
at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:250)
at com.taobao.arthas.core.Arthas.attachAgent(Arthas.java:102)
at com.taobao.arthas.core.Arthas.<init>(Arthas.java:27)
at com.taobao.arthas.core.Arthas.main(Arthas.java:161)
[ERROR] Start arthas failed, exception stack trace:
[ERROR] attach fail, targetPid: 1
需要先切换为xxxxx_user再访问
usermod -s /bin/bash xxxxx_user # 允许xxxxx_user用户登录
su xxxxx_user # 切换xxxxx_user
whoami # 确认切换成功
cd / && java -jar arthas-boot.jar
4 分析占用情况
运行arthas,选择目标进程为iot-xxxxx.jar
执行thread -n 5,看看cpu占用排行前5的线程堆栈情况
输出如下:
bash-4.4$ cd / && java -jar arthas-boot.jar
[INFO] JAVA_HOME: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.312.b07-2.el8_5.x86_64/jre
[INFO] arthas-boot version: 4.0.5
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 1 iot-xxxxx.jar
[2]: 91 -- main class information unavailable
1
[INFO] arthas home: /home/xxxxx_user/.arthas/lib/4.0.5/arthas
[INFO] The target process already listen port 3658, skip attach.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://arthas.aliyun.com/doc
tutorials https://arthas.aliyun.com/doc/arthas-tutorials.html
version 4.0.5
main_class iot-xxxxx.jar
pid 1
start_time 2025-06-13 19:33:44.764
currnt_time 2025-06-15 10:18:11.722
[arthas@1]$ thread -n 5
"Disruptor_Thread-2" Id=125 cpuUsage=98.32% deltaTime=199ms time=142858586ms RUNNABLE
at java.lang.Thread.yield(Native Method)
at com.lmax.disruptor.YieldingWaitStrategy.applyWaitMethod(YieldingWaitStrategy.java:58)
at com.lmax.disruptor.YieldingWaitStrategy.waitFor(YieldingWaitStrategy.java:40)
at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56)
at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159)
at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)
at java.lang.Thread.run(Thread.java:748)
"Disruptor_Thread-3" Id=126 cpuUsage=97.3% deltaTime=197ms time=142859828ms RUNNABLE
at java.lang.Thread.yield(Native Method)
at com.lmax.disruptor.YieldingWaitStrategy.applyWaitMethod(YieldingWaitStrategy.java:58)
at com.lmax.disruptor.YieldingWaitStrategy.waitFor(YieldingWaitStrategy.java:40)
at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56)
at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159)
at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)
at java.lang.Thread.run(Thread.java:748)
"Disruptor_Thread-1" Id=124 cpuUsage=96.94% deltaTime=196ms time=142868416ms RUNNABLE
at java.lang.Thread.yield(Native Method)
at com.lmax.disruptor.YieldingWaitStrategy.applyWaitMethod(YieldingWaitStrategy.java:58)
at com.lmax.disruptor.YieldingWaitStrategy.waitFor(YieldingWaitStrategy.java:40)
at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56)
at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159)
at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)
at java.lang.Thread.run(Thread.java:748)
"Disruptor_Thread-0" Id=123 cpuUsage=94.72% deltaTime=192ms time=142871747ms RUNNABLE
at java.lang.Thread.yield(Native Method)
at com.lmax.disruptor.YieldingWaitStrategy.applyWaitMethod(YieldingWaitStrategy.java:58)
at com.lmax.disruptor.YieldingWaitStrategy.waitFor(YieldingWaitStrategy.java:40)
at com.lmax.disruptor.ProcessingSequenceBarrier.waitFor(ProcessingSequenceBarrier.java:56)
at com.lmax.disruptor.BatchEventProcessor.processEvents(BatchEventProcessor.java:159)
at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:125)
at java.lang.Thread.run(Thread.java:748)
可以看到占用cpu靠前的进程都是disruptor框架的线程,disruptor这个框架我不熟,只能丢给豆包分析一下是否正常
根据豆包的分析,我怀疑是我们项目使用的策略问题。检查相关代码,发现使用的是YieldingWaitStrategy
// 阻塞策略
//BlockingWaitStrategy、SleepingWaitStrategy、YieldingWaitStrategy,YieldingWaitStrategy 的性能是最好的,适合用于低延迟的系统。在要求极高性能且事件处理线数小于 CPU 逻辑核心数的场景中,推荐使用此策略;例如,CPU开启超线程的特性。
WaitStrategy strategy = null;
int processors = Runtime.getRuntime().availableProcessors();
// 4是指4个handle
if (processors < 4 || processors < (4 * 2)) {
strategy = new BlockingWaitStrategy();
} else {
strategy = new YieldingWaitStrategy();
}
让豆包给我整理一下,有没有更好的策略
策略名称 | 核心机制 | CPU 消耗 | 最低延迟 | 适用场景 |
BlockingWaitStrategy | 使用 ReentrantLock+Condition 实现线程阻塞 / 唤醒 | 低 | 毫秒级 | 资源受限、对延迟不敏感的系统 |
SleepingWaitStrategy | 多级策略:自旋→yield→固定时间 sleep(默认 100 纳秒) | 中低 | 微秒级 | 一般业务系统(平衡延迟和 CPU) |
YieldingWaitStrategy | 自旋 + Thread.yield () | 高 | 纳秒级 | 低延迟、CPU 资源充足的场景 |
BusySpinWaitStrategy | 纯忙等待(死循环) | 极高 | 纳秒级 | 对延迟要求极致且有专用 CPU 核心的场景 |
PhasedBackoffWaitStrategy | 自适应策略:自旋→yield→指数退避 sleep→最终可回退到 Blocking 策略 | 动态调整 | 纳秒→毫秒 | 负载波动大的复杂系统 |
TimeoutBlockingWaitStrategy | 带超时的 Blocking 策略(防止永久阻塞) | 低 | 毫秒级 | 需要超时控制的场景 |
发现PhasedBackoffWaitStrategy可能更适合我们系统,问豆包如果用它代替,效果为什么样,答案是这样的
指标 | YieldingWaitStrategy | PhasedBackoffWaitStrategy |
平均 CPU 使用率 (%) | ~95 | ~30-60(取决于配置) |
平均延迟 (μs) | ~0.2 | ~0.5-1(初始阶段接近) |
99.9% 分位延迟 (μs) | ~1 | ~2-5(需优化配置) |
吞吐量 (TPS) | 高 | 接近(可能略有下降) |
3 结论
当cpu核数 < 8时,系统启用了超低延时的策略,导致系统没有负载时,cpu也持续高使用率。
应该可以考虑使用更适配的策略如PhasedBackoffWaitStrategy,或者使用参数较合理的BlockingWaitStrategy
4 arthas其他功能
arthas是分析老项目性能问题的利器,除了前面介绍的查看cpu占用高的线程堆栈外,arthas还有其它好用的功能,可以到在线教程查看
https://arthas.aliyun.com/doc/
我这里贴几个豆包ai介绍的常用功能
基础监控功能
1. dashboard - 系统概览
功能
实时展示 JVM 进程的整体状态,包括线程、内存、GC、类加载等信息。
示例输出
ID NAME GROUP PRIORITY STATE %CPU TIME INTERRUPTED DAEMON
1 main main 5 RUNNABLE 0.0 0:00:00 false false
2 Reference Handler system 10 WAITING 0.0 0:00:00 false true
...
常用参数
● -i 2000:指定刷新间隔(毫秒)。
2. thread - 线程分析
功能
查看线程详情,定位阻塞、死锁或高 CPU 线程。
常用命令
thread -n 3 # 查看 CPU 使用率最高的 3 个线程
thread -b # 找出阻塞其他线程的线程
thread <threadId> # 查看指定线程的堆栈
示例
"http-nio-8080-exec-1" Id=12 BLOCKED on java.util.concurrent.locks.ReentrantLock$NonfairSync@7c53a9eb
3. jvm - JVM 信息
功能
查看 JVM 运行时信息,如启动参数、系统属性、内存区域等。
用分析
1. watch - 方法监控
功能
监听方法的入参、返回值、异常及执行耗时。
示例
watch com.example.Service methodName '{params, returnObj, throwExp}' -x 2 -n 50
参数说明
● -x 2:指定输出对象的深度。
● -n 50:只监控 50 次调用。
2. trace - 方法调用链路分析
功能
追踪方法内部调用路径,计算每个子调用的耗时和次数。
示例
trace com.example.Service methodName
输出示例
[Trace] ---
`---[0.013ms] com.example.Service:methodName()
+---[0.002ms] com.example.Dao:query()
+---[0.005ms] com.example.Validator:validate()
`---[0.006ms] com.example.Cache:update()
3. stack - 方法调用栈
功能
记录方法被调用的调用路径。
示例
stack com.example.Service methodName
4. tt - 时光回溯(TimeTunnel)
功能
记录方法调用的所有信息,并可以进行回放。
示例
tt -t com.example.Service methodName # 记录方法调用
tt -i 1000 -p # 回放第 1000 次调用
类与字节码操作
1. sc - 类查找
功能
查找 JVM 中已加载的类。
示例
sc -d *Service # 查找所有 Service 结尾的类
sc -d com.example.UserService # 查看类的详细信息
2. sm - 方法查找
功能
查找类中的方法。
示例
sm com.example.UserService * # 查看 UserService 类的所有方法
3. jad - 反编译
功能
反编译已加载的类,查看运行时的代码。
示例
jad com.example.UserService
4. redefine - 热更新类
功能
在不重启 JVM 的情况下更新类的字节码。
示例
# 1. 反编译获取源码
jad com.example.UserService > /tmp/UserService.java
# 2. 修改代码并编译
javac -cp .:/path/to/your/classes /tmp/UserService.java
# 3. 重新定义类
redefine /tmp/UserService.class
JVM 参数与状态
1. vmoption - JVM 参数调整
功能
查看和修改 JVM 参数。
示例
vmoption PrintGCDetails true # 开启 GC 详细日志
vmoption -l # 列出所有 JVM 参数
2. heapdump - 堆转储
功能
生成当前堆内存的快照(类似 jmap -dump)。
示例
heapdump /tmp/dump.hprof
3. perfcounter - 性能计数器
功能
查看 JVM 内部的性能计数器信息。
示例
perfcounter
日志功能
1. logger - 日志级别调整
功能
动态调整日志框架的日志级别。
示例
logger -l # 查看所有 logger 及其级别
logger --name root --level debug # 将 root logger 级别设为 DEBUG
2. 结合 watch 监控日志
功能
监控方法调用时的日志输出。
示例
watch com.example.Service processOrder '{params[0], returnObj, #cost}' -x 2
高级功能
1. profiler - 性能分析
功能
基于 Async-profiler 生成 CPU 火焰图或内存分配火焰图。
示例
profiler start # 开始采集 CPU 火焰图
profiler stop # 停止采集并生成报告
profiler start --event alloc # 采集内存分配火焰图
2. ognl - 执行表达式
功能
执行 OGNL 表达式,动态获取或修改对象状态。
示例
ognl '@System@getProperty("java.home")' # 获取系统属性
ognl '@com.example.Cache@instance.clear()' # 清空缓存
3. mc & retransform - 内存编译与加载
功能
动态编译 Java 代码并加载到 JVM 中。
示例
# 1. 编写 Java 代码到 /tmp/Test.java
# 2. 内存编译
mc /tmp/Test.java -d /tmp
# 3. 加载类
retransform /tmp/com/example/Test.class
常用组合技巧
1. 定位高 CPU 问题
步骤
1. 查看 CPU 使用率最高的线程
thread -n 3
2. 获取线程 ID 对应的堆栈
thread <threadId>
3. 使用 profiler 生成火焰图
profiler start
# 等待一段时间后
profiler stop
2. 排查方法性能问题
步骤
1. 监控方法执行耗时
watch com.example.Service methodName '{params, returnObj, cost}' -x 2
2. 分析方法内部调用链路
trace com.example.Service methodName
3. 查看方法调用路径
stack com.example.Service methodName
注意事项
1. 性能影响
部分功能(如 trace、profiler)可能对性能有一定影响,建议在低峰期使用。
2. 权限要求
需要有足够的权限访问目标 JVM 进程。
3. 版本兼容性
确保 Arthas 版本与目标 JVM 兼容。
猜你喜欢
- 2025-07-10 SpringBoot扩展——定时任务!(springboot定时任务实现的几种方式)
- 2025-07-10 面试官:可重复读隔离级别实现原理是什么?(一文搞懂MVCC机制)
- 2025-07-10 开发必看!Spring Boot3 如何无缝整合 SkyWalking 实现高效性能监测
- 2025-07-10 Spring Boot3 中可整合的最新内容汇总
- 2025-07-10 java日志大全-第4篇:Logback(java 日志系统)
- 2025-07-10 每天一个 Python 库:logging 用法精讲,高效简洁的输出日志
- 2025-07-10 C#.NET log4net 详解(c#.net教程)
- 2025-07-10 java内存分析利器,mat与arthas哪个更强?
- 2025-07-10 Spring Boot3 学习提升相关知识点汇总
- 2025-07-10 Java日志埋点实战:3个技巧让Bug无所遁形
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)