Java 代码优化技巧:从"能跑就行"到"丝滑如飞"的进阶之路

同样的业务逻辑,有人写出来 QPS 5000,有人写出来 QPS 500。差距不在硬件,在代码


一、先立观念:优化的三条铁律

铁律 说明
🚫 不要过早优化 先保证正确,再追求性能。没有数据支撑的优化都是自嗨
📊 先测量,再优化 用 JProfiler / Arthas 找瓶颈,别凭感觉瞎改
🎯 优化热点代码 80% 的性能问题集中在 20% 的代码里,抓大放小

💡 记住:可读性 > 可维护性 > 性能。 除非性能是核心指标,否则别为了快 5ms 牺牲代码清晰度。


二、String 字符串:99% 的人都在浪费内存

❌ 反模式:循环中用 + 拼接


java

// 每次循环都创建新的 StringBuilder 对象,性能灾难!
String result = "";
for (int i = 0; i < 10000; i++) {
    result += i;  // 实际等价于:result = new StringBuilder().append(result).append(i).toString()
}

✅ 正确姿势


java

// 方式一:StringBuilder(单线程首选)
StringBuilder sb = new StringBuilder(10000);  // 预分配容量,避免扩容
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}
String result = sb.toString();

// 方式二:String.join()(拼接集合)
String result = String.join(",", list);

// 方式三:String.format()(少量拼接,可读性优先)
String msg = String.format("User %s logged in at %s", name, time);

🔥 进阶:intern() 减少重复字符串内存


java

// 大量重复字符串场景(如状态码、枚举值),intern() 让它们共用同一块内存
String status1 = "SUCCESS".intern();
String status2 = "SUCCESS".intern();
System.out.println(status1 == status2);  // true,指向同一对象

三、集合操作:选对数据结构,性能差 100 倍

📊 核心选择指南

场景 推荐 避免 原因
随机访问 ArrayList LinkedList ArrayList O(1) vs LinkedList O(n)
频繁插入删除 LinkedList ArrayList LinkedList O(1) vs ArrayList O(n)
查找元素 HashSet / HashMap ArrayList.contains() O(1) vs O(n)
有序 + 去重 TreeSet HashSet 自动排序
线程安全 ConcurrentHashMap Hashtable 锁粒度更细,并发性能高 10 倍

❌ 反模式:用 ArrayList 做大量 contains 查询


java

// ❌ 灾难级写法:O(n²) 复杂度
List<String> list = getHugeList();  // 10万条数据
if (list.contains(target)) { ... }  // 每次 O(n),循环里调用就是 O(n²)

// ✅ 改成 HashSet:O(1) 查询
Set<String> set = new HashSet<>(list);
if (set.contains(target)) { ... }  // 瞬间完成

🔥 初始化容量,避免反复扩容


java

// ❌ 默认容量 10,超过就扩容(复制数组 + 新建数组)
List<String> list = new ArrayList<>();

// ✅ 预知大小时,直接指定初始容量
List<String> list = new ArrayList<>(10000);

// ✅ Java 8+ 推荐:用 Stream 收集时指定大小
List<String> result = list.stream()
    .filter(s -> s.startsWith("A"))
    .collect(Collectors.toCollection(() -> new ArrayList<>(list.size())));

四、流(Stream)API:优雅且高效,但别滥用

✅ Stream 的正确打开方式


java

// 场景:从用户列表中找出年龄 > 18 的名字,转大写,排序
List<String> result = users.stream()
    .filter(u -> u.getAge() > 18)       // 过滤
    .map(User::getName)                  // 转换
    .map(String::toUpperCase)            // 转换
    .sorted()                            // 排序
    .collect(Collectors.toList());       // 收集

❌ Stream 反模式

反模式 问题 改进
stream().forEach(System.out::println) 打印用 for 循环更直接 简单操作别用 Stream
嵌套多层 Stream 可读性极差,性能不一定好 拆成方法
大量小对象 Stream 操作 装箱/拆箱 + 对象创建开销大 基本类型用 IntStream 等

java

// ❌ 装箱开销大
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int sum = list.stream().mapToInt(Integer::intValue).sum();

// ✅ 直接用 IntStream,零装箱
int sum = IntStream.of(1, 2, 3, 4, 5).sum();

五、并发编程:用对工具,效率翻倍

🔧 线程池:别再 new Thread() 了


java

// ❌ 每次请求都新建线程,OOM 预定
new Thread(() -> {
    // do something
}).start();

// ✅ 线程池复用
private static final ExecutorService POOL = new ThreadPoolExecutor(
    10,                      // 核心线程数
    50,                      // 最大线程数
    60L, TimeUnit.SECONDS,   // 空闲线程存活时间
    new LinkedBlockingQueue<>(100),  // 工作队列
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

// ✅ Java 8+ 更推荐:用 ThreadPoolExecutor 包装
ExecutorService pool = Executors.newFixedThreadPool(10);

🔧 并发容器:性能碾压同步容器

场景 推荐 原因
高并发 Map ConcurrentHashMap 分段锁 / CAS,并发性能远超 Hashtable
高并发 List CopyOnWriteArrayList 读多写少场景,读不加锁
计数器 LongAdder 比 AtomicLong 吞吐量高 10 倍

java

// ❌ 高并发下 AtomicLong 竞争激烈
AtomicLong counter = new AtomicLong(0);
counter.incrementAndGet();

// ✅ LongAdder:分散热点,吞吐量更高
LongAdder counter = new LongAdder();
counter.increment();
long result = counter.sum();

🔧 CompletableFuture:告别回调地狱


java

// ❌ 传统 Future:阻塞 + 手动轮询
Future<String> future = executor.submit(() -> getUserName());
String name = future.get();  // 阻塞!

// ✅ CompletableFuture:非阻塞 + 链式调用
CompletableFuture.supplyAsync(() -> getUserId())
    .thenCompose(id -> CompletableFuture.supplyAsync(() -> getUserName(id)))
    .thenApply(name -> "Hello, " + name)
    .exceptionally(ex -> "Error: " + ex.getMessage())
    .thenAccept(System.out::println);

六、I/O 优化:I/O 是性能杀手

📁 文件读写:缓冲是关键


java

// ❌ 逐字节读取,慢到怀疑人生
FileInputStream fis = new FileInputStream("bigfile.txt");
int b;
while ((b = fis.read()) != -1) { ... }

// ✅ 加上缓冲流,性能提升 100 倍+
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("bigfile.txt"));
byte[] buffer = new byte[8192];  // 8KB 缓冲区
int len;
while ((len = bis.read(buffer)) != -1) { ... }

// ✅ Java 8+ 最优雅:Files.lines() 流式读取
try (Stream<String> lines = Files.lines(Paths.get("bigfile.txt"))) {
    lines.filter(line -> line.contains("ERROR"))
         .forEach(System.out::println);
}

🌐 网络 I/O:NIO 碾压 BIO

模型 说明 适用场景
BIO(阻塞 I/O) 一个连接一个线程 连接数 < 100
NIO(非阻塞 I/O) 一个线程管理多个连接 高并发(万级连接)
AIO(异步 I/O) 真正的异步 极高并发(Netty 底层)

💡 Spring Boot 默认用的就是 NIO(Tomcat/Undertow),你只需要确保别在请求里做阻塞操作(如 Thread.sleep、JDBC 同步查询)。


七、数据库优化:80% 的慢查询都是代码写出来的

❌ 反模式一:N+1 查询


java

// ❌ 经典 N+1:先查 1 次用户,再循环查 N 次订单
List<User> users = userMapper.selectAll();  // 1 次 SQL
for (User user : users) {
    List<Order> orders = orderMapper.selectByUserId(user.getId());  // N 次 SQL
}
// 总共 1 + N 次查询!

// ✅ 一次联表查询搞定
List<User> users = userMapper.selectAllWithOrders();  // 1 次 SQL,JOIN 搞定

❌ 反模式二:SELECT *


sql

-- ❌ 查出所有字段,浪费带宽和内存
SELECT * FROM users WHERE id = 1;

-- ✅ 只查需要的字段
SELECT id, name, email FROM users WHERE id = 1;

✅ 其他 SQL 优化技巧

技巧 说明
索引覆盖 SELECT id, name FROM users WHERE name = ?,如果 name 有索引,直接从索引拿数据,不回表
批量操作 INSERT INTO ... VALUES (...), (...), (...) 一次插入多条,比循环单条快 10 倍
分页优化 深分页用 WHERE id > lastId LIMIT n 代替 OFFSET
连接池 Druid / HikariCP,别用默认的

java

// ❌ 循环单条插入
for (User user : users) {
    userMapper.insert(user);  // N 次网络往返
}

// ✅ 批量插入
userMapper.batchInsert(users);  // 1 次网络往返

// MyBatis 批量配置
<insert id="batchInsert">
    INSERT INTO users (name, email) VALUES
    <foreach collection="list" item="u" separator=",">
        (#{u.name}, #{u.email})
    </foreach>
</insert>

八、JVM 层面的优化:最后的大招

🔥 JVM 参数调优(生产环境实战配置)


bash

# ✅ 推荐的生产环境 JVM 参数
java -server \
  -Xms4g -Xmx4g \                    # 堆内存固定,避免动态扩容
  -XX:+UseG1GC \                      # G1 垃圾回收器(JDK 9+ 默认)
  -XX:MaxGCPauseMillis=200 \          # GC 停顿目标 200ms
  -XX:+ParallelRefProcEnabled \       # 并行处理引用对象
  -XX:+HeapDumpOnOutOfMemoryError \   # OOM 时自动 dump
  -XX:HeapDumpPath=/logs/heapdump.hprof \
  -jar app.jar
参数 作用 建议值
-Xms / -Xmx 堆内存初始/最大 设为相同值,避免运行时扩容
-XX:+UseG1GC G1 回收器 JDK 8u40+ 推荐,大堆首选
-XX:MetaspaceSize 元空间初始大小 256m
-XX:MaxMetaspaceSize 元空间最大大小 512m

🔥 对象池化:减少 GC 压力


java

// ❌ 频繁创建大对象 → GC 压力大
public void process() {
    byte[] buffer = new byte[1024 * 1024];  // 每次 1MB
    // ...
}

// ✅ 对象池复用(Netty 的 ByteBuf 就是这个思路)
private static final ThreadLocal<byte[]> BUFFER_POOL = 
    ThreadLocal.withInitial(() -> new byte[1024 * 1024]);

public void process() {
    byte[] buffer = BUFFER_POOL.get();  // 复用
    // ...
}

九、缓存:空间换时间的艺术

📊 缓存策略对比

策略 说明 适用场景
本地缓存(Caffeine) 进程内,速度最快,但各节点不一致 热点数据、配置信息
Redis 分布式缓存 集中式,各节点共享 会话、计数器、排行榜
多级缓存(Caffeine + Redis) L1 本地 + L2 远程 高 QPS 热点数据

java

// ✅ Caffeine 本地缓存(推荐,性能优于 Guava Cache)
Cache<String, User> cache = Caffeine.newBuilder()
    .maximumSize(10_000)           // 最大条目数
    .expireAfterWrite(5, TimeUnit.MINUTES)  // 写入后 5 分钟过期
    .recordStats()                  // 开启统计
    .build();

User user = cache.get(userId, id -> userMapper.selectById(id));

🔥 缓存穿透 / 击穿 / 雪崩解决方案

问题 现象 解决方案
缓存穿透 查不存在的数据,请求直打 DB 布隆过滤器 / 缓存空值
缓存击穿 热点 Key 过期,瞬间打 DB 互斥锁 / 永不过期 + 异步刷新
缓存雪崩 大量 Key 同时过期 过期时间加随机偏移

java

// ✅ 缓存空值,防止穿透
User user = cache.get(userId);
if (user == null) {
    user = userMapper.selectById(userId);
    cache.put(userId, user != null ? user : NULL_USER, 5, TimeUnit.MINUTES);
}

// ✅ 过期时间加随机抖动,防止雪崩
.expireAfterWrite(5 + random.nextInt(3), TimeUnit.MINUTES)

十、其他高价值优化技巧速查表

技巧 代码 效果
用枚举代替常量字符串 enum Status { SUCCESS, FAIL } 编译期检查,性能更优
懒加载单例 static class Holder { static final INSTANCE = new X(); } 线程安全,无锁
位运算代替乘除 x << 1 代替 x * 2 极微小提升,但体现功底
避免在循环中创建对象 new Date() 移出循环 减少 GC 压力
用 try-with-resources try (Stream s = ...) {} 自动关闭资源,避免泄漏
日志用占位符 log.info("User {} logged in", name) 避免无意义的字符串拼接
使用 @PostConstruct 初始化逻辑放这里 代替构造函数做复杂初始化
减少锁粒度 synchronized(this) → synchronized(lock) 并发性能提升

总结:优化优先级路线图


第一步 ➜ 先用 Arthas / JProfiler 找到瓶颈(别瞎优化)
    │
第二步 ➜ 解决 N+1 查询、SELECT *、循环拼接字符串(立竿见影)
    │
第三步 ➜ 引入缓存(Redis / Caffeine),解决 80% 的读性能问题
    │
第四步 ➜ 优化集合选择、流操作、并发模型(精细化调优)
    │
第五步 ➜ JVM 参数调优 + GC 优化(最后的 5% 提升)

🎯 性能优化不是炫技,是工程能力的体现。 把上面这些技巧刻进肌肉记忆,你写出来的代码,就是比别人快、比别人稳。

现在就去你的项目里找一个反模式改掉,这就是最好的开始。 🚀

更多推荐