Skip to content

性能调优实践指南

一、为什么需要性能调优?

1.1 性能问题的代价

影响描述数据参考
用户体验页面加载慢导致用户流失Google:加载时间从 1s 到 3s,跳出率增加 32%
收入损失性能差直接影响转化率Amazon:每 100ms 延迟损失 1% 销售额
资源浪费低效代码消耗更多 CPU/内存优化后可能节省 50% 以上服务器成本
系统崩溃高负载下系统不可用OOM、线程池满、连接池耗尽

1.2 性能优化的核心指标

指标含义观察方式典型阈值
QPS每秒查询数Nginx 日志、监控系统根据业务场景量化
TPS每秒事务数APM 工具根据业务场景量化
RT(响应时间)请求处理耗时P50/P95/P99P99 < 200ms(接口)
CPU 使用率处理器占用top, htop, Arthas< 70%(保留余量)
内存使用率堆内存/物理内存占用jstat, Arthas, Grafana堆 < 75%
GC 频率和耗时GC 暂停时间和频率GC 日志, jstatFull GC < 1次/天, STW < 100ms
错误率请求失败比例监控系统< 0.1%
连接数数据库/Redis 连接池连接池监控不超过配置上限的 80%

1.3 性能调优方法论

核心流程: 监控 -> 分析 -> 假设 -> 优化 -> 验证

+----------+    +----------+    +----------+    +----------+    +----------+
| 1. 监控   | -> | 2. 分析   | -> | 3. 假设   | -> | 4. 优化   | -> | 5. 验证   |
| 收集指标  |    | 定位瓶颈  |    | 提出方案  |    | 实施改动  |    | 压测对比  |
+----------+    +----------+    +----------+    +----------+    +----------+
     ↑                                                                |
     +----------------------------------------------------------------+
                          效果不理想则继续迭代

关键原则:

  1. 不要过早优化: "Premature optimization is the root of all evil." -- Donald Knuth。先保证正确性,再优化性能。
  2. 数据驱动,不凭感觉: 用监控数据说话,用压测结果验证,不要凭经验猜测。
  3. 遵循二八原则: 80% 的性能问题来自 20% 的代码,先找到瓶颈再优化。
  4. 一次只改一个变量: 每次只调整一个参数,对比效果,否则无法归因。
  5. 优化有上限,架构无上限: 单机优化到家也有限,必要的架构调整(缓存、异步、分库分表)才能突破瓶颈。

二、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 分析工具:

工具用途特点
GCViewerGC 日志可视化离线分析,查看吞吐量、暂停时间分布
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 压力
字符串拼接+ 拼接StringBuilder10-100x 提升
集合初始化默认容量预估容量减少扩容开销
装箱拆箱包装类型基本类型减少对象创建
并行流串行 stream适量 parallelStream多核利用率提升
资源关闭手动 try-finallytry-with-resources避免资源泄漏
正则编译每次 Pattern.compile预编译 Pattern5-10x 提升
HashMap 遍历keySet + getentrySet减少一次哈希查找

五、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测量配置正式测量的轮数和时间
@ForkFork 进程数多个独立 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 降到 100ms
java
/**
 * 并行调用优化示例
 */
@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% 如何排查?

  1. top 找到 CPU 最高的 Java 进程 PID
  2. top -H -p <PID> 找到 CPU 最高的线程 TID
  3. printf '%x\n' <TID> 将线程 ID 转为十六进制
  4. jstack <PID> | grep <十六进制TID> -A 30 查看线程堆栈
  5. 或者直接使用 Arthas:thread -n 3 一键查看 CPU 最高的线程

Q2: 线上 OOM 如何排查?

  1. JVM 参数添加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof
  2. /tmp/heap.hprof 获取堆转储文件
  3. 使用 MAT(Memory Analyzer Tool)分析:
    • Leak Suspects Report:查看疑似泄漏报告
    • Dominator Tree:按占用内存大小排序对象
    • Histogram:按类统计对象数量
  4. 找到占用内存最大的对象,分析 GC Root 引用链
  5. 常见原因:静态集合不断增长、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 慢查询?

  1. 开启慢查询日志:long_query_time = 0.1
  2. 使用 mysqldumpslowpt-query-digest 分析慢查询日志
  3. 对慢 SQL 执行 EXPLAIN 分析执行计划
  4. 重点关注:type=ALL(全表扫描)、Extra=Using filesort/Using temporary
  5. 根据分析结果:添加索引、改写 SQL、优化表结构

Q6: 性能调优的常见误区有哪些?

  • 过早优化:系统还没上线就做极致优化,浪费精力
  • 凭感觉优化:不分析数据,凭经验猜测瓶颈
  • 只关注代码,忽视架构:代码优化到极致也就 2-3 倍提升,架构优化可能 10-100 倍
  • 一次改多个变量:改了代码又改配置又改架构,无法确定哪个有效
  • 不做压测验证:优化完不压测,凭感觉认为"应该快了"

Q7: Arthas 的 trace 和 watch 命令有什么区别?

  • trace:追踪方法内部调用链和每个方法的耗时,适合排查"慢在哪里"
  • watch:观测方法的入参、返回值、异常,适合排查"结果对不对"
  • 组合使用:先用 trace 找到慢的方法,再用 watch 看该方法的入参和返回值是否异常