第 239 章:性能调优 - JVM
学习目标
- JVM 内存模型
- GC 调优策略
- 线程与锁优化
- 编写高性能代码
一、JVM 内存模型
1.1 堆分代
| 区域 | 特点 | GC 算法 |
|---|---|---|
| Eden | 新对象创建 | Minor GC |
| Survivor S0/S1 | 短期存活 | Minor GC |
| Old | 长期存活对象 | Major GC / Full GC |
1.2 内存配置
bash
# 启动参数
java -Xms4g -Xmx4g \
-XX:MaxMetaspaceSize=512m \
-XX:ReservedCodeCacheSize=256m \
-jar myapp.jar| 参数 | 说明 | 建议 |
|---|---|---|
-Xms | 初始堆 | = -Xmx |
-Xmx | 最大堆 | 机器内存 60% |
-Xmn | 年轻代大小 | = 堆 1/3 ~ 1/4 |
-XX:MaxMetaspaceSize | Metaspace | 256M ~ 1G |
-XX:ReservedCodeCacheSize | JIT 代码缓存 | 256M |
1.3 对象生命周期
二、垃圾回收器
2.1 回收器对比
| 回收器 | 算法 | 暂停 | 适合 |
|---|---|---|---|
| Serial | 串行 | 长 | 单核 |
| Parallel | 并行 | 中 | 吞吐优先 |
| CMS | 标记清除 | 短 | 已废弃 |
| G1 | 区域分代 | 可控 | 默认推荐 |
| ZGC | 染色指针 | < 1ms | 大堆低延迟 |
| Shenandoah | 转发指针 | < 1ms | 大堆低延迟 |
2.2 G1 默认 / 通用
bash
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标停顿
-XX:G1HeapRegionSize=16m # 区域大小
-XX:+G1UseAdaptiveIHOP # 自适应堆占用阈值2.3 ZGC(超低延迟)
bash
-XX:+UseZGC
-XX:+UnlockExperimentalVMOptions
-XX:ZCollectionInterval=120适合:
- 大堆(8GB+)
- 延迟敏感(微服务)
- JDK 11+
2.4 选型决策
三、GC 调优
3.1 调优原则
- 优先级:内存足够 > GC 调优
- 不该:频繁 Full GC
- 关注:Young GC 频率 + Full GC 时长
3.2 目标
| 指标 | 目标 |
|---|---|
| Young GC | 几秒一次 ~ 几十秒一次 |
| Young GC 耗时 | < 50 ms |
| Full GC | 0 次每小时 / 极少 |
| Full GC 耗时 | < 500 ms |
| 堆利用率 | < 75% |
3.3 日志分析
bash
# 启动参数
-Xlog:gc*,gc+heap=trace,gc+age=trace,safepoint:file=gc.log:time,level,tags
# 使用工具
# 在线分析:GC Easy / GCEasy.io
# 本地工具: GCViewer, JClarity Censum3.4 常见 GC 问题
Young GC 频繁
症状: Minor GC 每秒多次
原因: 短生命周期对象太多 / 年轻代太小
方案:
- 调大 -Xmn
- 复用对象避免高频创建
- 减少大对象Full GC 频繁
症状: 几秒一次 Full GC,停顿几百 ms
原因:
- System.gc() 被调用
- 大对象直接进 Old
- 内存泄漏 / 长期引用未释放
- Perm 区小(老版本)
方案:
- 排查代码:不用 System.gc
- 检查内存泄漏:jmap + MAT
- 调大堆GC 暂停过长
症状: 单次 GC > 1 秒
方案:
- 换 ZGC 接近 0 暂停
- 减少大对象分配
- 调整 G1 region 大小四、JVM 参数模板
4.1 通用 Java 应用
bash
java \
-Xms4g -Xmx4g \
-XX:MaxMetaspaceSize=512m \
-XX:ReservedCodeCacheSize=256m \
-Xss512k \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=16m \
-XX:+ParallelRefProcEnabled \
-XX:+UseStringDeduplication \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heap.hprof \
-XX:+UnlockDiagnosticVMOptions \
-XX:NativeMemoryTracking=summary \
-Xlog:gc*,gc+heap=trace,safepoint:file=/tmp/gc-%t.log:time,level,tags:filecount=10,filesize=100m \
-jar myapp.jar4.2 JVM TLAB
yaml
TLAB: Thread Local Allocation Buffer
- 每个线程独占,分配对象无需同步
- 减少 Eden 竞争
- 默认开,可调大小五、内存溢出定位
5.1 OOM 类型
5.2 排查步骤
bash
# 1. 看进程是否 OOM 退出
dmesg | grep -i "kill"
ls -lh /tmp/heap.hprof
# 2. 用 MAT 分析 heap 文件
# 下载 MAT 工具,启动,File -> Open Heap Dump
# 3. 在 MAT 中:
# - Leak Suspects Report
# - Dominator Tree
# - 找 Retained Heap 大的对象5.3 内存分析工具
| 工具 | 用途 |
|---|---|
| jmap | dump 堆 |
| jstack | 线程栈 |
| jstat | GC 监控 |
| jcmd | 综合 |
| async-profiler | 火焰图 |
| Arthas | 在线诊断 |
| VisualVM | 桌面工具 |
5.4 内存泄漏常见场景
java
// 1. 静态集合持有
private static Map<Long, Object> cache = new HashMap<>();
cache.put(id, obj); // 永不释放
// 2. 长生命周期对象引用短生命周期
private static List<Object> list = new ArrayList<>();
public void add(Object o) { list.add(o); }
// 3. 未关闭资源
Connection conn = ds.getConnection();
// 忘记 close()
// 4. ThreadLocal 不清理
threadLocal.set(obj);
// 不 remove,线程复用时仍持有5.5 ThreadLocal 内存泄漏
java
@Component
public class UserContext {
private static final ThreadLocal<User> CURRENT = new ThreadLocal<>();
public static void set(User user) {
CURRENT.set(user);
}
public static User get() {
return CURRENT.get();
}
// 在请求结束清理,避免线程复用泄漏
public static void clear() {
CURRENT.remove(); // 必须 remove!
}
}
// Spring 拦截器
Interceptor:
preHandle() -> UserContext.set(currentUser);
afterCompletion() -> UserContext.clear();六、线程与并发调优
6.1 线程池参数
yaml
默认 ThreadPoolExecutor:
corePoolSize: cpu * 2
maxPoolSize: cpu * 4
queueCapacity: 1000
计算公式:
- IO 密集型:cpu * 2 ~ 4 (大部分时间等 IO)
- CPU 密集型:cpu + 1
- 混合:压测定6.2 优化 Tomcat
yaml
server:
tomcat:
threads:
max: 400 # 最大工作线程
min-spare: 50 # 最小空闲
accept-count: 200 # 等待队列
max-connections: 8192
connection-timeout: 20s6.3 优化连接池(HikariCP)
yaml
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
auto-commit: false # 关闭自动提交6.4 减少锁竞争
java
// ❌ 全局锁
private static final Object lock = new Object();
public synchronized void process() { ... }
// ✅ 分段锁(ConcurrentHashMap 思想)
private final Object[] locks = new Object[16];
public void process(long key) {
synchronized (locks[(int)(key & 15)]) { ... }
}
// ✅ 读写锁
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void read() {
rwLock.readLock().lock();
try { ... } finally { rwLock.readLock().unlock(); }
}
// ✅ 乐观锁(CAS,无锁)
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
// ✅ 不可变对象(无锁)
public final class ImmutableValue {
private final int value;
}七、内存可见性
7.1 JMM 内存模型
| 特性 | 说明 |
|---|---|
| 可见性 | 一线程修改,另一线程能看到 |
| 原子性 | 一组操作不可分割 |
| 有序性 | 重排序不影响单线程 |
7.2 volatile
java
private volatile boolean running = true;
void stop() { running = false; }特性:
- 可见性:写立即刷新到主内存
- 禁止重排序
- 不保证复合原子性
7.3 synchronized
java
private final Object lock = new Object();
private int count = 0;
public void increment() {
synchronized (lock) {
count++;
}
}JDK 1.6 后引入锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。
八、JIT 编译器
8.1 C1 vs C2
yaml
C1 (Client):
- 启动快
- 优化少
- 桌面应用
C2 (Server):
- 启动慢
- 激进优化(内联/逃逸分析)
- **服务端默认**
TieredCompilation:
- C1 渐进 → C2
- JDK 7+ 默认8.2 热点代码识别
bash
# 打印 JIT 编译日志
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintCompilation
-XX:+LogCompilation8.3 方法内联
java
// 短方法被内联(JIT 自动)
public int add(int a, int b) { return a + b; }8.4 逃逸分析
java
public String concat(List<String> list) {
// JIT 优化:不开新对象,栈上分配
StringBuilder sb = new StringBuilder();
for (String s : list) sb.append(s);
return sb.toString();
}九、性能案例
9.1 案例 1:大对象频繁创建
java
// ❌ 大量临时数组
public byte[] process(byte[] input) {
byte[] result = new byte[input.length * 2];
// ...
return result;
}
// ✅ 对象池或复用
private final ThreadLocal<byte[]> BUFFER = ThreadLocal.withInitial(() -> new byte[2048]);
public byte[] process(byte[] input) {
byte[] buf = BUFFER.get();
// ...
return Arrays.copyOf(buf, length);
}9.2 案例 2:对象字段顺序
java
// ❌ padding 浪费空间
class Data {
boolean a; // 1 byte
long b; // 8 byte (对齐)
boolean c; // 1 byte (对齐)
long d; // 8 byte (对齐)
}
// size: 32 bytes
// ✅ 字段排序紧凑
class Data {
long b; // 8 byte
long d;
boolean a; // 1 byte
boolean c;
}
// size: 24 bytes9.3 案例 3:字符串拼接
java
// ❌ 多次拼接
String result = "";
for (String s : list) {
result += s; // 创建多个中间对象
}
// ✅ StringBuilder
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s);
}
String result = sb.toString();十、性能监控指标
yaml
JVM 指标:
- jvm_memory_used_bytes
- jvm_memory_max_bytes
- jvm_gc_pause_seconds_count
- jvm_gc_pause_seconds_sum
- jvm_threads_states_threads
应用指标:
- JVM uptime
- thread count
- loaded classes count
- heap size
导出方式:
- /actuator/prometheus
- JMX
工具:
- JMX exporter
- VisualVM
- async-profiler十一、本章小结
| 主题 | 关键 |
|---|---|
| 内存 | 合理分代,避免大对象 |
| GC | ZGC/G1,关注暂停 |
| 线程 | 池化、无锁、减少竞争 |
| JIT | 短方法、逃逸分析 |
| 排查 | jmap + MAT |
动手练习
- 用 jmap dump 一次堆,用 MAT 分析大对象
- 用 async-profiler 打火焰图,找 CPU 热点
- 写一段代码,测试线程池合理大小(压测)
- 配置 G1 启动参数,分析 GC 日志