Java 性能调优:CPU、内存、锁与 IO 排查
本文从性能调优方法论出发,覆盖 CPU/内存/锁/IO 四类性能问题的分析手段、JVM 调优参数、常用 Profiling 工具以及常见性能陷阱,帮助你在遇到线上性能问题时有章可循。
目录
| 章节 | 说明 |
|---|---|
| 性能调优方法论 | 指标体系、测试策略、三步走流程 |
| CPU 性能问题 | 上下文切换、死循环、正则回溯 |
| 内存性能问题 | 内存泄漏排查、堆结构、GC 调优 |
| 锁与并发优化 | Synchronized 锁升级、Lock 锁分离、乐观锁 |
| 线程池调优 | 线程数计算公式、参数含义 |
| JVM 调优 | 内存模型、GC 算法选型、常用参数 |
| 性能分析工具 | JDK 内置工具、压测工具 |
| 常见性能陷阱 | 代码层面的高频问题 |
性能调优方法论
性能指标体系
| 指标 | 说明 | 典型目标 |
|---|---|---|
| 响应时间 | 接口端到端耗时(ms 级) | P99 < 200ms |
| 吞吐量 TPS | 每秒处理事务数 | 越高越好 |
| CPU 使用率 | 系统/用户态 CPU 占比 | < 70% |
| 内存使用率 | 堆/非堆内存占比 | < 80% |
| GC 吞吐量 | 应用时间 / 总运行时间 | > 95% |
| 负载承受能力 | 压测时响应时间随并发数的曲线 | 平缓上升 |
调优三步走
测试(发现瓶颈)
↓
分析(定位根因):自下而上 OS → JVM → Application
↓
调优(解决问题):自上而下 Application → JVM → OS
测试策略
微基准测试:精准定位某个方法/模块在不同实现下的性能差异,适合对比两种方案。
宏基准测试:综合模拟线上环境,需要考虑:
- 测试环境与线上一致
- 并行业务接口的干扰
- 目标:吞吐量达标 + 探底 TPS + 观察资源使用率变化
注意热身问题:JVM JIT 编译需要预热,第一批请求的响应时间偏高,测试结果应取稳定阶段的均值。
兜底策略
- 限流:基于压测探底的 TPS 设置入口限流阈值 + 熔断
- 横向扩容:当超过阈值时自动扩容(Kubernetes HPA)
- 提前扩容:秒杀等瞬时高并发场景在活动前手动扩容
CPU 性能问题
常见 CPU 高耗场景
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 代码死循环 | 递归未收敛、逻辑 Bug | 代码 Review + jstack 找线程 |
| 正则回溯 | 复杂正则在特定输入下指数级回溯 | 简化正则、限制输入长度 |
| JVM Full GC 频繁 | 老年代空间不足 | 调大堆/优化对象生命周期 |
| 大量上下文切换 | 线程数过多、锁竞争激烈 | 减少线程数、优化锁粒度 |
| 异常频繁抛出 | 构建异常栈消耗大 | 避免用异常控制流程 |
上下文切换排查
# 查看进程级上下文切换
vmstat 1 5
# cs 列:每秒上下文切换次数,正常应 < 5000
# 查看线程级切换
pidstat -w -p <pid> 1 3
# 查看线程堆栈(找 BLOCKED/WAITING 线程)
jstack <pid> | grep -A 5 "BLOCKED\|WAITING"
内存性能问题
JVM 内存模型
JVM 内存
├── 堆(Heap)—— 线程共享
│ ├── 新生代
│ │ ├── Eden 区
│ │ ├── From Survivor (S0)
│ │ └── To Survivor (S1)
│ └── 老年代
├── 方法区(元空间,Java 8+,本地内存)
├── 程序计数器 —— 线程私有,记录字节码执行位置
├── 虚拟机栈 —— 线程私有,存储栈帧(局部变量、操作数栈)
└── 本地方法栈 —— 线程私有,C 语言 native 方法
Java 8 变化:永久代(PermGen)被元空间(Metaspace)取代,元空间使用本地内存,避免了
OutOfMemoryError: PermGen。
内存泄漏排查流程
# 1. 查看进程内存使用
top -p <pid>
# 2. 查看堆内存使用(老年代持续增长是泄漏信号)
jmap -heap <pid>
# 3. 查看存活对象分布
jmap -histo:live <pid> | head -30
# 4. Dump 堆文件
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
# 5. 用 MAT 工具分析 dump 文件,找 Retained Heap 最大的对象
启动参数(OOM 时自动 dump):
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heapdump.hprof \
-jar app.jar
常见内存泄漏场景
// 典型泄漏:ThreadLocal 使用后未 remove
// 线程池中的线程生命周期很长,ThreadLocal 的 value 无法被 GC
@RequestMapping("/api")
public String handle() {
ThreadLocal<byte[]> local = new ThreadLocal<>();
local.set(new byte[4096 * 1024]); // 4MB
// 忘记 local.remove(),线程归还线程池后 value 仍然存在
return "ok";
}
// 正确写法
ThreadLocal<byte[]> LOCAL = new ThreadLocal<>();
try {
LOCAL.set(new byte[4096 * 1024]);
// ... 业务逻辑
} finally {
LOCAL.remove(); // 必须在 finally 中 remove
}
锁与并发优化
Synchronized 锁升级机制
JDK 1.6 引入分级锁,锁状态单向升级:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
| 锁状态 | 适用场景 | 原理 |
|---|---|---|
| 偏向锁 | 单线程反复获取同一锁 | Mark Word 记录线程 ID,无需 CAS |
| 轻量级锁 | 线程交替执行(无激烈竞争) | CAS 替换 Mark Word |
| 重量级锁 | 多线程激烈竞争 | OS Mutex,涉及用户态/内核态切换 |
# 高并发下偏向锁撤销反而有开销,可关闭
-XX:-UseBiasedLocking
# 关闭自旋锁(锁竞争激烈、持有时间长时)
-XX:-UseSpinning
锁消除与锁粗化(JIT 自动优化):
- 锁消除:逃逸分析发现锁对象不会被其他线程访问,直接消除锁
- 锁粗化:相邻的同步块使用同一锁,合并为一个大同步块
减小锁粒度:将一个大锁对象拆分为多个小对象(如 ConcurrentHashMap 的分段锁思想)。
Lock 同步锁
import java.util.concurrent.locks.*;
// ReentrantReadWriteLock:读多写少场景
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
public double read() {
readLock.lock();
try {
return Math.sqrt(x * x + y * y);
} finally {
readLock.unlock(); // 必须在 finally 中释放
}
}
public void write(double dx, double dy) {
writeLock.lock();
try {
x += dx;
y += dy;
} finally {
writeLock.unlock();
}
}
StampedLock(Java 8+):三种模式,读多写少场景性能优于 RRW:
StampedLock sl = new StampedLock();
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead(); // 乐观读,不加锁
double x = this.x, y = this.y;
if (!sl.validate(stamp)) { // 检查期间是否有写操作
stamp = sl.readLock(); // 升级为悲观读
try {
x = this.x;
y = this.y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(x * x + y * y);
}
StampedLock 的局限:不支持重入,不支持条件变量,调用
lock()后若线程被中断会导致 CPU 100%(需用lockInterruptibly()),因此未被广泛替代 RRW。
乐观锁(CAS)
import java.util.concurrent.atomic.*;
// AtomicInteger:单变量 CAS 操作
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(1, 2); // CAS:期望值=1 时更新为 2
// LongAdder(Java 8+):高并发写场景性能更好
// 原理:分散到 cell[] 数组,减少 CAS 竞争,最终汇总
LongAdder adder = new LongAdder();
adder.increment();
long sum = adder.sum(); // 近似值,但 longValue() 返回准确值
| 场景 | 推荐方案 |
|---|---|
| 读 >> 写 | StampedLock 乐观读 |
| 读 > 写 | ReentrantReadWriteLock |
| 读写相当 | ReentrantLock 或 Synchronized |
| 写 >> 读(单变量计数) | LongAdder |
| 写 >> 读(需精确值) | AtomicLong |
线程池调优
ThreadPoolExecutor 参数
new ThreadPoolExecutor(
int corePoolSize, // 核心线程数(常驻)
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit,
BlockingQueue<Runnable> workQueue, // 任务队列(建议有界)
ThreadFactory threadFactory,
RejectedExecutionHandler handler // 拒绝策略
);
不推荐 直接使用
Executors工厂方法:newFixedThreadPool和newSingleThreadExecutor使用无界队列,newCachedThreadPool最大线程数为Integer.MAX_VALUE,均可能 OOM。
线程数计算公式
| 任务类型 | 公式 | 说明 |
|---|---|---|
| CPU 密集型 | N + 1(N = CPU 核数) | +1 防止偶发缺页中断导致 CPU 空闲 |
| IO 密集型 | 2N | IO 等待期间 CPU 空闲,可多开线程 |
| 混合型 | N × (1 + WT/ST) | WT=线程等待时间,ST=线程运行时间 |
公式给出初始值,最终需要通过实际压测验证,逐步调整到最优值。
JVM 调优
GC 算法对比
| 收集器 | 特点 | 适用场景 |
|---|---|---|
| Serial | 单线程,Stop-the-World | 单核/客户端 |
| Parallel Scavenge | 多线程,吞吐量优先 | 批处理,JDK 8 默认 |
| CMS | 并发标记,低停顿 | 响应时间敏感(已废弃) |
| G1 | 分 Region,可预测停顿 | JDK 9+ 默认,大堆 |
| ZGC | 停顿 < 10ms | JDK 15+,超大堆 |
| Shenandoah | 停顿极低 | JDK 12+,OpenJDK |
GC 调优策略
降低 Minor GC 频率:增大 Eden 区,减少短生命周期对象晋升老年代。
降低 Full GC 频率:
- 避免创建大对象(超过
-XX:PretenureSizeThreshold直接进老年代) - 增大堆内存,设置
-Xms=-Xmx避免动态扩缩堆
常用 JVM 参数
# 堆内存
-Xms4g -Xmx4g # 初始堆 = 最大堆(避免动态扩缩)
-Xmn1g # 新生代大小
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1
# GC 选择
-XX:+UseG1GC # 使用 G1(JDK 9+ 默认)
-XX:MaxGCPauseMillis=200 # G1 目标停顿时间
# GC 日志
-XX:+PrintGCDateStamps \
-XX:+PrintGCDetails \
-Xloggc:/var/log/gc.log
# OOM 时 dump
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heapdump.hprof
# 锁相关
-XX:-UseBiasedLocking # 关闭偏向锁(高并发竞争激烈时)
GC 性能衡量
# 查看堆内存使用(含 GC 次数和耗时)
jstat -gc <pid> 1000 10 # 每 1 秒采样,共 10 次
# 关键指标
# YGC/YGCT:Minor GC 次数/耗时
# FGC/FGCT:Full GC 次数/耗时
# GCT:GC 总耗时
# 吞吐量 = (运行时间 - GCT) / 运行时间,目标 > 95%
性能分析工具
JDK 内置工具
| 工具 | 用途 | 常用命令 |
|---|---|---|
jstat | 监控 JVM 统计信息(GC、类加载) | jstat -gc <pid> 1000 |
jstack | 线程堆栈快照,排查死锁/阻塞 | jstack <pid> |
jmap | 堆内存信息、对象分布、dump | jmap -histo:live <pid> |
jps | 列出 JVM 进程 | jps -l |
jinfo | 查看/修改 JVM 参数 | jinfo -flags <pid> |
jcmd | 综合诊断命令 | jcmd <pid> VM.flags |
Linux 系统工具
# 进程级 CPU/内存
top -p <pid>
top -Hp <pid> # 线程级
# 上下文切换
vmstat 1 5 # cs 列
# 线程级详细统计
pidstat -p <pid> -u -r -w 1 5
# -u CPU,-r 内存,-w 上下文切换
压测工具
| 工具 | 特点 | 适用场景 |
|---|---|---|
| ab(ApacheBench) | 轻量,命令行,快速验证 | 单接口 GET/POST 压测 |
| JMeter | 功能全,支持场景录制,图形化 | 复杂业务流程压测 |
| wrk | 高性能,Lua 脚本扩展 | HTTP 高并发基准测试 |
# ab 示例:100 请求,10 并发
ab -n 100 -c 10 http://localhost:8080/api/test
# 关键输出指标
# Requests per second:吞吐率(TPS)
# Time per request:用户平均等待时间
# Percentage of requests served within a certain time:响应时间分布
GC 日志分析工具
- GCViewer:本地图形化工具,打开 gc.log 文件
- GCeasy(https://www.gceasy.io):在线分析,上传压缩后的 gc.log,输出直观报告
常见性能陷阱
String 相关
// 陷阱:循环内用 + 拼接字符串,产生大量临时对象
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // 每次都创建新 String 对象
}
// 正确:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();
集合相关
// 陷阱:用 for(int i) 遍历 LinkedList,O(n²) 复杂度
LinkedList<String> list = getList();
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i)); // 每次 get 都从头遍历
}
// 正确:使用 Iterator 或 for-each
for (String s : list) {
System.out.println(s);
}
数据库相关
// 陷阱:一次查询返回大对象(60 个字段)
// 解决:首次只查关键字段,需要时再查其余字段
// 陷阱:频繁创建大对象导致 Full GC
// 解决:分页查询,避免单次加载过多数据
异常相关
// 陷阱:用异常控制正常业务流程
// 构建异常栈(fillInStackTrace)非常耗性能
try {
int value = Integer.parseInt(str);
} catch (NumberFormatException e) {
value = 0; // 如果 str 经常不是数字,这里性能很差
}
// 正确:先校验再转换
if (str != null && str.matches("\\d+")) {
int value = Integer.parseInt(str);
}
线程安全相关
// 陷阱:HashMap 在并发场景下使用(JDK 7 可能死循环)
// 高并发场景必须用 ConcurrentHashMap
// 陷阱:SimpleDateFormat 不是线程安全的
// 解决:使用 ThreadLocal 或 DateTimeFormatter(Java 8+)
private static final ThreadLocal<SimpleDateFormat> SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
参考资料
- Java 21 官方文档
- JVM 调优指南 — Oracle
- Java Platform, Standard Edition Tools Reference
- 《Java 性能权威指南(第二版)》— Scott Oaks
- 《深入理解 Java 虚拟机(第三版)》— 周志明
评论 (0)