第 21 章:性能调优
学习目标
- 掌握性能调优的整体方法论
- 学会用压测找瓶颈
- 实战 CPU/内存/磁盘/网络调优
- 避免过早优化、调优无依据的坑
一、性能调优方法论
text
1. 设定目标(QPS、延迟、错误率)
2. 压测摸底(单接口 / 全链路)
3. 定位瓶颈(CPU / 内存 / IO / DB / 缓存)
4. 实施优化(代码 / 配置 / 架构)
5. 验证效果(再压测、A/B)⚠️ 坑 1:"我觉得这里慢" → 改了一通 → 没效果。先压测,看指标,再调优。
二、JMH 微基准测试
java
// 测试字符串拼接
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class StringConcatBenchmark {
@Benchmark
public String concatPlus() {
return "a" + "b" + "c";
}
@Benchmark
public String concatBuilder() {
return new StringBuilder().append("a").append("b").append("c").toString();
}
}bash
mvn clean install
java -jar target/benchmarks.jar StringConcatBenchmark三、Arthas 在线诊断
bash
# 1. 启动 Arthas
java -jar arthas-boot.jar
# 2. 查看最慢的 3 个方法
dashboard
# 3. 跟踪方法调用
trace com.example.OrderService createOrder '#cost > 100'
# 4. 查看方法入参
watch com.example.OrderService createOrder '{params, returnObj, throwExp}' -x 2
# 5. 反编译查看
jad com.example.OrderService createOrder
# 6. 热更新代码(临时生效)
redefine /tmp/OrderService.classtext
常用命令
├── dashboard: 系统信息
├── thread: 线程状态
├── jvm: JVM 信息
├── trace: 方法追踪
├── watch: 观察入参返回值
├── jad: 反编译
└── redefine: 热更新四、CPU 飙高排查
bash
# 1. 找到高 CPU 进程
top -Hp <pid>
# 2. 找到高 CPU 线程(十进制 PID)
printf '%x' <pid>
# 3. 打印线程栈
jstack <pid> | grep -A 30 <hex_pid>java
// 典型场景:死循环
public void hotLoop() {
while (true) { // CPU 100%
doSomething();
}
}
// 修复:加退出条件
public void hotLoop() {
while (!Thread.currentThread().isInterrupted()) {
doSomething();
}
}java
// 频繁 Full GC
public void createObjects() {
for (int i = 0; i < 10000; i++) {
list.add(new Order()); // 大量临时对象
}
}
// 修复:对象池 / 减少分配五、内存泄漏排查
java
// 常见 1:静态集合
public class Cache {
private static final Map<String, Object> cache = new HashMap<>();
// 永不释放,类卸载时才释放
}
// 修复:用 WeakHashMap 或 Caffeine 带过期java
// 常见 2:线程局部变量未清理
public void handleRequest() {
ThreadLocal<User> tl = new ThreadLocal<>();
tl.set(user);
// 业务
// ❌ 忘记 remove
}
// 修复:try-finally removebash
# 导出堆 dump
jmap -dump:format=b,file=heap.hprof <pid>
# 用 MAT / VisualVM 分析
# 重点看:Retained Heap 大的对象⚠️ 坑 2:
-Xmx配太大(16G),堆 dump 文件 10G+,分析工具崩溃。生产不超过 8G。
六、数据库慢查询
sql
-- 1. 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 1 秒
-- 2. 查看慢查询
SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10;java
// 3. EXPLAIN 分析
// 加索引
@Query("SELECT o FROM Order o WHERE o.userId = :userId AND o.status = :status")
@Index(name = "idx_user_status", columns = ["userId", "status"])
List<Order> findByUserAndStatus(@Param("userId") Long userId, @Param("status") String status);sql
-- 4. 避免 SELECT *
SELECT * FROM order WHERE id = 1; -- ❌
SELECT id, user_id, amount FROM order WHERE id = 1; -- ✅七、缓存设计
java
// 1. 缓存热点数据
@Cacheable(value = "product", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id).orElseThrow();
}
// 2. 缓存预热
@PostConstruct
public void warmup() {
List<Product> hotProducts = productRepository.findByHot();
hotProducts.forEach(p -> cache.put("product:" + p.id(), p));
}
// 3. 防止缓存穿透
public Product getProduct(Long id) {
return cache.get("product:" + id, () -> {
return productRepository.findById(id).orElse(new Product()); // 空值也缓存
});
}java
// 4. 防止缓存雪崩
@Cacheable(value = "product", key = "#id", unless = "#result == null")
// TTL 加随机值
public Product getProduct(Long id) {
Product p = productRepository.findById(id).orElse(null);
redisTemplate.opsForValue()
.set("product:" + id, p, Duration.ofSeconds(300 + ThreadLocalRandom.current().nextInt(60)));
return p;
}⚠️ 坑 3:同一个 key 多个请求无缓存,100 个请求同时打到 DB(缓存击穿)。加分布式锁或用
synchronized限流。
八、连接池调优
yaml
# HikariCP
spring:
datasource:
hikari:
maximum-pool-size: 20 # 公式:2 * CPU + 有效磁盘数
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000 # 60s 没归还报警java
// 监控连接池
@Bean
public HikariDataSource dataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setMetricRegistry(meterRegistry); // 暴露指标
return ds;
}九、Tomcat 调优
yaml
server:
tomcat:
threads:
max: 200 # 业务线程,默认 200
min-spare: 10
accept-count: 100 # 等待队列
max-connections: 10000
connection-timeout: 5000java
// 异步 servlet
@WebServlet(urlPatterns = "/api/order", asyncSupported = true)
public class OrderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
AsyncContext ctx = req.startAsync();
ctx.start(() -> {
// 业务,工作线程
try {
resp.getWriter().write("ok");
} catch (IOException e) {
e.printStackTrace();
} finally {
ctx.complete();
}
});
}
}十、压测验证
java
// JMeter 脚本或 wrk
wrk -t4 -c100 -d30s http://localhost:8080/api/ordertext
压测结果
├── QPS: 1000 → 5000
├── P99: 200ms → 50ms
├── 错误率: 0.01% → 0.001%
└── CPU: 80% → 60%本章小结
| 阶段 | 工具 |
|---|---|
| 性能测试 | JMeter、wrk、JMH |
| 在线诊断 | Arthas、async-profiler |
| 内存分析 | MAT、VisualVM |
| 慢查询 | 慢查询日志、EXPLAIN |
| 优化点 | 收益 |
|---|---|
| 缓存 | 10x |
| 异步 | 5x |
| 数据库 | 5x |
| JVM | 2x |
动手练习
- JMH 基准:对比 ArrayList vs LinkedList,看场景差异
- Arthas 实战:用一个有性能问题的 demo,定位慢方法
- 缓存压测:无缓存 vs 加缓存,对比 QPS 提升
- 数据库优化:分析慢查询,加索引,验证效果
下一章:第 22 章:JVM 调优 →