Java高质量编程【二、高性能】

在Java开发中,高性能编码不仅仅是写出跑得快的代码,更关乎资源利用率(CPU、内存、I/O)和系统稳定性。高性能往往意味着减少垃圾回收(GC)压力、降低锁竞争、优化数据结构以及避免不必要的对象创建。

以下是Java高性能编码的核心方法、规范详解及正反示例,分为内存与对象管理集合与数据结构并发编程I/O与流处理字符串处理五个维度。


一、内存与对象管理 (Memory & Object Management)

核心原则:减少对象分配频率,降低GC压力,避免内存泄漏。

1. 避免在循环中创建临时对象

  • 规范:将对象创建移出循环,或复用对象。
  • 原理:循环内创建对象会导致短时间内产生大量“朝生夕死”的对象,触发频繁的 Minor GC,甚至导致 Stop-The-World (STW)。

❌ 反面示例:

public long sumValues(List<Integer> list) {
    long sum = 0;
    for (Integer i : list) {
        // 每次循环都自动装箱/拆箱,且如果逻辑复杂可能创建临时对象
        // 这里的 new BigDecimal 是典型的循环内创建大对象
        BigDecimal val = new BigDecimal(i); 
        sum += val.longValue();
    }
    return sum;
}

✅ 正面示例:

public long sumValues(List<Integer> list) {
    long sum = 0;
    // 如果需要复杂计算,尽量在循环外初始化可复用对象,或使用基本类型
    for (Integer i : list) {
        // 直接使用基本类型运算,避免对象创建
        sum += i; 
    }
    return sum;
}
// 如果必须用BigDecimal,考虑是否在循环外复用(注意线程安全),或者接受必要的开销但避免其他多余操作

2. 使用基本类型代替包装类

  • 规范:在计算密集型场景,优先使用 int, long, double 而非 Integer, Long, Double
  • 原理:包装类包含对象头开销(约12-16字节)+ 引用开销,且涉及自动装箱/拆箱的CPU指令。

❌ 反面示例:

// 使用包装类列表,内存占用大,且有拆箱开销
List<Long> ids = new ArrayList<>();
for (long i = 0; i < 1000000; i++) {
    ids.add(i); // 自动装箱:new Long(i)
}

✅ 正面示例:

// 使用基本类型数组或第三方库(如 fastutil, HPPC)
long[] ids = new long[1000000];
for (long i = 0; i < 1000000; i++) {
    ids[(int)i] = i; // 无装箱开销
}
// 或者使用 fastutil 的 LongArrayList

3. 警惕隐式内存泄漏

  • 规范:长生命周期的对象(如静态Map、缓存)持有短生命周期对象的引用时,必须手动清理。
  • 原理:GC Roots 链未断开,导致对象无法回收。

❌ 反面示例:

public class Cache {
    private static final Map<String, Object> store = new HashMap<>();
    
    public void put(String key, Object value) {
        store.put(key, value);
        // 忘记 remove,随着时间推移,store 无限膨胀,导致 OOM
    }
}

✅ 正面示例:

import java.util.WeakHashMap; // 方案A:使用弱引用
// 或者
import com.github.benmanes.caffeine.cache.Cache; 
import com.github.benmanes.caffeine.cache.Caffeine;

public class SafeCache {
    // 方案B:使用带有淘汰策略的专业缓存库(推荐)
    private final Cache<String, Object> store = Caffeine.newBuilder()
        .maximumSize(10_000)
        .expireAfterWrite(java.time.Duration.ofMinutes(5))
        .build();

    public void put(String key, Object value) {
        store.put(key, value);
    }
}

二、集合与数据结构 (Collections & Data Structures)

核心原则:预分配容量,选择合适的数据结构,减少扩容和哈希冲突。

1. 初始化集合时指定容量

  • 规范:如果知道集合的大致大小,务必在构造函数中指定 initialCapacity
  • 原理:避免数组多次扩容(resize)和元素拷贝(System.arraycopy)。HashMap 扩容还会导致 rehash。

❌ 反面示例:

// 默认容量16,负载因子0.75。当放入1000个元素时,会经历多次扩容 (16->32->64...->1024)
// 每次扩容都伴随着数组创建和数据迁移,性能损耗极大
List<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
    list.add("item" + i);
}

✅ 正面示例:

// 预估大小,一次性分配足够内存
List<String> list = new ArrayList<>(10000);
for (int i = 0; i < 10000; i++) {
    list.add("item" + i);
}

// HashMap 同样需要计算容量:expectedSize / loadFactor + 1
Map<String, String> map = new HashMap<>((int)(10000 / 0.75f) + 1);

2. 选择正确的 Map 实现

  • 规范
    • 键值对少且Key简单:HashMap
    • 高并发读多写少:ConcurrentHashMap (JDK8+ 性能很好)。
    • 只需要Key不需要Value:HashSet (底层是HashMap) 或 专用位图。
    • 范围查询/排序:TreeMap (O(logN)),但如果只需排序一次,建议 ArrayList + Collections.sort (O(NlogN) 但常数更小)。

❌ 反面示例:

// 场景:只需要判断元素是否存在,却用了 HashMap<String, Boolean>
Map<String, Boolean> existsMap = new HashMap<>();
existsMap.put("key", true);
// 浪费了一个 Boolean 对象的空间

✅ 正面示例:

// 使用 HashSet 或者更极致的 BitSet (如果Key是整数)
Set<String> existsSet = new HashSet<>();
existsSet.add("key");

三、并发编程 (Concurrency)

核心原则:减少锁粒度,利用无锁算法,避免上下文切换。

1. 优先使用 LongAdder 而非 AtomicLong (高并发计数)

  • 规范:在高并发写场景下,使用 LongAdder
  • 原理AtomicLong 基于 CAS,竞争激烈时自旋消耗大量 CPU;LongAdder 采用分段累加(Cell数组),仅在求和时合并,大幅降低竞争。

❌ 反面示例:

private final AtomicLong count = new AtomicLong(0);

public void increment() {
    // 高并发下,CAS 失败率高,CPU 空转
    count.incrementAndGet(); 
}

✅ 正面示例:

private final LongAdder count = new LongAdder();

public void increment() {
    // 线程更新不同的 Cell,几乎无冲突
    count.increment();
}

public long getCount() {
    return count.sum(); // 读取时稍慢,但写入极快
}

2. 缩小锁的范围 (Lock Striping / Fine-grained Locking)

  • 规范:只锁定必要的代码段,避免锁定整个方法或大块逻辑。
  • 原理:减少线程等待时间,提高吞吐量。

❌ 反面示例:

public synchronized void process(List<Data> list) {
    // 锁住了整个方法,包括耗时的 I/O 操作和无关的计算
    Data data = fetchFromDB(); // I/O 阻塞,锁被长时间占用
    transform(data);
    save(data);
}

✅ 正面示例:

private final Object lock = new Object();

public void process(List<Data> list) {
    Data data = fetchFromDB(); // 在锁外执行 I/O
    
    synchronized (lock) {
        // 仅锁定共享资源修改的临界区
        transform(data);
        save(data);
    }
}

3. 使用线程池,严禁手动 new Thread()

  • 规范:使用 ThreadPoolExecutorExecutors (需注意坑) 创建线程池,并合理配置参数。
  • 原理:线程创建和销毁开销大,无限创建线程会导致 OOM 或 CPU 过度切换。

❌ 反面示例:

// 每次请求都新建线程,系统资源迅速耗尽
public void handleRequest() {
    new Thread(() -> {
        doHeavyWork();
    }).start();
}

✅ 正面示例:

// 预定义线程池,复用线程
private static final ExecutorService executor = new ThreadPoolExecutor(
    10, 50, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000),
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略防止任务丢失
);

public void handleRequest() {
    executor.submit(() -> doHeavyWork());
}

四、I/O 与 流处理 (I/O & Streams)

核心原则:减少系统调用次数,使用缓冲,利用 NIO。

1. 使用缓冲流 (Buffered Streams)

  • 规范:包装 InputStream/OutputStream/Reader/Writer 时使用 Buffered 版本。
  • 原理:减少磁盘/网络系统调用(syscall)的次数。一次读1KB比读1000次1字节快得多。

❌ 反面示例:

// 每次 read() 都可能触发一次系统调用
try (FileInputStream fis = new FileInputStream("large.txt")) {
    int data;
    while ((data = fis.read()) != -1) { // 逐字节读取,极慢
        process(data);
    }
}

✅ 正面示例:

// 内部维护缓冲区,批量读取
try (FileInputStream fis = new FileInputStream("large.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    
    byte[] buffer = new byte[8192];
    int len;
    while ((len = bis.read(buffer)) != -1) { // 批量读取
        process(buffer, len);
    }
}

2. 谨慎使用 Java Stream API (在极致性能场景)

  • 规范:Stream API 代码优雅,但在极度敏感的热点路径(Hot Path),传统 for 循环通常更快。
  • 原理:Stream 涉及 Lambda 对象创建、接口调用开销、流水线构建开销。JDK 优化后差距缩小,但在简单遍历累加中,for 仍胜一筹。

❌ 反面示例 (微基准测试敏感场景):

// 在每秒调用百万次的场景中,Stream 的开销不可忽视
long sum = list.stream()
    .filter(x -> x > 0)
    .mapToLong(Long::valueOf)
    .sum();

✅ 正面示例:

long sum = 0;
for (long x : list) {
    if (x > 0) {
        sum += x;
    }
}
// 注:对于业务逻辑复杂的非热点代码,Stream 的可读性优势大于微小的性能损失,应优先保证可读性。

五、字符串处理 (String Handling)

核心原则:利用不可变性,避免频繁拼接,注意编码转换。

1. 循环中使用 StringBuilder

  • 规范:在循环或频繁修改字符串时,必须使用 StringBuilder (单线程) 或 StringBuffer (多线程,较少用)。
  • 原理:String 是不可变的,+ 操作符在循环中会生成大量中间 String 对象。

❌ 反面示例:

String result = "";
for (int i = 0; i < 1000; i++) {
    result += i; // 编译为 new StringBuilder().append(result).append(i).toString()
                 // 每次循环都丢弃旧的 StringBuilder 和 String
}

✅ 正面示例:

StringBuilder sb = new StringBuilder(1000 * 4); // 预估容量
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();

2. 字符串常量池与 intern() 的慎用

  • 规范:不要随意调用 string.intern(),除非你非常清楚自己在做什么(如节省大量重复长字符串的内存)。
  • 原理intern() 会将字符串放入 JVM 的永久代/元空间中的常量池,操作涉及同步锁,且容易导致 Metaspace OOM。

❌ 反面示例:

// 解析大量日志时,对每个字段都 intern,导致元空间爆满且性能下降
String category = parseCategory(line).intern(); 

✅ 正面示例:

// 正常创建字符串,依靠 JVM 的 G1/ZGC 处理重复字符串(现代 GC 对短字符串优化很好)
// 或者使用自定义的字典映射 (Map<String, Integer>) 来枚举化
String category = parseCategory(line);

六、总结与最佳实践清单

类别 关键动作 性能收益
对象 循环外创建对象,复用对象池 减少 GC 频率,降低 STW 时间
类型 基本类型 > 包装类 减少内存占用,消除装箱拆箱
集合 初始化指定容量 (capacity) 避免数组扩容拷贝和 Rehash
并发 LongAdder > AtomicLong (高并发写) 降低 CAS 自旋,提升 CPU 利用率
并发 缩小 synchronized 范围 减少线程阻塞等待
I/O 必须使用 Buffered 减少系统调用次数 (Syscalls)
字符串 循环拼接用 StringBuilder 避免 $O(N^2)$ 的对象创建复杂度
日志 使用占位符 log.info("id={}", id) 避免在不打印日志时构造字符串

七、如何验证性能?

不要盲目优化。测量优于猜测

  1. JMH (Java Microbenchmark Harness):编写微基准测试,科学地对比两种写法的性能差异。
  2. Profiler 工具:使用 Async Profiler, JVisualVM, Arthas 查看 CPU 热点方法和内存分配情况。
  3. GC 日志分析:观察 GC 频率和停顿时间,判断是否因对象创建过多导致。

示例:使用 JMH 测试

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class StringBenchmark {

    @Param({"100", "1000"})
    public int size;

    @Benchmark
    public String testConcat() {
        String s = "";
        for (int i = 0; i < size; i++) {
            s += i;
        }
        return s;
    }

    @Benchmark
    public String testBuilder() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < size; i++) {
            sb.append(i);
        }
        return sb.toString();
    }
}

运行结果通常会显示 testBuildertestConcat 快几个数量级。

遵循这些规范和模式,能够显著提升 Java 应用的响应速度和吞吐量,同时保持系统的稳定性。

posted @ 2026-03-13 17:06  蓝迷梦  阅读(0)  评论(0)    收藏  举报