4、代码级性能优化:热点代码识别(Profiling)、算法优化、数据结构选择、减少对象创建、避免过度同步
代码级优化,说白了就是「把每一行代码都榨出油水来」。很多团队喜欢上来就搞架构重构,我个人习惯是——先拿 profiler 跑一遍,看看 CPU 到底在忙什么。你想想看,如果 90% 的时间都花在一个循环里,你优化其他 90% 的代码又有什么用?
4.1 热点代码识别:别猜,要测
我见过太多人凭直觉优化代码。「我觉得这里慢」、「我觉得那里有性能问题」——嗯,直觉在性能调优里基本靠不住。我在项目中遇到过一位同事,花了两周优化一个他认为很慢的排序算法,结果 profiler 一跑,那个排序只占了总耗时的 0.3%。
热点识别的核心工具就是 Profiling。常用的有:
- CPU Profiling:看哪个函数吃 CPU 最多
- Allocation Profiling:看哪个地方疯狂创建对象
- Lock Profiling:看锁竞争有多严重
我的经验法则:先跑采样模式(Sampling),快速定位热点。再跑插桩模式(Instrumentation),精确分析热点函数内部。别一上来就全量插桩,那会让程序慢 10 倍以上。
举个例子,在 Java 中我常用 async-profiler:
# 采样 CPU 热点,持续 30 秒
./profiler.sh -e cpu -d 30 -f flamegraph.html <pid>
# 采样分配热点
./profiler.sh -e alloc -d 30 -f alloc.html <pid>
火焰图怎么看?「宽」就是热,「高」就是深。如果一个函数顶部很宽,说明它本身很耗时;如果整个调用栈很高,说明调用层级太多,可以考虑内联或扁平化。
小技巧:我习惯把火焰图导出成 SVG,然后直接在浏览器里搜索函数名。比如搜 "parse"、"format"、"lock" 这些关键词,往往能快速找到问题点。
4.2 算法优化:选对算法,事半功倍
算法优化是性价比最高的优化手段。你想想看,把 O(n²) 改成 O(n log n),数据量一大,效果立竿见影。我在报价路由系统里遇到过最典型的例子——匹配引擎里的价格排序。
一开始用的是冒泡排序(别笑,真的有人这么写),后来改成 TimSort(Java 默认排序),再后来发现其实只需要维护 Top-K 个最优价格,于是换成了优先队列。性能提升了 40 倍。
| 场景 | 推荐算法 | 时间复杂度 | 我踩过的坑 |
|---|---|---|---|
| 价格排序(全量) | 快速排序 / TimSort | O(n log n) | 递归太深导致栈溢出 |
| Top-K 最优报价 | 优先队列(堆) | O(n log k) | 忘记处理相等价格 |
| 订单匹配 | 红黑树 / 跳表 | O(log n) | 跳表内存开销大 |
| 频率统计 | 哈希表 + 计数 | O(1) 平均 | 哈希冲突导致退化 |
注意:算法优化不是越复杂越好。我曾经把一段简单的线性查找改成二分查找,结果数据量只有几十个,二分查找的常数开销反而更大。先测数据规模,再选算法。
4.3 数据结构选择:选错结构,性能腰斩
数据结构选错了,神仙也救不了。我举个例子——报价路由系统里需要频繁地按价格区间查询订单。有人用了 ArrayList,每次查询都遍历一遍。数据量一上来,CPU 直接飙到 100%。
后来改成 TreeMap(红黑树),按价格排序存储,查询区间时用 subMap() 方法,复杂度从 O(n) 降到 O(log n + k)。
常见的数据结构选择建议:
- 频繁按 key 查询:HashMap / ConcurrentHashMap(注意初始容量和负载因子)
- 需要有序遍历:TreeMap / ConcurrentSkipListMap
- 频繁插入删除:LinkedList(头尾操作) / ArrayDeque(双端操作)
- 需要去重:HashSet / TreeSet
- 缓存场景:LinkedHashMap(LRU 实现) / Caffeine
我的一个教训:曾经在并发场景下用了 HashMap,结果死循环了。嗯,HashMap 在并发扩容时会形成环形链表,CPU 直接打满。后来改成 ConcurrentHashMap,问题解决。
4.4 减少对象创建:GC 是性能杀手
对象创建多了,GC 就频繁。GC 一频繁,STW(Stop-The-World)就来了。在报价路由这种低延迟系统里,一次 GC 暂停可能意味着错过一笔交易。
减少对象创建的核心思路:
- 对象池化:复用对象,避免频繁 new。比如 Netty 的 ByteBuf 池、连接池
- 基本类型替代包装类型:int 代替 Integer,long 代替 Long
- StringBuilder 代替字符串拼接:循环里用 + 拼接字符串,每次都会创建新对象
- 避免在热点路径创建临时对象:比如在循环里 new 对象
// 不好的写法:每次循环都创建对象
for (int i = 0; i < 100000; i++) {
Order order = new Order();
order.setPrice(prices[i]);
// ...
}
// 好的写法:复用对象
Order order = new Order();
for (int i = 0; i < 100000; i++) {
order.setPrice(prices[i]);
// 注意:如果 order 需要被持久化,这里要 clone 或 copy
}
小技巧:我习惯在代码里加一个「对象创建计数器」,用 profiler 看每秒创建了多少对象。如果超过 10 万/秒,就要警惕了。
4.5 避免过度同步:锁是并发的敌人
同步是必要的,但过度同步就是性能毒药。我见过一个系统,几乎每个方法都加了 synchronized,结果并发量一上来,所有线程都在等锁,性能还不如单线程。
避免过度同步的几个原则:
- 缩小锁范围:只锁必要的代码块,不要锁整个方法
- 读写分离:读多写少的场景用 ReadWriteLock 或 StampedLock
- 无锁数据结构:AtomicInteger、ConcurrentHashMap、LongAdder
- ThreadLocal:线程私有数据,不需要同步
// 不好的写法:锁了整个方法
public synchronized void updatePrice(String symbol, double price) {
// 读操作
OrderBook book = orderBooks.get(symbol);
// 写操作
book.update(price);
}
// 好的写法:只锁写操作
public void updatePrice(String symbol, double price) {
OrderBook book = orderBooks.get(symbol); // 读操作不需要锁
synchronized (book) { // 只锁 book 对象
book.update(price);
}
}
我曾经踩过的坑:用 synchronized 锁了一个 String 常量,结果不同业务线用了同一个字符串常量池里的对象,导致互相阻塞。后来改用 new Object() 作为锁对象,问题解决。
4.6 本章知识体系
下面这张图总结了代码级性能优化的核心脉络:
这张图把五个核心方向串起来了。从上到下,先识别热点,再针对性地优化算法、数据结构、对象创建和同步策略。每一步都有对应的工具和技巧。
总结一句话:代码级优化不是靠猜,而是靠 profiler 说话。先找到热点,再选对算法和数据结构,最后用对象池和细粒度锁收尾。这套流程我在多个项目里验证过,效果稳定。