mcpField
P
performance-smell-detection

performance-smell-detection

github.com/decebals测试与质量761318 次安装7 周前更新
代码仓库

关于 performance-smell-detection

Performance Smell Detection 面向审查代码或排查已测得性能问题的 Java 开发者。它指出潜在的代码级瓶颈,并强调先测量、兼顾可读性,避免过早优化。

主要功能

  1. 01审查 Java 性能模式
  2. 02标记潜在代码级瓶颈
  3. 03涵盖字符串、流和集合
  4. 04分析装箱与正则开销
  5. 05强调优化前先分析测量

适用场景

  1. 01审查性能关键的 Java 路径
  2. 02排查已测得的 Java 性能下降
  3. 03了解 Java 性能取舍

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/decebals/claude-code-java --skill performance-smell-detection

安装后在 Claude Code 中输入/performance-smell-detection确认已加载。

附带文件README.md

详细介绍

性能代码异味检测技能

识别 Java 代码中潜在的代码级性能问题。

理念

“过早优化是万恶之源”——Donald Knuth

此技能旨在帮助你发现潜在的性能异味,而不是盲目地“修复”它们。现代 JVM(Java 21/25)已经高度优化。请始终:

  1. 先测量——使用 JMH、性能分析器或生产环境指标
  2. 关注热点路径——90% 的时间花在 10% 的代码上
  3. 考虑可读性——清晰的代码往往比微优化更重要

适用场景

  • 审查对性能要求很高的代码路径
  • 调查已测出的性能问题
  • 学习 Java 性能模式
  • 关注性能的代码审查

范围

此技能: 代码级性能(流、集合、对象) 数据库相关: 使用 jpa-patterns 技能(N+1、延迟加载、分页) 架构相关: 使用 architecture-review 技能


快速参考:潜在异味

异味严重程度上下文
在循环中编译正则表达式🔴 高通常值得修复
在循环中拼接字符串🟡 中在 Java 21/25 中仍然适用
在紧密循环中使用流🟡 中取决于集合大小
热点路径中的装箱🟡 中先测量
无界集合🔴 高内存风险
未设置集合容量🟢 低影响较小,仅在关键场景下测量

字符串操作(Java 9+ / 21 / 25)

变化内容

从 Java 9(JEP 280)开始,使用 + 拼接字符串时会使用 invokedynamic,而不是 StringBuilder。JVM 能很好地优化简单的字符串拼接。

Java 25 为 String 键的 Map 查找增加了 String::hashCode 常量折叠优化。

仍然适用:在循环中使用 StringBuilder
// 🔴 Still problematic - new String each iteration
String result = "";
for (String s : items) {
    result += s;  // O(n²) - creates n strings
}

// ✅ StringBuilder for loops
StringBuilder sb = new StringBuilder();
for (String s : items) {
    sb.append(s);
}
String result = sb.toString();

// ✅ Or use String.join / Collectors.joining
String result = String.join("", items);
现在没问题:简单拼接
// ✅ Fine in Java 9+ - JVM optimizes this
String message = "User " + name + " logged in at " + timestamp;

// ✅ Also fine
return "Error: " + code + " - " + description;
在热点路径中避免:String.format
// 🟡 String.format has parsing overhead
log.debug(String.format("Processing %s with id %d", name, id));

// ✅ Parameterized logging (SLF4J)
log.debug("Processing {} with id {}", name, id);

Stream API(审慎看待)

实际情况

流有开销,但通常可以接受:

  • 少于 100 个元素:流可能慢 2–5 倍(但耗时仍以微秒计)
  • 1K–10K 个元素:差距会显著缩小
  • 超过 10K 个元素:性能通常与循环相差不超过 50%
  • GraalVM:可以将流优化到与循环性能相当

建议:优先使用流以提高可读性。只有在性能分析表明存在瓶颈时,才改用循环进行优化。

流可能造成问题的场景
// 🔴 Stream created per iteration in hot loop
for (int i = 0; i < 1_000_000; i++) {
    boolean found = items.stream()
        .anyMatch(item -> item.getId() == i);
}

// ✅ Pre-compute lookup structure
Set<Integer> itemIds = items.stream()
    .map(Item::getId)
    .collect(Collectors.toSet());

for (int i = 0; i < 1_000_000; i++) {
    boolean found = itemIds.contains(i);
}
使用流没有问题的场景
// ✅ Single pass, readable, not in tight loop
List<String> names = users.stream()
    .filter(User::isActive)
    .map(User::getName)
    .sorted()
    .collect(Collectors.toList());

// ✅ Primitive streams avoid boxing
int sum = numbers.stream()
    .mapToInt(Integer::intValue)
    .sum();
并行流:谨慎使用
// 🔴 Parallel on small collection - overhead > benefit
smallList.parallelStream().map(...);  // < 10K items

// 🔴 Parallel with shared mutable state
List<String> results = new ArrayList<>();
items.parallelStream()
    .forEach(results::add);  // Race condition!

// ✅ Parallel for CPU-intensive + large collections
List<Result> results = largeDataset.parallelStream()  // > 10K items
    .map(this::expensiveCpuComputation)
    .collect(Collectors.toList());

装箱/拆箱

仍然是实际问题

装箱会在堆上创建对象,增加 GC 压力。JVM 会缓存小值(-128 到 127),但不会缓存更大的值。

未来:Project Valhalla 将大幅改善这一点。

// 🔴 Boxing in tight loop - creates millions of objects
Long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
    sum += i;  // Unbox, add, box
}

// ✅ Primitive
long sum = 0L;
for (int i = 0; i < 1_000_000; i++) {
    sum += i;
}
使用基本类型流
// 🟡 Boxing overhead
int sum = list.stream()
    .reduce(0, Integer::sum);

// ✅ Primitive stream
int sum = list.stream()
    .mapToInt(Integer::intValue)
    .sum();

正则表达式

始终在循环外预编译

这条建议并未过时——Pattern.compile 的开销很大。

// 🔴 Compiles pattern every iteration
for (String input : inputs) {
    if (input.matches("\\d{3}-\\d{4}")) {  // Compiles regex!
        process(input);
    }
}

// ✅ Pre-compile
private static final Pattern PHONE = Pattern.compile("\\d{3}-\\d{4}");

for (String input : inputs) {
    if (PHONE.matcher(input).matches()) {
        process(input);
    }
}

集合

容量提示(轻微优化)
// 🟢 Low severity - but free optimization if size known
List<User> users = new ArrayList<>(expectedSize);
Map<String, User> map = new HashMap<>(expectedSize * 4 / 3 + 1);
根据用途选择合适的集合
// 🟡 O(n) lookup in loop
List<String> allowed = getAllowed();
for (Request r : requests) {
    if (allowed.contains(r.getId())) { }  // O(n) each time
}

// ✅ O(1) lookup
Set<String> allowed = new HashSet<>(getAllowed());
for (Request r : requests) {
    if (allowed.contains(r.getId())) { }  // O(1)
}
无界集合
// 🔴 Memory risk - could grow unbounded
@GetMapping("/users")
public List<User> getAllUsers() {
    return userRepository.findAll();  // Millions of rows?
}

// ✅ Pagination
@GetMapping("/users")
public Page<User> getUsers(Pageable pageable) {
    return userRepository.findAll(pageable);
}

现代 Java(21/25)模式

使用虚拟线程处理 I/O(Java 21+)
// 🟡 Traditional thread pool for I/O - wastes OS threads
ExecutorService executor = Executors.newFixedThreadPool(100);
for (Request request : requests) {
    executor.submit(() -> callExternalApi(request));  // Blocks OS thread
}

// ✅ Virtual threads - millions of concurrent I/O operations
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (Request request : requests) {
        executor.submit(() -> callExternalApi(request));
    }
}
结构化并发(Java 21+ 预览版)
// ✅ Structured concurrency for parallel I/O
try (StructuredTaskScope.ShutdownOnFailure scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<User> user = scope.fork(() -> fetchUser(id));
    Future<Orders> orders = scope.fork(() -> fetchOrders(id));

    scope.join();
    scope.throwIfFailed();

    return new UserProfile(user.resultNow(), orders.resultNow());
}

性能审查检查清单

🔴 高严重程度(通常值得修复)
  • 在循环中调用 Regex Pattern.compile
  • 未分页的无界查询
  • 在循环中拼接字符串(StringBuilder 仍然适用)
  • 使用共享可变状态的并行流
🟡 中严重程度(先测量)
  • 在紧密循环中使用流(迭代次数 >100K)
  • 热点路径中的装箱
  • 在循环中调用 List.contains()(改用 Set)
  • 使用传统线程处理 I/O(考虑使用虚拟线程)
🟢 低严重程度(有益但非必需)
  • 设置集合初始容量
  • 轻微优化流
  • toArray(new T[0]) 与 toArray(new T[size]) 的选择

何时不应优化

  • 不是热点路径 - 初始化代码、配置、管理端点
  • 没有经过测量的问题 - “看起来很慢”并不算测量结果
  • 可读性变差 - 清晰的代码优于微优化
  • 集合较小 - 即使处理 100 个项目,也只需几微秒

分析命令

# Find regex in loops (potential compile overhead)
grep -rn "\.matches(\|\.split(" --include="*.java"

# Find potential boxing (Long/Integer as variables)
grep -rn "Long\s\|Integer\s\|Double\s" --include="*.java" | grep "= 0\|+="

# Find ArrayList without capacity
grep -rn "new ArrayList<>()" --include="*.java"

# Find findAll without pagination
grep -rn "findAll()" --include="*.java"

评论

支持 Markdown