Java 代码性能优化的 11 个实用技巧
前言
在开发任何 Java 应用时,性能优化(Optimization)都是不可忽视的核心议题。随着业务量的增长,低效的代码会导致响应延迟增加、吞吐量下降以及服务器资源成本上升。作为开发者,除了保证代码的整洁与无缺陷外,必须时刻关注性能问题。
本文总结了 11 个在实际开发中高频使用的 Java 代码性能优化技巧,涵盖内存管理、循环结构、字符串处理、数据库交互等多个维度。理解这些原理并应用到实践中,能显著提升系统的整体表现。
1. 避免方法过长
问题描述:
当一个方法体过大时,不仅难以维护,还会影响 JVM 的执行效率。
技术原理:
从维护角度看,单一职责原则要求方法功能明确,过长的方法通常意味着逻辑耦合度过高,增加了阅读和调试的难度。从性能角度看,JVM 在加载类和方法时,会将方法字节码加载到内存中。如果方法体过大,可能导致局部变量表膨胀,增加栈帧的大小,进而影响 CPU 缓存命中率。此外,过大的方法可能阻碍 JIT 编译器进行内联优化(Inlining),导致无法生成最优机器码。
优化建议:
将长方法拆分为多个小方法,提取公共逻辑。这不仅能提高可读性,还能让 JVM 更好地对热点方法进行编译优化。
2. 避免深层嵌套的 if-else 语句
问题描述:
在业务逻辑复杂时,开发者容易写出多层嵌套的 if-else 结构,甚至出现'箭头型'代码。
技术原理:
虽然现代 JVM 对分支预测做了大量优化,但深层嵌套的条件判断依然会增加 CPU 流水线停顿的风险。特别是在循环内部使用复杂的条件判断,会显著增加比较指令的数量。如果条件组合固定,使用 switch-case 或策略模式往往比多层 if-else 更高效,因为 switch 在某些情况下可以转换为查找表(Lookup Table)或跳转表(Jump Table),减少比较次数。
优化建议:
- 将条件分组,先计算布尔结果再判断。
- 优先使用 switch 替代多重 if-else。
- 考虑使用卫语句(Guard Clauses)提前返回,减少嵌套层级。
if (condition1) {
if (condition2) {
if (condition3 || condition4) {
execute();
} else {
handleElse();
}
}
}
boolean result = condition1 && condition2 && (condition3 || condition4);
if (result) {
execute();
} else {
handleElse();
}
3. 谨慎使用 foreach 遍历集合
问题描述:
Java 5 引入的增强 for 循环(foreach)语法简洁,但在特定场景下存在性能开销。
技术原理:
foreach 语法糖在编译后通常会转换为 Iterator 迭代器模式。对于实现了 Iterable 接口的集合,每次进入循环都会创建一个新的 Iterator 对象。虽然这个对象很小,但在高频调用的循环中,频繁的短生命周期对象分配会给 Eden 区带来 GC 压力,触发 Minor GC 的频率增加。
优化建议:
如果对性能有极致追求,或者在极高频的循环中,建议使用传统的索引式 for 循环,避免 Iterator 对象的重复创建。注意确保集合在遍历过程中不被修改。
int size = strs.size();
for (int i = 0; i < size; i++) {
String value = strs.get(i);
}
4. 避免在循环中获取集合大小
问题描述:
在 for 循环的条件判断中直接调用 list.size() 是常见的性能陷阱。
技术原理:
虽然大多数 List 实现(如 ArrayList)的 size() 方法是 O(1) 操作,但在某些特殊实现或并发场景下,它可能涉及锁竞争或计算开销。更重要的是,在循环条件中反复调用该方法会产生不必要的指令开销。将大小提取到循环外部,可以减少字节码中的方法调用指令数量。
优化建议:
在循环开始前,将集合大小赋值给一个局部变量。
List<String> objList = getData();
int size = objList.size();
for (int i = 0; i < size; i++) {
}
5. 避免使用 + 号拼接字符串
问题描述:
在 JDK 5 之前,+ 号拼接字符串会创建大量临时对象。虽然 JDK 9+ 引入了 StringConcatFactory 优化,但在循环中仍需谨慎。
技术原理:
String 是不可变类(final class)。每次使用 + 拼接,都会创建一个新的 String 对象。如果在循环中进行拼接,会产生大量的垃圾对象,导致堆内存快速消耗并频繁触发 Full GC。尽管编译器会自动将 + 优化为 StringBuilder,但在循环中如果不指定初始容量,StringBuilder 内部的数组可能需要多次扩容,造成额外的内存拷贝。
优化建议:
在循环拼接字符串时,显式使用 StringBuilder 并预设初始容量,避免扩容开销。
StringBuilder stringBuilder = new StringBuilder("sample");
for (int i = 0; i < count; i++) {
stringBuilder.append("-");
stringBuilder.append(i);
}
String str = stringBuilder.toString();
6. 尽可能使用基本类型
问题描述:
在定义变量或数组时,盲目使用包装类(Wrapper Class)而非基本类型。
技术原理:
基本类型(int, double 等)存储在栈内存中,访问速度快且无额外开销。而包装类(Integer, Double 等)是对象,存储在堆内存中,包含对象头信息。更关键的是,基本类型与包装类之间的自动装箱(Boxing)和拆箱(Unboxing)操作会创建新的对象实例,增加 GC 负担。在大数据量计算或高频循环中,这种差异会被放大。
优化建议:
除非需要 null 语义或泛型支持,否则优先使用基本类型。
int count = 0;
Integer countObj = 0;
7. 避免过度使用 BigDecimal 类
问题描述:
为了精度使用 BigDecimal 是必要的,但不应滥用。
技术原理:
BigDecimal 提供了任意精度的小数运算,但其底层实现基于 BigInteger,计算过程涉及对象创建、数组拷贝和复杂的算法逻辑。相比 long 或 double,BigDecimal 的计算开销大得多,内存占用也更高。如果业务允许一定的精度误差,或者数值范围在 long/double 的安全范围内,应优先使用基本类型。
优化建议:
仅在金融计算等对精度要求极高的场景使用 BigDecimal。普通数值计算优先使用 double 或 long。
8. 避免频繁创建'代价昂贵'的对象
问题描述:
某些对象创建成本高,如数据库连接、系统配置对象、会话对象等。
技术原理:
这些对象通常涉及 I/O 操作、网络握手或复杂的初始化逻辑。每次请求都重新创建它们会严重拖慢系统响应速度,并耗尽系统资源(如文件句柄、线程池)。
优化建议:
采用单例模式(Singleton)或对象池(Object Pool)机制来复用这些对象。例如,使用连接池(HikariCP)管理数据库连接,使用静态常量存储配置信息。
9. 使用 PreparedStatement 代替 Statement
问题描述:
在使用 JDBC 进行 SQL 查询时,错误地使用了 Statement。
技术原理:
Statement 对象在每次执行 SQL 时都需要由数据库解析、编译和执行。而 PreparedStatement 会在第一次执行时预编译 SQL 语句,后续执行只需传入参数。这不仅减少了数据库端的解析开销,提高了执行效率,还能有效防止 SQL 注入攻击。
优化建议:
始终使用 PreparedStatement 处理动态参数查询。
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, userId);
ResultSet rs = pstmt.executeQuery();
10. 避免不必要的日志语句和不正确的日志级别
问题描述:
在代码中随意打印日志,尤其是未检查日志级别就直接构造日志消息。
技术原理:
即使当前日志级别设置为 INFO,如果代码中写的是 log.debug(...),JVM 依然会执行字符串拼接或格式化操作来构建日志内容,然后再被 Logger 丢弃。如果日志内容涉及复杂计算或对象序列化,这部分开销是实打实的浪费。此外,过多的 DEBUG 日志在运行期也会占用磁盘 IO。
优化建议:
在记录日志前,先检查日志级别是否开启。使用 SLF4J 等框架提供的占位符功能,避免手动拼接字符串。
log.debug("User [{}] called method X with [{}]", userName, i);
if (log.isDebugEnabled()) {
log.debug("User [{}...]", expensiveComputation());
}
11. 选择 SQL 查询中的必要字段
问题描述:
在编写 SQL 查询时,习惯性使用 SELECT *。
技术原理:
SELECT * 会读取表中所有列的数据,包括不需要的字段。这会导致以下问题:
- 网络传输量大:数据库返回的数据包变大,增加网络延迟。
- 内存占用高:应用程序需要反序列化更多数据。
- 索引失效风险:有时全表扫描比索引查询更慢。
- 架构耦合:如果表结构变更(如新增大字段),查询性能会受影响。
优化建议:
明确指定需要的列名,只查询业务所需的最小数据集。
SELECT book_title, book_desc, book_price
FROM books
WHERE book_id = 6;
SELECT * FROM books WHERE book_id = 6;
结语
性能优化是一个系统工程,不能仅依赖上述技巧。开发者应遵循'测量优先'的原则,使用 APM 工具(如 Arthas, JProfiler)定位瓶颈后再进行针对性优化。盲目优化不仅浪费时间,还可能降低代码的可读性和可维护性。通过掌握上述 11 个基础技巧,结合合理的架构设计,可以有效提升 Java 应用的性能表现。
补充建议:性能测试与监控
在进行优化后,务必进行回归测试,确保功能正常且性能指标有所提升。同时,建立完善的监控体系,实时跟踪应用的 CPU、内存、GC 频率及接口响应时间,以便及时发现潜在的性能退化问题。