本文目录导读:

这是一个非常务实的问题,很多开发者看过无数的优化理论,但到了实际项目里却不知道从哪里下手,或者“优化”后反而出了问题。
核心原则:先度量,再优化,后验证。
不要凭感觉优化,性能优化的本质是用可控的成本(开发时间、代码复杂度、硬件资源)换取更低的延迟或更高的吞吐量。
下面我从三个常见且高回报的落地场景出发,结合具体的案例和操作步骤,告诉你如何真正把优化做到代码里。
SQL与数据库层面的优化(性价比最高)
场景: 一个列表查询接口,数据量从100万涨到1000万后,接口响应时间从200ms飙升到10秒。
错误做法: 直接给所有字段加索引,或者在代码里用 Thread.sleep() 等“伪优化”。
正确落地步骤:
- 定位瓶颈(必须做): 使用 APM(如 SkyWalking、Pinpoint)或数据库慢查询日志,确认是
select * from order where user_id = ?这个SQL慢。 - 分析执行计划: 在数据库里
EXPLAIN这条SQL,发现type=ALL(全表扫描),rows=1000万。 - 制定方案:
- 加索引:
ALTER TABLE order ADD INDEX idx_user_id (user_id); - *避免 `SELECT
** 只查需要的字段SELECT id, amount, create_time FROM order`。 - 分页优化(如果涉及): 避免
LIMIT 1000000, 10,改用子查询或游标分页。
- 加索引:
- 验证效果: 再次执行
EXPLAIN,确认type=ref,rows降到个位数,压测接口,延迟从10秒降到10ms。
关键点: 落地不是写完代码就完事,一定要在预发布环境用压测数据验证索引是否被正确使用,很多“优化的代码”上线后,索引没命中,反而更慢。
JVM内存与GC优化(高并发必备)
场景: 一个网关服务,运行一周后CPU飙升100%,频繁Full GC。
错误做法: 无脑调大堆内存 -Xmx。
正确落地步骤:
- 获取现场(必须做):
jstat -gcutil <pid> 1000观察GC次数和时间,发现 Full GC 每秒都在发生。jmap -histo <pid> | head -20查看堆中对象,发现byte[]和com.example.UserCache占用了80%的内存。jstack <pid>查看线程栈,发现大量线程阻塞在ConcurrentHashMap的put方法上。
- 分析原因: 代码里有一个本地缓存
UserCache,用ConcurrentHashMap无限存储用户数据,没有过期策略,用户量增加导致缓存撑爆了年老代,触发 Full GC。 - 制定方案:
- 替换缓存组件: 使用 Guava Cache 或 Caffeine,并设置
maximumSize(10000)和expireAfterWrite(10, TimeUnit.MINUTES)。 - 调整JVM参数: 根据对象生命周期,调整新生代与老年代比例
-XX:NewRatio=2。
- 替换缓存组件: 使用 Guava Cache 或 Caffeine,并设置
- 验证效果: 运行72小时,监控
jstat,Full GC 次数从每5分钟一次降为0,CPU恢复正常。
关键点: 这种问题靠加机器没用,落地时需要把 jmap 导出的 dump 文件下载到本地,用 MAT 或 JProfiler 分析,定位到具体是哪一行代码创建了过多的 byte[]。
高并发下的锁与并发优化
场景: 一个秒杀系统的库存扣减接口,QPS 上到2000后,大量请求超时或返回“库存不足”。
错误做法: 在方法上加 synchronized 关键字,或者使用 ReentrantLock 锁住整个 doService 方法。
正确落地步骤:
-
定位瓶颈(必须做): 使用压测工具(如 JMeter)发现
tps上不去,通过火焰图(Async Profiler)发现AbstractQueuedSynchronizer.park占用CPU极高,说明锁竞争激烈。 -
分析代码:
// 旧的伪代码 public synchronized boolean deductStock(long skuId, int num) { int stock = getStockFromDB(skuId); if (stock < num) return false; updateStockToDB(skuId, stock - num); return true; }这段代码把整个方法同步了,导致大量线程排队等待数据库查询。
-
制定方案(渐进式):
-
第一版(立即见效): 将锁的粒度从方法级别降到库存ID级别。
private final Map<Long, Lock> lockMap = new ConcurrentHashMap<>(); public boolean deductStock(long skuId, int num) { Lock lock = lockMap.computeIfAbsent(skuId, k -> new ReentrantLock()); lock.lock(); try { int stock = getStockFromDB(skuId); if (stock < num) return false; updateStockToDB(skuId, stock - num); return true; } finally { lock.unlock(); } } -
第二版(终极方案): 将数据库行锁改为Redis原子操作。
// 使用 Redis Lua 脚本或者 Redisson 的 RLock public boolean deductStock(long skuId, int num) { String key = "stock:" + skuId; Long remain = redisTemplate.opsForValue().decrement(key, num); if (remain == null || remain < 0) { redisTemplate.opsForValue().increment(key, num); // 回滚 return false; } // 异步落库 asyncService.insertOrder(skuId, num); return true; }
-
-
验证效果: 用JMeter模拟2000并发,第一版QPS提升至8000,第二版提升至5万+,注意验证库存不能超卖。
关键点: 逐级优化,不要一开始就上Redis,先解决显性的锁竞争,再做架构级优化,落地时一定要写单元测试验证并发安全(如 CountDownLatch 模拟高并发)。
Java性能优化落地的通用方法论
| 步骤 | 核心动作 | 常用工具 | 检查点 |
|---|---|---|---|
| 定性 | 确认是CPU高、内存泄露、IO慢还是锁竞争? | top, vmstat, iostat |
系统资源 vs 业务逻辑 |
| 定位 | 找到最消耗资源的代码行 | Arthas, JProfiler, FlameGraph | 方法调用次数、耗时 |
| 定量 | 确定代码里的数据规模(深度、层数、集合大小) | jmap, MAT, 打印日志 |
某个 List 有多大? SQL 查了多少行? |
| 修改 | 制定最小改动方案(优先改代码,再调参数,最后换架构) | 代码Review | 改动是否可回滚? |
| 验证 | 在相同压测场景下对比优化前后的指标 | JMeter, LoadRunner | 成功率、TP99、GC次数 |
实践建议: 下次你想优化一段代码,先不要改,在代码里打印一下关键路径的时间戳:
long start = System.nanoTime();
// ... 你的业务逻辑
long end = System.nanoTime();
if (end - start > 100_000_000) { // 超过100ms 记录
log.warn("慢操作:{} 耗时 {} ms", Thread.currentThread().getStackTrace()[2], (end - start) / 1_000_000);
}
定位到热点,再动手。 这是Java性能优化落地最核心的认知。