在 Java 应用性能优化中,代码层面的优化是最基础也最有效的手段。良好的编码习惯能从源头减少性能损耗,避免 “千里之堤溃于蚁穴” 的情况。本文将围绕 5 个核心方向,通过真实案例和代码对比,详解代码优化的实战技巧。
一、避免频繁创建对象:让对象 “循环使用”
Java 中的对象创建和垃圾回收都会消耗系统资源,尤其是频繁创建大对象或短生命周期对象,会导致 GC 频繁触发,严重影响性能。
问题现场
在处理用户请求的接口中,有这样一段代码:
// 每次请求都创建新的JSON解析器
@RequestMapping("/user")
public UserDTO getUser(Long id) {
// 问题:每次调用都创建新的ObjectMapper实例
ObjectMapper mapper = new ObjectMapper();
User user = userService.getById(id);
return mapper.convertValue(user, UserDTO.class);
}
当接口 QPS 达到 1000 时,每秒会创建 1000 个 ObjectMapper 对象,这些对象使用后立即被回收,导致 Young GC 频繁触发。
优化方案:对象池复用
将频繁创建的对象放入对象池管理,通过复用减少创建和回收成本:
// 1. 创建对象池
public class ObjectMapperPool {
private static final GenericObjectPool\<ObjectMapper> pool;
static {
// 配置池参数
GenericObjectPoolConfig\<ObjectMapper> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(50); // 最大对象数
config.setMinIdle(5); // 最小空闲对象
pool = new GenericObjectPool<>(new BasePooledObjectFactory\<ObjectMapper>() {
@Override
public ObjectMapper create() {
return new ObjectMapper();
}
@Override
public PooledObject\<ObjectMapper> wrap(ObjectMapper obj) {
return new DefaultPooledObject<>(obj);
}
}, config);
}
// 获取对象
public static ObjectMapper borrowObject() throws Exception {
return pool.borrowObject();
}
// 归还对象
public static void returnObject(ObjectMapper mapper) {
pool.returnObject(mapper);
}
}
// 2. 优化后的接口
@RequestMapping("/user")
public UserDTO getUser(Long id) throws Exception {
ObjectMapper mapper = ObjectMapperPool.borrowObject();
try {
User user = userService.getById(id);
return mapper.convertValue(user, UserDTO.class);
} finally {
ObjectMapperPool.returnObject(mapper); // 归还对象到池
}
}
实战效果
通过压测对比,优化后接口的:
-
Young GC 频率从每秒 5 次降低到每秒 1 次
-
接口平均响应时间从 28ms 减少到 15ms
-
系统 CPU 使用率降低约 30%
适用场景:数据库连接、线程、解析器等创建成本高的对象,注意结合业务设置合理的池大小。
二、合理使用数据结构:选对工具事半功倍
Java 集合框架提供了丰富的数据结构,选择合适的数据结构能显著提升操作效率,反之则可能成为性能瓶颈。
ArrayList vs LinkedList:场景决定选择
-
ArrayList:基于数组实现,随机访问快(O (1)),增删末尾元素快,但中间插入删除慢(O (n))
-
LinkedList:基于双向链表,随机访问慢(O (n)),但中间插入删除快(O (1))
反例:在需要频繁随机访问的场景使用 LinkedList
// 问题代码:频繁随机访问却用LinkedList
List\<User> users = new LinkedList<>();
// 初始化10000个用户
for (int i = 0; i < 10000; i++) {
users.add(new User(i, "user" + i));
}
// 频繁随机访问导致性能低下
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
int randomIndex = new Random().nextInt(10000);
User user = users.get(randomIndex); // LinkedList的get是O(n)操作
}
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
// 输出:耗时:286ms
优化:替换为 ArrayList
List\<User> users = new ArrayList<>();
// 初始化代码同上...
long start = System.currentTimeMillis();
// 同样的访问操作
for (int i = 0; i < 1000; i++) {
int randomIndex = new Random().nextInt(10000);
User user = users.get(randomIndex); // ArrayList的get是O(1)操作
}
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
// 输出:耗时:3ms
HashMap 初始容量设置:避免频繁扩容
HashMap 默认初始容量为 16,负载因子 0.75,当元素数量达到容量 × 负载因子时会触发扩容(rehash),这是耗时操作。
优化方案:根据预期元素数量设置初始容量
// 预期存储1000个元素
int expectedSize = 1000;
// 计算初始容量:expectedSize / 0.75 + 1
int initialCapacity = (int) (expectedSize / 0.75 + 1);
Map\<String, User> userMap = new HashMap<>(initialCapacity);
// 避免了扩容操作,性能提升约20%
三、减少同步开销:无锁编程提升并发效率
过多的同步操作会导致线程阻塞和上下文切换,合理使用无锁并发类能显著提升并发场景下的性能。
问题场景:过度使用 synchronized
// 问题代码:全局锁导致并发瓶颈
public class Counter {
private int count = 0;
// 全局锁导致所有线程排队
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
优化方案:使用原子类和并发集合
// 1. 计数器优化:使用AtomicInteger
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
// 无锁操作,性能提升显著
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
// 2. 集合优化:使用ConcurrentHashMap替代HashMap+synchronized
// 问题代码
Map\<String, Integer> map = new HashMap<>();
public synchronized void add(String key) {
map.put(key, map.getOrDefault(key, 0) + 1);
}
// 优化后
Map\<String, Integer> map = new ConcurrentHashMap<>();
public void add(String key) {
// 原子操作,无需显式同步
map.merge(key, 1, Integer::sum);
}
性能对比
在 100 线程并发场景下:
-
synchronized 计数器:每秒执行约 50 万次操作
-
AtomicInteger 计数器:每秒执行约 280 万次操作
-
HashMap+synchronized:每秒执行约 30 万次 put 操作
-
ConcurrentHashMap:每秒执行约 220 万次 put 操作
四、避免过度装箱拆箱:减少隐形的性能损耗
Java 的自动装箱(int→Integer)和拆箱(Integer→int)会创建临时对象,在循环等高频场景中累积的性能损耗不容忽视。
问题代码:循环中的自动装箱
// 问题:循环中频繁自动装箱创建Integer对象
long start = System.currentTimeMillis();
Integer sum = 0; // 声明为Integer类型
for (int i = 0; i < 1000000; i++) {
sum += i; // 每次计算都会触发拆箱和装箱
}
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
// 输出:耗时:48ms
优化方案:使用基本数据类型
long start = System.currentTimeMillis();
int sum = 0; // 使用基本数据类型int
for (int i = 0; i < 1000000; i++) {
sum += i; // 直接进行数值计算,无装箱拆箱
}
System.out.println("耗时:" + (System.currentTimeMillis() - start) + "ms");
// 输出:耗时:3ms
进阶优化:集合中的类型选择
避免使用List<Integer>
存储大量整数,可使用专门的基本类型集合库:
// 使用Eclipse Collections替代JDK集合
IntList intList = IntLists.mutable.empty();
for (int i = 0; i < 100000; i++) {
intList.add(i); // 直接存储int,无装箱
}
五、优化循环逻辑:减少循环内的 “负重前行”
循环是代码执行的高频场景,将耗时操作移出循环、减少循环次数,能显著提升整体性能。
问题代码:循环内的重复计算和 IO
// 问题:循环内频繁计算和IO操作
public List\<UserVO> convertUsers(List\<User> users) {
List\<UserVO> result = new ArrayList<>();
for (User user : users) {
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setName(user.getName());
// 问题1:循环内重复计算
vo.setAge(calculateAge(user.getBirthday()));
// 问题2:循环内频繁IO操作
vo.setAvatarUrl(getAvatarUrlFromOSS(user.getId()));
result.add(vo);
}
return result;
}
优化方案:批量处理 + 预计算
public List\<UserVO> convertUsers(List\<User> users) {
List\<UserVO> result = new ArrayList<>(users.size()); // 预设容量
// 优化1:预计算公共参数
LocalDate today = LocalDate.now(); // 移到循环外计算一次
// 优化2:批量获取IO资源
List\<Long> userIds = users.stream()
.map(User::getId)
.collect(Collectors.toList());
Map\<Long, String> avatarMap = batchGetAvatarUrlsFromOSS(userIds); // 批量IO
for (User user : users) {
UserVO vo = new UserVO();
vo.setId(user.getId());
vo.setName(user.getName());
// 使用预计算结果
vo.setAge(calculateAge(user.getBirthday(), today));
// 使用批量获取的结果
vo.setAvatarUrl(avatarMap.get(user.getId()));
result.add(vo);
}
return result;
}
循环次数优化:倒序循环与边界缓存
// 优化前:每次循环都获取size()
for (int i = 0; i < list.size(); i++) {
process(list.get(i));
}
// 优化后:缓存边界值
for (int i = 0, size = list.size(); i < size; i++) {
process(list.get(i));
}
总结:代码优化的核心原则
代码层面的性能优化并非盲目追求技巧,而是遵循 “测量 - 分析 - 优化 - 验证” 的闭环:
-
先通过 Profiler 工具找到性能瓶颈,避免 “优化不存在的问题”
-
优先优化高频执行的代码(如循环、核心接口)
-
兼顾可读性和性能,避免过度优化导致代码维护困难
-
所有优化都需要通过压测验证实际效果
记住:最好的性能优化是写出简洁、清晰、符合 Java 最佳实践的代码。当基础编码规范做好后,再结合业务场景进行针对性优化,才能真正实现 “既快又稳” 的 Java 应用。