目录
正在加载目录…
专栏文章
专栏文章
Java 专栏
1. Java:跨平台语言与生态全景 2. Java 并发:JMM、锁与线程池原理 3. JVM:类加载、内存、GC 与性能诊断 4. Java 性能调优:CPU、内存、锁与 IO 排查 5. Java 新特性速查:从 Lambda 到虚拟线程 6. Java 业务开发:高频陷阱与 Code Review 清单 7. Java 核心知识:集合、并发与 JVM 面试要点 8. AQS 原理:同步队列与加锁解锁全流程 9. JVM 与 Linux 内存:分区、分配与 GC 边界 10. AtomicLong 与 LongAdder:并发计数器选型 11. Java 引用与内存泄漏:六类场景与修复 12. Java 测试实践:JUnit、Mockito 与 Testcontainers 13. Java 并发入门:线程、锁与线程池全景

Java 性能调优:CPU、内存、锁与 IO 排查

发布于 2026-06-17 14:40 · 最后编辑于 2026-07-31 15:51 · 字数 2,707 👁 174 次阅读

本文从性能调优方法论出发,覆盖 CPU/内存/锁/IO 四类性能问题的分析手段、JVM 调优参数、常用 Profiling 工具以及常见性能陷阱,帮助你在遇到线上性能问题时有章可循。

目录

章节说明
性能调优方法论指标体系、测试策略、三步走流程
CPU 性能问题上下文切换、死循环、正则回溯
内存性能问题内存泄漏排查、堆结构、GC 调优
锁与并发优化Synchronized 锁升级、Lock 锁分离、乐观锁
线程池调优线程数计算公式、参数含义
JVM 调优内存模型、GC 算法选型、常用参数
性能分析工具JDK 内置工具、压测工具
常见性能陷阱代码层面的高频问题

性能调优方法论

../../assets/04 Java 性能调优/file 20260605123644627

性能指标体系

指标说明典型目标
响应时间接口端到端耗时(ms 级)P99 < 200ms
吞吐量 TPS每秒处理事务数越高越好
CPU 使用率系统/用户态 CPU 占比< 70%
内存使用率堆/非堆内存占比< 80%
GC 吞吐量应用时间 / 总运行时间> 95%
负载承受能力压测时响应时间随并发数的曲线平缓上升

调优三步走

测试(发现瓶颈)
  ↓
分析(定位根因):自下而上 OS → JVM → Application
  ↓
调优(解决问题):自上而下 Application → JVM → OS

测试策略

微基准测试:精准定位某个方法/模块在不同实现下的性能差异,适合对比两种方案。

宏基准测试:综合模拟线上环境,需要考虑:

  • 测试环境与线上一致
  • 并行业务接口的干扰
  • 目标:吞吐量达标 + 探底 TPS + 观察资源使用率变化

注意热身问题:JVM JIT 编译需要预热,第一批请求的响应时间偏高,测试结果应取稳定阶段的均值。

兜底策略

  • 限流:基于压测探底的 TPS 设置入口限流阈值 + 熔断
  • 横向扩容:当超过阈值时自动扩容(Kubernetes HPA)
  • 提前扩容:秒杀等瞬时高并发场景在活动前手动扩容

CPU 性能问题

常见 CPU 高耗场景

场景原因解决方案
代码死循环递归未收敛、逻辑 Bug代码 Review + jstack 找线程
正则回溯复杂正则在特定输入下指数级回溯简化正则、限制输入长度
JVM Full GC 频繁老年代空间不足调大堆/优化对象生命周期
大量上下文切换线程数过多、锁竞争激烈减少线程数、优化锁粒度
异常频繁抛出构建异常栈消耗大避免用异常控制流程

上下文切换排查

# 查看进程级上下文切换
vmstat 1 5
# cs 列:每秒上下文切换次数,正常应 < 5000

# 查看线程级切换
pidstat -w -p <pid> 1 3

# 查看线程堆栈(找 BLOCKED/WAITING 线程)
jstack <pid> | grep -A 5 "BLOCKED\|WAITING"

内存性能问题

JVM 内存模型

JVM 内存
├── 堆(Heap)—— 线程共享
│   ├── 新生代
│   │   ├── Eden 区
│   │   ├── From Survivor (S0)
│   │   └── To Survivor (S1)
│   └── 老年代
├── 方法区(元空间,Java 8+,本地内存)
├── 程序计数器 —— 线程私有,记录字节码执行位置
├── 虚拟机栈 —— 线程私有,存储栈帧(局部变量、操作数栈)
└── 本地方法栈 —— 线程私有,C 语言 native 方法

Java 8 变化:永久代(PermGen)被元空间(Metaspace)取代,元空间使用本地内存,避免了 OutOfMemoryError: PermGen

内存泄漏排查流程

# 1. 查看进程内存使用
top -p <pid>

# 2. 查看堆内存使用(老年代持续增长是泄漏信号)
jmap -heap <pid>

# 3. 查看存活对象分布
jmap -histo:live <pid> | head -30

# 4. Dump 堆文件
jmap -dump:format=b,file=/tmp/heap.hprof <pid>

# 5. 用 MAT 工具分析 dump 文件,找 Retained Heap 最大的对象

启动参数(OOM 时自动 dump)

java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/tmp/heapdump.hprof \
     -jar app.jar

常见内存泄漏场景

// 典型泄漏:ThreadLocal 使用后未 remove
// 线程池中的线程生命周期很长,ThreadLocal 的 value 无法被 GC
@RequestMapping("/api")
public String handle() {
    ThreadLocal<byte[]> local = new ThreadLocal<>();
    local.set(new byte[4096 * 1024]);  // 4MB
    // 忘记 local.remove(),线程归还线程池后 value 仍然存在
    return "ok";
}

// 正确写法
ThreadLocal<byte[]> LOCAL = new ThreadLocal<>();
try {
    LOCAL.set(new byte[4096 * 1024]);
    // ... 业务逻辑
} finally {
    LOCAL.remove();  // 必须在 finally 中 remove
}

锁与并发优化

Synchronized 锁升级机制

JDK 1.6 引入分级锁,锁状态单向升级:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
锁状态适用场景原理
偏向锁单线程反复获取同一锁Mark Word 记录线程 ID,无需 CAS
轻量级锁线程交替执行(无激烈竞争)CAS 替换 Mark Word
重量级锁多线程激烈竞争OS Mutex,涉及用户态/内核态切换
# 高并发下偏向锁撤销反而有开销,可关闭
-XX:-UseBiasedLocking

# 关闭自旋锁(锁竞争激烈、持有时间长时)
-XX:-UseSpinning

锁消除与锁粗化(JIT 自动优化):

  • 锁消除:逃逸分析发现锁对象不会被其他线程访问,直接消除锁
  • 锁粗化:相邻的同步块使用同一锁,合并为一个大同步块

减小锁粒度:将一个大锁对象拆分为多个小对象(如 ConcurrentHashMap 的分段锁思想)。

Lock 同步锁

import java.util.concurrent.locks.*;

// ReentrantReadWriteLock:读多写少场景
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock  = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

public double read() {
    readLock.lock();
    try {
        return Math.sqrt(x * x + y * y);
    } finally {
        readLock.unlock();  // 必须在 finally 中释放
    }
}

public void write(double dx, double dy) {
    writeLock.lock();
    try {
        x += dx;
        y += dy;
    } finally {
        writeLock.unlock();
    }
}

StampedLock(Java 8+):三种模式,读多写少场景性能优于 RRW:

StampedLock sl = new StampedLock();

double distanceFromOrigin() {
    long stamp = sl.tryOptimisticRead();   // 乐观读,不加锁
    double x = this.x, y = this.y;
    if (!sl.validate(stamp)) {             // 检查期间是否有写操作
        stamp = sl.readLock();             // 升级为悲观读
        try {
            x = this.x;
            y = this.y;
        } finally {
            sl.unlockRead(stamp);
        }
    }
    return Math.sqrt(x * x + y * y);
}

StampedLock 的局限:不支持重入,不支持条件变量,调用 lock() 后若线程被中断会导致 CPU 100%(需用 lockInterruptibly()),因此未被广泛替代 RRW。

乐观锁(CAS)

import java.util.concurrent.atomic.*;

// AtomicInteger:单变量 CAS 操作
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(1, 2);   // CAS:期望值=1 时更新为 2

// LongAdder(Java 8+):高并发写场景性能更好
// 原理:分散到 cell[] 数组,减少 CAS 竞争,最终汇总
LongAdder adder = new LongAdder();
adder.increment();
long sum = adder.sum();  // 近似值,但 longValue() 返回准确值
场景推荐方案
读 >> 写StampedLock 乐观读
读 > 写ReentrantReadWriteLock
读写相当ReentrantLockSynchronized
写 >> 读(单变量计数)LongAdder
写 >> 读(需精确值)AtomicLong

线程池调优

ThreadPoolExecutor 参数

new ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数(常驻)
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程空闲存活时间
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,   // 任务队列(建议有界)
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler     // 拒绝策略
);

不推荐 直接使用 Executors 工厂方法:newFixedThreadPoolnewSingleThreadExecutor 使用无界队列,newCachedThreadPool 最大线程数为 Integer.MAX_VALUE,均可能 OOM。

线程数计算公式

任务类型公式说明
CPU 密集型N + 1(N = CPU 核数)+1 防止偶发缺页中断导致 CPU 空闲
IO 密集型2NIO 等待期间 CPU 空闲,可多开线程
混合型N × (1 + WT/ST)WT=线程等待时间,ST=线程运行时间

公式给出初始值,最终需要通过实际压测验证,逐步调整到最优值。

JVM 调优

../../assets/04 Java 性能调优/file 20260605123644814

GC 算法对比

收集器特点适用场景
Serial单线程,Stop-the-World单核/客户端
Parallel Scavenge多线程,吞吐量优先批处理,JDK 8 默认
CMS并发标记,低停顿响应时间敏感(已废弃)
G1分 Region,可预测停顿JDK 9+ 默认,大堆
ZGC停顿 < 10msJDK 15+,超大堆
Shenandoah停顿极低JDK 12+,OpenJDK

GC 调优策略

降低 Minor GC 频率:增大 Eden 区,减少短生命周期对象晋升老年代。

降低 Full GC 频率

  • 避免创建大对象(超过 -XX:PretenureSizeThreshold 直接进老年代)
  • 增大堆内存,设置 -Xms = -Xmx 避免动态扩缩堆

常用 JVM 参数

# 堆内存
-Xms4g -Xmx4g          # 初始堆 = 最大堆(避免动态扩缩)
-Xmn1g                  # 新生代大小
-XX:SurvivorRatio=8     # Eden:S0:S1 = 8:1:1

# GC 选择
-XX:+UseG1GC            # 使用 G1(JDK 9+ 默认)
-XX:MaxGCPauseMillis=200  # G1 目标停顿时间

# GC 日志
-XX:+PrintGCDateStamps \
-XX:+PrintGCDetails \
-Xloggc:/var/log/gc.log

# OOM 时 dump
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heapdump.hprof

# 锁相关
-XX:-UseBiasedLocking   # 关闭偏向锁(高并发竞争激烈时)

GC 性能衡量

# 查看堆内存使用(含 GC 次数和耗时)
jstat -gc <pid> 1000 10   # 每 1 秒采样,共 10 次

# 关键指标
# YGC/YGCT:Minor GC 次数/耗时
# FGC/FGCT:Full GC 次数/耗时
# GCT:GC 总耗时
# 吞吐量 = (运行时间 - GCT) / 运行时间,目标 > 95%

性能分析工具

JDK 内置工具

工具用途常用命令
jstat监控 JVM 统计信息(GC、类加载)jstat -gc <pid> 1000
jstack线程堆栈快照,排查死锁/阻塞jstack <pid>
jmap堆内存信息、对象分布、dumpjmap -histo:live <pid>
jps列出 JVM 进程jps -l
jinfo查看/修改 JVM 参数jinfo -flags <pid>
jcmd综合诊断命令jcmd <pid> VM.flags

Linux 系统工具

# 进程级 CPU/内存
top -p <pid>
top -Hp <pid>          # 线程级

# 上下文切换
vmstat 1 5             # cs 列

# 线程级详细统计
pidstat -p <pid> -u -r -w 1 5
# -u CPU,-r 内存,-w 上下文切换

压测工具

工具特点适用场景
ab(ApacheBench)轻量,命令行,快速验证单接口 GET/POST 压测
JMeter功能全,支持场景录制,图形化复杂业务流程压测
wrk高性能,Lua 脚本扩展HTTP 高并发基准测试
# ab 示例:100 请求,10 并发
ab -n 100 -c 10 http://localhost:8080/api/test

# 关键输出指标
# Requests per second:吞吐率(TPS)
# Time per request:用户平均等待时间
# Percentage of requests served within a certain time:响应时间分布

GC 日志分析工具

  • GCViewer:本地图形化工具,打开 gc.log 文件
  • GCeasy(https://www.gceasy.io):在线分析,上传压缩后的 gc.log,输出直观报告

常见性能陷阱

String 相关

// 陷阱:循环内用 + 拼接字符串,产生大量临时对象
String result = "";
for (int i = 0; i < 10000; i++) {
    result += i;   // 每次都创建新 String 对象
}

// 正确:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i);
}
String result = sb.toString();

集合相关

// 陷阱:用 for(int i) 遍历 LinkedList,O(n²) 复杂度
LinkedList<String> list = getList();
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));  // 每次 get 都从头遍历
}

// 正确:使用 Iterator 或 for-each
for (String s : list) {
    System.out.println(s);
}

数据库相关

// 陷阱:一次查询返回大对象(60 个字段)
// 解决:首次只查关键字段,需要时再查其余字段

// 陷阱:频繁创建大对象导致 Full GC
// 解决:分页查询,避免单次加载过多数据

异常相关

// 陷阱:用异常控制正常业务流程
// 构建异常栈(fillInStackTrace)非常耗性能
try {
    int value = Integer.parseInt(str);
} catch (NumberFormatException e) {
    value = 0;  // 如果 str 经常不是数字,这里性能很差
}

// 正确:先校验再转换
if (str != null && str.matches("\\d+")) {
    int value = Integer.parseInt(str);
}

线程安全相关

// 陷阱:HashMap 在并发场景下使用(JDK 7 可能死循环)
// 高并发场景必须用 ConcurrentHashMap

// 陷阱:SimpleDateFormat 不是线程安全的
// 解决:使用 ThreadLocal 或 DateTimeFormatter(Java 8+)
private static final ThreadLocal<SimpleDateFormat> SDF =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论