Java 代码优化技巧:从“能跑就行“到“丝滑如飞“
·
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% 提升)
🎯 性能优化不是炫技,是工程能力的体现。 把上面这些技巧刻进肌肉记忆,你写出来的代码,就是比别人快、比别人稳。
现在就去你的项目里找一个反模式改掉,这就是最好的开始。 🚀
更多推荐

所有评论(0)