Appearance
性能调优实践指南
一、为什么需要性能调优?
1.1 性能问题的代价
| 影响 | 描述 | 数据参考 |
|---|---|---|
| 用户体验 | 页面加载慢导致用户流失 | Google:加载时间从 1s 到 3s,跳出率增加 32% |
| 收入损失 | 性能差直接影响转化率 | Amazon:每 100ms 延迟损失 1% 销售额 |
| 资源浪费 | 低效代码消耗更多 CPU/内存 | 优化后可能节省 50% 以上服务器成本 |
| 系统崩溃 | 高负载下系统不可用 | OOM、线程池满、连接池耗尽 |
1.2 性能优化的核心指标
| 指标 | 含义 | 观察方式 | 典型阈值 |
|---|---|---|---|
| QPS | 每秒查询数 | Nginx 日志、监控系统 | 根据业务场景量化 |
| TPS | 每秒事务数 | APM 工具 | 根据业务场景量化 |
| RT(响应时间) | 请求处理耗时 | P50/P95/P99 | P99 < 200ms(接口) |
| CPU 使用率 | 处理器占用 | top, htop, Arthas | < 70%(保留余量) |
| 内存使用率 | 堆内存/物理内存占用 | jstat, Arthas, Grafana | 堆 < 75% |
| GC 频率和耗时 | GC 暂停时间和频率 | GC 日志, jstat | Full GC < 1次/天, STW < 100ms |
| 错误率 | 请求失败比例 | 监控系统 | < 0.1% |
| 连接数 | 数据库/Redis 连接池 | 连接池监控 | 不超过配置上限的 80% |
1.3 性能调优方法论
核心流程: 监控 -> 分析 -> 假设 -> 优化 -> 验证
+----------+ +----------+ +----------+ +----------+ +----------+
| 1. 监控 | -> | 2. 分析 | -> | 3. 假设 | -> | 4. 优化 | -> | 5. 验证 |
| 收集指标 | | 定位瓶颈 | | 提出方案 | | 实施改动 | | 压测对比 |
+----------+ +----------+ +----------+ +----------+ +----------+
↑ |
+----------------------------------------------------------------+
效果不理想则继续迭代关键原则:
- 不要过早优化: "Premature optimization is the root of all evil." -- Donald Knuth。先保证正确性,再优化性能。
- 数据驱动,不凭感觉: 用监控数据说话,用压测结果验证,不要凭经验猜测。
- 遵循二八原则: 80% 的性能问题来自 20% 的代码,先找到瓶颈再优化。
- 一次只改一个变量: 每次只调整一个参数,对比效果,否则无法归因。
- 优化有上限,架构无上限: 单机优化到家也有限,必要的架构调整(缓存、异步、分库分表)才能突破瓶颈。
二、JVM 调优
2.1 堆内存大小设置
bash
# 堆内存设置原则
# -Xms:初始堆大小,建议与 -Xmx 相同,避免堆扩容带来的性能抖动
# -Xmx:最大堆大小,建议不超过物理内存的 75%,预留系统和其他进程内存
# -Xmn:新生代大小,建议占堆的 1/4 ~ 1/3
# 典型配置(4 核 8G 服务器)
java -Xms4g -Xmx4g -Xmn1g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -jar app.jar
# 新生代大小估算公式:
# 新生代 > 每秒产生的对象大小 * 一次 Minor GC 的间隔时间
# 例如:每秒产生 50MB 对象,期望 10 秒一次 Young GC,则新生代至少 500MB堆内存布局:
+-----------------------------------------------------------+
| 堆内存 (Heap) |
| +---------------------+ +-----------------------------+ |
| | 新生代 (Young) | | 老年代 (Old) | |
| | +----+ +----+ +---+ | | | |
| | |Eden| | S0 | | S1| | | | |
| | +----+ +----+ +---+ | | | |
| +---------------------+ +-----------------------------+ |
| Eden:S0:S1 = 8:1:1 |
+-----------------------------------------------------------+2.2 GC 选择
| GC 收集器 | 特点 | 适用场景 | 关键参数 |
|---|---|---|---|
| Serial | 单线程,STW 长 | 客户端,小堆(< 100MB) | -XX:+UseSerialGC |
| Parallel | 多线程,吞吐量优先 | 后台计算,批处理 | -XX:+UseParallelGC |
| CMS | 并发标记,低延迟 | Web 服务(JDK 8 及以前) | -XX:+UseConcMarkSweepGC |
| G1 | 分区收集,可控 STW | 大堆(> 4GB),JDK 9+ 默认 | -XX:+UseG1GC |
| ZGC | 超低延迟(< 10ms) | 超低延迟,超大堆(TB) | -XX:+UseZGC (JDK 11+) |
| Shenandoah | 低延迟,并发回收 | 大堆,低延迟 | -XX:+UseShenandoahGC (JDK 12+) |
GC 选择决策树:
堆大小 < 4GB 且追求吞吐量 → Parallel GC
堆大小 > 4GB 且追求低延迟 → G1 GC
追求极低延迟 (< 10ms) → ZGC (JDK 11+)
JDK 8 且追求低延迟 → CMS (已废弃,建议升级)2.3 G1 GC 调优参数
bash
# G1 GC 核心参数
java \
-Xms8g -Xmx8g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \ # 期望最大 GC 暂停时间(毫秒)
-XX:G1HeapRegionSize=4m \ # Region 大小(1/2/4/8/16/32M)
-XX:InitiatingHeapOccupancyPercent=45 \ # 触发并发标记的堆占用阈值
-XX:G1ReservePercent=10 \ # 预留空间,防止晋升失败
-XX:ConcGCThreads=2 \ # 并发 GC 线程数
-XX:ParallelGCThreads=4 \ # 并行 GC 线程数
-jar app.jar关键调优思路:
MaxGCPauseMillis:不是越小越好,太小会导致 GC 频繁,吞吐量下降。一般设置 100-200ms。IHOP:并发标记触发阈值,太低导致频繁并发标记,太高导致并发标记来不及完成(Full GC)。默认 45%。- 如果出现
to-space exhausted或 Full GC,说明晋升失败,需要增大堆或调整G1ReservePercent。
2.4 GC 日志分析
bash
# GC 日志参数(JDK 8)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/gc.log
# GC 日志参数(JDK 11+)
-Xlog:gc*:file=/var/log/gc.log:time,level,tags
# GC 日志示例(G1)
[2024-01-15T10:30:45.123+0800] GC(100) Pause Young (Normal) (G1 Evacuation Pause) 150M->100M(500M) 15.2ms
[2024-01-15T10:30:45.456+0800] GC(101) Pause Young (Concurrent Start) (G1 Evacuation Pause) 200M->150M(500M) 18.3ms
[2024-01-15T10:30:46.789+0800] GC(102) Pause Remark 80M->80M(500M) 5.2ms
[2024-01-15T10:30:47.012+0800] GC(103) Pause Cleanup 80M->80M(500M) 0.5ms
# 日志解读
# 150M->100M(500M):GC 前堆占用 -> GC 后堆占用(堆总大小)
# 15.2ms:GC 暂停时间常用 GC 分析工具:
| 工具 | 用途 | 特点 |
|---|---|---|
| GCViewer | GC 日志可视化 | 离线分析,查看吞吐量、暂停时间分布 |
| GCEasy.io | 在线 GC 分析 | 自动生成报告,给出优化建议 |
| jstat | 实时 GC 统计 | 命令行,jstat -gcutil <pid> 1000 |
| Arthas dashboard | 实时 JVM 监控 | 可视化面板,GC 次数/时间 |
2.5 JVM 调优案例
java
/**
* 常见问题:频繁 Full GC
*
* 排查步骤:
* 1. jstat -gcutil <pid> 1000 观察 FGC 频率
* 2. jmap -histo <pid> 查看对象分布
* 3. jmap -dump:format=b,file=heap.bin <pid> 导出堆转储
* 4. MAT/JProfiler 分析大对象和 GC Root 引用链
*/
// 常见 Full GC 原因及解决方案:
// 1. 大对象直接进入老年代(调整 -XX:PretenureSizeThreshold)
// 2. 老年代空间不足(增大堆内存或调整晋升阈值)
// 3. 元空间不足(增大 -XX:MaxMetaspaceSize)
// 4. System.gc() 显式调用(-XX:+DisableExplicitGC)
// 5. 内存泄漏(分析堆转储,找到泄漏点)
// 内存泄漏检查示例
@RestController
public class MemoryLeakExample {
// 错误示例:静态集合持续增长,导致 OOM
private static final List<Object> leakList = new ArrayList<>();
@GetMapping("/leak")
public void leak() {
// 每次请求都往静态集合添加对象,永不释放
leakList.add(new byte[1024 * 1024]); // 每次 1MB
}
// 正确做法:使用 LRU 缓存或定期清理
private static final Map<Long, Object> cache =
Collections.synchronizedMap(new LinkedHashMap<Long, Object>() {
@Override
protected boolean removeEldestEntry(Map.Entry<Long, Object> eldest) {
return size() > 1000; // 超过 1000 条自动删除最旧条目
}
});
}三、数据库调优
3.1 慢查询优化
sql
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.1; -- 超过 100ms 记录
SET GLOBAL log_queries_not_using_indexes = ON;
-- 使用 EXPLAIN 分析执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 1001 AND status = 'PAID';
-- EXPLAIN 输出关键字段解读:
-- type: 访问类型,ALL(全表扫描) < index < range < ref < eq_ref < const
-- 目标:至少达到 range 级别,最好达到 ref 级别
-- key: 实际使用的索引,NULL 表示没用到索引
-- rows: 预估扫描行数,越小越好
-- Extra: 额外信息,Using filesort(需要优化) / Using temporary(需要优化)
-- Using index(覆盖索引,好) / Using where(需要回表)3.2 索引优化原则
索引设计的核心原则(三星索引):
1. 一星:WHERE 条件列全部命中索引(减少扫描行数)
2. 二星:避免排序(ORDER BY 列在索引中)
3. 三星:覆盖索引(SELECT 列全部在索引中,避免回表)sql
-- 索引优化示例
-- 场景:查询用户的待支付订单
-- SELECT order_id, user_id, amount, create_time
-- FROM orders
-- WHERE user_id = 1001 AND status = 'PENDING'
-- ORDER BY create_time DESC;
-- 1. 差:单列索引(只用到 user_id)
CREATE INDEX idx_user_id ON orders(user_id);
-- 2. 中:联合索引(覆盖 WHERE 和 ORDER BY)
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);
-- 3. 优:覆盖索引(避免回表)
CREATE INDEX idx_cover ON orders(user_id, status, create_time, order_id, amount);
-- 索引设计原则:
-- 1. 最左前缀:联合索引必须从最左列开始匹配
-- 2. 区分度高的列放前面:如 user_id 区分度 > status 区分度
-- 3. 不为索引列使用函数:WHERE DATE(create_time) = '2024-01-01' 不走索引
-- 4. 避免使用 SELECT *:指定需要的列,利于覆盖索引
-- 5. 联合索引列数不宜过多(通常不超过 5 列)3.3 连接池调优
java
// HikariCP 连接池配置(Spring Boot 默认,性能最优)
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.hikari")
public HikariConfig hikariConfig() {
HikariConfig config = new HikariConfig();
// 核心参数
config.setMaximumPoolSize(20); // 最大连接数
config.setMinimumIdle(10); // 最小空闲连接数
config.setConnectionTimeout(30000); // 连接超时(ms)
config.setIdleTimeout(600000); // 空闲超时(ms)
config.setMaxLifetime(1800000); // 连接最大生命周期(ms)
// 连接池大小计算公式:
// 连接数 = ((核心数 * 2) + 有效磁盘数)
// 示例:4 核 CPU + 1 块 SSD
// 连接数 = (4 * 2) + 1 = 9
// 实际生产中,还需要考虑下游服务的并发承载能力
return config;
}
}
// application.yml
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# 监控
pool-name: OrderPool
connection-test-query: SELECT 1连接池监控指标:
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| Active Connections | 当前活跃连接数 | > 80% 最大连接数 |
| Idle Connections | 空闲连接数 | < 最小空闲连接数 |
| Pending Connections | 等待连接的线程数 | > 0 持续超过 5 秒 |
| Connection Timeout | 获取连接超时次数 | > 0 |
| Connection Creation Rate | 连接创建速率 | 突然飙升说明有问题 |
3.4 数据库优化清单
| 优化方向 | 具体措施 | 预期效果 |
|---|---|---|
| SQL 优化 | 避免 SELECT *,使用覆盖索引 | 减少 IO 和网络传输 |
| 索引优化 | 根据查询模式创建联合索引 | 查询从秒级降到毫秒级 |
| 连接池 | 合理配置连接数,HikariCP | 避免连接泄漏和等待 |
| 批量操作 | 批量 INSERT/UPDATE 替代逐条 | 提升 10-100 倍写入性能 |
| 读写分离 | 主库写,从库读 | 分散读压力 |
| 分库分表 | 水平拆分热点表 | 突破单库性能瓶颈 |
| 冷热分离 | 历史数据归档,热数据保留 | 减少单表数据量 |
| 参数调优 | InnoDB buffer pool、连接数 | 充分利用硬件资源 |
四、代码级调优
4.1 避免在循环中创建对象
java
/**
* 常见性能陷阱及优化
*/
@Service
public class CodeOptimizationExample {
// ========== 1. 避免循环中创建对象 ==========
// 错误:每次循环创建 SimpleDateFormat
public List<String> formatDatesBad(List<Date> dates) {
List<String> result = new ArrayList<>();
for (Date date : dates) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
result.add(sdf.format(date));
}
return result;
}
// 正确:复用对象或使用 ThreadLocal
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public List<String> formatDatesGood(List<Date> dates) {
SimpleDateFormat sdf = DATE_FORMAT.get();
List<String> result = new ArrayList<>(dates.size());
for (Date date : dates) {
result.add(sdf.format(date));
}
return result;
}
// ========== 2. 字符串拼接使用 StringBuilder ==========
// 错误:循环中使用 + 拼接(每次创建新 String 对象)
public String buildSqlBad(List<Long> ids) {
String sql = "SELECT * FROM orders WHERE id IN (";
for (Long id : ids) {
sql += id + ","; // 每次循环创建新的 String 对象
}
return sql.substring(0, sql.length() - 1) + ")";
}
// 正确:使用 StringBuilder
public String buildSqlGood(List<Long> ids) {
StringBuilder sb = new StringBuilder("SELECT * FROM orders WHERE id IN (");
for (int i = 0; i < ids.size(); i++) {
if (i > 0) sb.append(",");
sb.append(ids.get(i));
}
sb.append(")");
return sb.toString();
}
// ========== 3. 集合初始化时指定容量 ==========
// 错误:默认容量 10,频繁扩容
public List<String> collectNamesBad(List<User> users) {
List<String> names = new ArrayList<>(); // 默认容量 10
for (User user : users) {
names.add(user.getName());
}
return names;
}
// 正确:指定初始容量,避免扩容开销
public List<String> collectNamesGood(List<User> users) {
List<String> names = new ArrayList<>(users.size()); // 预分配容量
for (User user : users) {
names.add(user.getName());
}
return names;
}
// HashMap 初始容量计算:expectedSize / 0.75 + 1
// 例如:期望存 100 个元素,初始容量 = 100 / 0.75 + 1 = 134
public Map<Long, User> buildUserMap(List<User> users) {
int initialCapacity = (int) (users.size() / 0.75) + 1;
Map<Long, User> map = new HashMap<>(initialCapacity);
for (User user : users) {
map.put(user.getId(), user);
}
return map;
}
// ========== 4. 合理使用并行流 ==========
// 注意:并行流不是万能的,数据量小、IO 密集时不适用
public List<UserVO> convertUsers(List<User> users) {
// 数据量 > 1000 且 CPU 密集计算时,使用并行流有收益
if (users.size() > 1000) {
return users.parallelStream()
.map(this::convertToVO) // 假设 convertToVO 是 CPU 密集型
.collect(Collectors.toList());
}
return users.stream()
.map(this::convertToVO)
.collect(Collectors.toList());
}
// ========== 5. 使用 try-with-resources 自动关闭资源 ==========
// 错误:手动关闭,容易遗漏
public String readFileBad(String path) throws IOException {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(path));
return reader.readLine();
} finally {
if (reader != null) {
reader.close(); // 可能抛异常,且 close 本身也可能抛异常
}
}
}
// 正确:try-with-resources
public String readFileGood(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
}
}
// ========== 6. 使用懒加载,延迟初始化 ==========
// 错误:启动时加载所有不需要的数据
@Component
public class BadConfigLoader {
private Map<String, String> config;
@PostConstruct
public void init() {
// 启动时加载全部配置,拖慢启动速度
config = loadAllConfigs(); // 耗时操作
}
}
// 正确:按需加载 + 缓存
@Component
public class LazyConfigLoader {
private volatile Map<String, String> config;
public String getConfig(String key) {
if (config == null) {
synchronized (this) {
if (config == null) {
config = loadAllConfigs(); // 首次访问时加载
}
}
}
return config.get(key);
}
}
// ========== 7. 避免装箱拆箱 ==========
// 错误:不必要的装箱拆箱
public long sumBad(List<Long> numbers) {
Long sum = 0L; // 使用包装类型
for (Long num : numbers) {
sum += num; // 每次循环拆箱、装箱
}
return sum;
}
// 正确:使用基本类型
public long sumGood(List<Long> numbers) {
long sum = 0L; // 使用基本类型
for (Long num : numbers) {
sum += num; // 只拆箱一次
}
return sum;
}
}4.2 性能优化清单
| 优化项 | 优化前 | 优化后 | 收益 |
|---|---|---|---|
| 循环中创建对象 | 每次 new | 复用或 ThreadLocal | 减少 GC 压力 |
| 字符串拼接 | + 拼接 | StringBuilder | 10-100x 提升 |
| 集合初始化 | 默认容量 | 预估容量 | 减少扩容开销 |
| 装箱拆箱 | 包装类型 | 基本类型 | 减少对象创建 |
| 并行流 | 串行 stream | 适量 parallelStream | 多核利用率提升 |
| 资源关闭 | 手动 try-finally | try-with-resources | 避免资源泄漏 |
| 正则编译 | 每次 Pattern.compile | 预编译 Pattern | 5-10x 提升 |
| HashMap 遍历 | keySet + get | entrySet | 减少一次哈希查找 |
五、Arthas 在线诊断
5.1 核心命令
Arthas 是阿里巴巴开源的 Java 在线诊断工具,无需重启应用即可排查问题。
bash
# 安装和启动 Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 选择目标 Java 进程
# 进入 Arthas 控制台后,常用命令:
# 1. dashboard - 实时监控面板
dashboard
# 显示:线程、内存、GC、CPU 等实时指标
# 2. thread - 查看线程情况
thread # 查看所有线程
thread -n 3 # 查看 CPU 使用率最高的 3 个线程
thread -b # 查看阻塞线程(死锁检测)
thread <thread-id> # 查看指定线程的堆栈
# 3. watch - 方法调用观测
watch com.example.OrderService createOrder '{params, returnObj, throwExp}' -x 3
# 观测 createOrder 方法的入参、返回值、异常
# 4. trace - 方法调用链路追踪
trace com.example.OrderService createOrder
# 追踪 createOrder 方法的调用链,显示每个调用的耗时
trace com.example.OrderService createOrder '#cost > 100'
# 只追踪耗时 > 100ms 的调用
# 5. monitor - 方法调用统计
monitor -c 5 com.example.OrderService createOrder
# 每 5 秒统计一次 createOrder 的调用次数、成功率、平均耗时
# 6. jad - 反编译
jad com.example.OrderService
# 反编译已加载的类,确认线上代码是否最新
# 7. stack - 调用路径追踪
stack com.example.OrderService createOrder
# 输出调用 createOrder 的调用路径
# 8. vmoption - 查看/设置 JVM 参数
vmoption
vmoption PrintGC true
# 9. logger - 动态修改日志级别
logger --name com.example --level DEBUG
# 将 com.example 包的日志级别临时改为 DEBUG
# 10. heapdump - 导出堆转储
heapdump /tmp/heap.hprof
# 导出后使用 MAT 或 JProfiler 分析
# 11. profiler - 火焰图
profiler start
profiler stop --format html
# 生成 CPU 火焰图,直观展示热点代码5.2 诊断实战例子
bash
# 场景:接口响应慢,排查瓶颈
# 步骤 1:查看整体情况
dashboard
# 观察到 CPU 使用率高,GC 频繁
# 步骤 2:找到 CPU 最高的线程
thread -n 5
# 发现 "http-nio-8080-exec-3" 线程 CPU 97%
# 步骤 3:追踪慢方法的调用链
trace com.example.controller.OrderController getOrderDetail '#cost > 200'
# 输出:
# +---[98%] com.example.service.OrderService.getOrder() #cost=450ms
# +---[80%] com.example.dao.OrderDao.selectById() #cost=360ms ← 瓶颈!
# +---[15%] com.example.service.UserService.getUser() #cost=68ms
# 步骤 4:分析 SQL 是否慢查询
# 在数据库端执行 EXPLAIN,发现 selectById 没有走索引
# 步骤 5:确认问题后,添加缺失的索引
# ALTER TABLE orders ADD INDEX idx_order_id(order_id);六、JMH 基准测试
6.1 为什么需要 JMH?
JVM 的 JIT 编译器会在运行时优化代码,简单的 System.currentTimeMillis() 测量方式无法准确反映真实性能。JMH(Java Microbenchmark Harness)是 OpenJDK 官方提供的微基准测试工具,能正确处理 JVM 预热、死代码消除、循环展开等优化。
java
/**
* JMH 基准测试示例
* 对比 StringBuilder 和 String 拼接的性能
*/
@BenchmarkMode(Mode.Throughput) // 吞吐量模式
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread) // 每个线程独立状态
@Warmup(iterations = 3, time = 1) // 预热 3 轮,每轮 1 秒
@Measurement(iterations = 5, time = 1) // 测量 5 轮,每轮 1 秒
@Fork(1) // 1 个 fork 进程
public class StringBenchmark {
private static final int SIZE = 1000;
private List<String> items;
@Setup
public void setup() {
items = new ArrayList<>(SIZE);
for (int i = 0; i < SIZE; i++) {
items.add("item" + i);
}
}
@Benchmark
public String stringConcat() {
String result = "";
for (String item : items) {
result += item;
}
return result;
}
@Benchmark
public String stringBuilder() {
StringBuilder sb = new StringBuilder();
for (String item : items) {
sb.append(item);
}
return sb.toString();
}
@Benchmark
public String stringBuilderWithSize() {
StringBuilder sb = new StringBuilder(SIZE * 5); // 预估容量
for (String item : items) {
sb.append(item);
}
return sb.toString();
}
// 运行结果示例:
// Benchmark Mode Cnt Score Error Units
// StringBenchmark.stringConcat thrpt 5 5.123 ± 0.456 ops/ms
// StringBenchmark.stringBuilder thrpt 5 89.456 ± 3.210 ops/ms
// StringBenchmark.stringBuilderWithSize thrpt 5 105.234 ± 2.890 ops/ms
// 结论:StringBuilder 比 String 拼接快 17 倍,预估容量再提升 18%
}6.2 JMH 常见注解
| 注解 | 作用 | 说明 |
|---|---|---|
@BenchmarkMode | 指定测试模式 | Throughput(吞吐量), AverageTime(平均时间), SampleTime(采样) |
@OutputTimeUnit | 结果时间单位 | MILLISECONDS, MICROSECONDS, NANOSECONDS |
@State | 状态作用域 | Thread(线程独享), Benchmark(测试共享), Group(组内共享) |
@Warmup | 预热配置 | 让 JVM 充分 JIT 编译后再测量 |
@Measurement | 测量配置 | 正式测量的轮数和时间 |
@Fork | Fork 进程数 | 多个独立 JVM 进程运行,避免互相影响 |
@Setup | 初始化方法 | 类似 @Before,每个 Benchmark 前执行 |
@TearDown | 清理方法 | 类似 @After,每个 Benchmark 后执行 |
@Param | 参数化测试 | 对不同参数值分别测试 |
@Benchmark | 标记测试方法 | 此方法会被 JMH 测试 |
java
/**
* 参数化 JMH 测试:对比不同集合大小的创建性能
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(1)
public class CollectionBenchmark {
@Param({"10", "100", "1000", "10000"})
private int size;
@Benchmark
public List<String> arrayListNoCapacity() {
List<String> list = new ArrayList<>();
for (int i = 0; i < size; i++) {
list.add("item" + i);
}
return list;
}
@Benchmark
public List<String> arrayListWithCapacity() {
List<String> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
list.add("item" + i);
}
return list;
}
}七、性能调优实战案例
7.1 案例:接口从 500ms 优化到 50ms
问题:订单详情接口 RT=500ms,P99=2s
排查过程:
1. Arthas trace 发现 getOrderDetail 内部调用链:
- getOrderFromDB() → 150ms(正常)
- getUserInfo() → 200ms(远程调用,慢)
- getProductInfo() → 150ms(远程调用,慢)
2. 优化措施:
- 用户信息:加 Redis 缓存(TTL=30min),命中率 95%+
- 商品信息:加 Redis 缓存(TTL=10min),命中率 90%+
- 优化结果:远程调用命中缓存后 < 5ms
3. 第二轮排查,发现还有慢请求:
- 缓存未命中时,串行调用两个远程服务
4. 进一步优化:并行调用
- 使用 CompletableFuture 并行获取用户和商品信息
- 总耗时 = max(getUserTime, getProductTime) 而非两者之和
5. 最终结果:
- 缓存命中:RT = 15ms
- 缓存未命中:RT = 50ms(并行调用)
- P99:从 2s 降到 100msjava
/**
* 并行调用优化示例
*/
@Service
public class OrderServiceOptimized {
@Autowired
private UserClient userClient;
@Autowired
private ProductClient productClient;
@Autowired
private OrderDao orderDao;
public OrderDetailVO getOrderDetail(Long orderId, Long userId) {
// 1. 查订单
Order order = orderDao.getById(orderId);
// 2. 并行查询用户和商品信息
CompletableFuture<UserVO> userFuture = CompletableFuture
.supplyAsync(() -> userClient.getUserById(userId));
CompletableFuture<ProductVO> productFuture = CompletableFuture
.supplyAsync(() -> productClient.getProductById(order.getProductId()));
// 3. 等待并合并结果
CompletableFuture.allOf(userFuture, productFuture).join();
UserVO user = userFuture.join();
ProductVO product = productFuture.join();
return OrderDetailVO.builder()
.order(order)
.user(user)
.product(product)
.build();
}
}7.2 案例:批量插入优化
java
/**
* 批量插入优化:从 100 条/秒 提升到 10000 条/秒
*/
@Service
public class BatchInsertService {
@Autowired
private JdbcTemplate jdbcTemplate;
// 优化前:逐条插入(100 条/秒)
@Transactional
public void insertOneByOne(List<Order> orders) {
for (Order order : orders) {
jdbcTemplate.update(
"INSERT INTO orders (user_id, amount, status) VALUES (?, ?, ?)",
order.getUserId(), order.getAmount(), order.getStatus()
);
}
}
// 优化后:批量插入(10000 条/秒)
@Transactional
public void batchInsert(List<Order> orders) {
String sql = "INSERT INTO orders (user_id, amount, status) VALUES (?, ?, ?)";
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Order order = orders.get(i);
ps.setLong(1, order.getUserId());
ps.setBigDecimal(2, order.getAmount());
ps.setString(3, order.getStatus());
}
@Override
public int getBatchSize() {
return orders.size();
}
});
}
// 超大批量:分批 + 批量插入
@Transactional
public void batchInsertLarge(List<Order> orders) {
int batchSize = 500; // 每批 500 条,避免一次提交太多
for (int i = 0; i < orders.size(); i += batchSize) {
int end = Math.min(i + batchSize, orders.size());
List<Order> batch = orders.subList(i, end);
batchInsert(batch);
}
}
}八、面试高频问题
Q1: 线上 CPU 100% 如何排查?
top找到 CPU 最高的 Java 进程 PIDtop -H -p <PID>找到 CPU 最高的线程 TIDprintf '%x\n' <TID>将线程 ID 转为十六进制jstack <PID> | grep <十六进制TID> -A 30查看线程堆栈- 或者直接使用 Arthas:
thread -n 3一键查看 CPU 最高的线程
Q2: 线上 OOM 如何排查?
- JVM 参数添加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof - 从
/tmp/heap.hprof获取堆转储文件 - 使用 MAT(Memory Analyzer Tool)分析:
- Leak Suspects Report:查看疑似泄漏报告
- Dominator Tree:按占用内存大小排序对象
- Histogram:按类统计对象数量
- 找到占用内存最大的对象,分析 GC Root 引用链
- 常见原因:静态集合不断增长、ThreadLocal 未清理、连接未关闭
Q3: 如何选择 G1 还是 ZGC?
- G1:JDK 9+ 默认,适合大多数场景,延迟目标 50-200ms
- ZGC:JDK 11+ 实验性,JDK 15+ 生产可用,延迟目标 < 10ms,适合超大堆(TB 级)
- 选择:堆 < 4GB 用 Parallel GC;堆 4-32GB 用 G1;超大堆或极低延迟用 ZGC
Q4: 索引失效的常见原因有哪些?
- 联合索引不满足最左前缀原则
- 在索引列上使用函数:
WHERE DATE(create_time) = '2024-01-01' - 隐式类型转换:
WHERE phone = 13800138000(phone 是 varchar 类型) - LIKE 以
%开头:WHERE name LIKE '%张三' - OR 条件中包含非索引列
- 使用了不等于(
!=、<>)、NOT IN、NOT EXISTS - 优化器认为全表扫描更快(数据量小或区分度低)
Q5: 如何分析 MySQL 慢查询?
- 开启慢查询日志:
long_query_time = 0.1 - 使用
mysqldumpslow或pt-query-digest分析慢查询日志 - 对慢 SQL 执行
EXPLAIN分析执行计划 - 重点关注:type=ALL(全表扫描)、Extra=Using filesort/Using temporary
- 根据分析结果:添加索引、改写 SQL、优化表结构
Q6: 性能调优的常见误区有哪些?
- 过早优化:系统还没上线就做极致优化,浪费精力
- 凭感觉优化:不分析数据,凭经验猜测瓶颈
- 只关注代码,忽视架构:代码优化到极致也就 2-3 倍提升,架构优化可能 10-100 倍
- 一次改多个变量:改了代码又改配置又改架构,无法确定哪个有效
- 不做压测验证:优化完不压测,凭感觉认为"应该快了"
Q7: Arthas 的 trace 和 watch 命令有什么区别?
trace:追踪方法内部调用链和每个方法的耗时,适合排查"慢在哪里"watch:观测方法的入参、返回值、异常,适合排查"结果对不对"- 组合使用:先用
trace找到慢的方法,再用watch看该方法的入参和返回值是否异常
