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 本章知识体系

下面这张图总结了代码级性能优化的核心脉络:

代码级性能优化知识体系 代码级性能优化 热点代码识别 (Profiling) 算法优化 数据结构选择 减少对象创建 避免过度同步 火焰图 async-profiler JFR / perf O(n²) → O(n log n) 优先队列 HashMap / TreeMap ConcurrentHashMap 对象池化 StringBuilder ReadWriteLock 无锁数据结构

这张图把五个核心方向串起来了。从上到下,先识别热点,再针对性地优化算法、数据结构、对象创建和同步策略。每一步都有对应的工具和技巧。

总结一句话:代码级优化不是靠猜,而是靠 profiler 说话。先找到热点,再选对算法和数据结构,最后用对象池和细粒度锁收尾。这套流程我在多个项目里验证过,效果稳定。

专注资料整理