JVM深度分析:性能优化实战指南

1. JVM参数体系

1.1 参数分类

标准参数(-)

特点:所有JVM都支持,向后兼容

bash 复制代码
java -version          # 查看JVM版本
java -help             # 查看帮助
java -showversion      # 显示版本并继续运行

# 常用标准参数
-classpath (-cp)      # 设置类搜索路径
-D<name>=<value>      # 设置系统属性
-verbose:class        # 输出类加载信息
-verbose:gc           # 输出GC信息
非标准参数(-X)

特点:非标准化,不同JVM实现可能不同

bash 复制代码
# 内存设置
-Xms4g               # 初始堆大小(minimum size)
-Xmx8g               # 最大堆大小(maximum size)
-Xmn2g               # 新生代大小(new generation)
-Xss256k             # 栈大小(stack size)

# 其他
-Xint                # 解释执行模式
-Xcomp               # 编译执行模式
-Xmixed              # 混合模式(默认)
高级参数(-XX)

特点:高级选项,用于调优和调试

bash 复制代码
# 布尔类型(+启用/-禁用)
-XX:+UseG1GC                      # 启用G1收集器
-XX:-UseParallelGC                # 禁用Parallel收集器

# 数值类型
-XX:MetaspaceSize=256m            # 元空间初始大小
-XX:MaxGCPauseMillis=200          # 最大GC停顿时间

# 字符串类型
-XX:HeapDumpPath=/logs/dump.hprof # 堆转储文件路径

1.2 常用JVM参数速查

内存配置
bash 复制代码
# 堆内存
-Xms4g                              # 初始堆大小4GB
-Xmx4g                              # 最大堆大小4GB(生产环境建议与Xms相同)
-Xmn2g                              # 新生代大小2GB
-XX:NewRatio=2                      # 老年代/新生代比例=2(即新生代占1/3)
-XX:SurvivorRatio=8                 # Eden/Survivor比例=8(即Eden占80%)

# 栈内存
-Xss256k                            # 每个线程的栈大小256KB

# 元空间(JDK 8+)
-XX:MetaspaceSize=256m              # 初始元空间256MB
-XX:MaxMetaspaceSize=512m           # 最大元空间512MB

# 永久代(JDK 7及以前)
-XX:PermSize=128m                   # 初始永久代128MB
-XX:MaxPermSize=256m                # 最大永久代256MB

# 直接内存
-XX:MaxDirectMemorySize=512m        # 最大直接内存512MB
GC收集器配置
bash 复制代码
# Serial收集器
-XX:+UseSerialGC

# ParNew + CMS
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC

# Parallel
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:ParallelGCThreads=8             # 并行GC线程数

# G1收集器
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200            # 最大停顿时间200ms
-XX:G1HeapRegionSize=16m            # Region大小16MB
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发GC的堆占用比例

# ZGC(JDK 11+)
-XX:+UseZGC
GC行为控制
bash 复制代码
# 对象晋升
-XX:MaxTenuringThreshold=15         # 对象晋升年龄阈值
-XX:PretenureSizeThreshold=3145728  # 大对象直接进入老年代的阈值(3MB)
-XX:TargetSurvivorRatio=90          # Survivor区目标使用率

# CMS配置
-XX:CMSInitiatingOccupancyFraction=75  # 老年代使用75%时触发CMS
-XX:+UseCMSInitiatingOccupancyOnly     # 只根据占用比例触发
-XX:+CMSParallelRemarkEnabled          # 并行重新标记
-XX:+UseCMSCompactAtFullCollection     # Full GC时进行碎片整理
-XX:CMSFullGCsBeforeCompaction=0       # 多少次Full GC后整理(0表示每次)
GC日志配置
bash 复制代码
# JDK 8
-XX:+PrintGC                        # 打印GC基本信息
-XX:+PrintGCDetails                 # 打印GC详细信息
-XX:+PrintGCDateStamps              # 打印GC时间戳(日期格式)
-XX:+PrintGCTimeStamps              # 打印GC时间戳(秒格式)
-XX:+PrintGCApplicationStoppedTime  # 打印应用停顿时间
-Xloggc:/logs/gc.log               # GC日志文件路径

# JDK 9+
-Xlog:gc*:file=/logs/gc.log:time,level,tags
故障诊断
bash 复制代码
# OOM时生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heapdump.hprof

# OOM时执行脚本
-XX:OnOutOfMemoryError="sh /scripts/restart.sh"

# 打印JVM配置
-XX:+PrintFlagsFinal                # 打印所有JVM参数最终值
-XX:+PrintCommandLineFlags          # 打印命令行参数
性能调优
bash 复制代码
# JIT编译
-XX:+TieredCompilation              # 分层编译(JDK 8默认开启)
-XX:ReservedCodeCacheSize=256m      # 代码缓存大小

# 字符串优化
-XX:+UseStringDeduplication         # 字符串去重(G1收集器)

# NUMA优化
-XX:+UseNUMA                        # NUMA优化(多CPU架构)

1.3 查看JVM参数

bash 复制代码
# 查看JVM默认参数
java -XX:+PrintFlagsFinal -version | grep <参数名>

# 示例:查看堆大小默认值
java -XX:+PrintFlagsFinal -version | grep MaxHeapSize

# 查看运行中JVM的参数
jinfo -flags <pid>

# 查看某个具体参数
jinfo -flag MaxHeapSize <pid>

2. 堆内存调优实战

2.1 堆内存设置原则

原则1:Xms = Xmx(避免动态扩容)
bash 复制代码
# 不推荐
-Xms2g -Xmx8g    # 堆会动态扩容,带来性能开销

# 推荐
-Xms8g -Xmx8g    # 固定堆大小,避免扩容

原因

  • 动态扩容会触发Full GC
  • 扩容过程需要重新分配内存
  • 增加GC开销
原则2:堆大小设置建议
bash 复制代码
# 服务器物理内存与堆大小对应关系

物理内存8GB:
  -Xms6g -Xmx6g    # 留2GB给操作系统和其他进程

物理内存16GB:
  -Xms12g -Xmx12g  # 留4GB

物理内存32GB:
  -Xms24g -Xmx24g  # 留8GB

经验值

  • 堆内存占物理内存的 60%-80%
  • 预留内存给操作系统、元空间、直接内存等
原则3:新生代大小设置
bash 复制代码
# 方式1:直接设置
-Xmn2g

# 方式2:通过比例设置
-XX:NewRatio=2    # 老年代/新生代 = 2

# 计算示例
堆大小: 6GB
NewRatio=2
  → 新生代: 6GB / (2+1) = 2GB
  → 老年代: 6GB - 2GB = 4GB

设置建议

  • 新生代占堆的 1/3 到 1/2
  • 对象存活时间短 → 增大新生代
  • 对象存活时间长 → 减小新生代

2.2 实战案例:电商系统调优

业务场景
复制代码
业务:电商订单系统
并发:5000 TPS
内存:32GB物理内存
JDK:JDK 8
初始配置(问题配置)
bash 复制代码
-Xms8g -Xmx8g
-Xmn2g
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=92
问题表现
复制代码
GC日志分析:
  Minor GC: 每秒5次,平均50ms
  Full GC: 每小时3次,平均3s

问题:
  1. Minor GC过于频繁(每秒5次)
  2. Full GC停顿时间长(3s)
  3. 老年代使用率持续增长
问题诊断
1. Minor GC频繁分析
bash 复制代码
# 查看GC日志
[GC (Allocation Failure) [ParNew: 1843200K->204800K(1843200K), 0.0523456 secs]

分析:
  新生代总大小: 1843200K ≈ 1.8GB
  Eden区大小: 1.8GB * 0.8 = 1.44GB

  每秒5次Minor GC → 每次回收约300MB
  → 1.44GB / 300MB = 约5次

结论: 新生代太小,对象分配速率约 1.5GB/s
2. Full GC原因分析
bash 复制代码
# 查看老年代使用率
[Full GC (Allocation Failure) [CMS: 5505024K->4505024K(6029312K), 3.234567 secs]

分析:
  老年代大小: 6029312K ≈ 5.75GB
  使用率: 5505024K / 6029312K = 91.3%

问题:
  1. CMSInitiatingOccupancyFraction=92,触发阈值太高
  2. 并发标记期间,老年代继续增长
  3. 触发Concurrent Mode Failure → Full GC
调优方案
bash 复制代码
# 调优后配置
-Xms24g -Xmx24g                               # 堆增大到24GB
-Xmn8g                                        # 新生代增大到8GB
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70         # 70%时触发CMS
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSParallelRemarkEnabled                 # 并行重新标记
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:MaxTenuringThreshold=6                    # 降低晋升年龄
-XX:SurvivorRatio=8
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/logs/gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heapdump.hprof
调优效果
复制代码
调优前:
  Minor GC: 5次/秒,平均50ms
  Full GC: 3次/小时,平均3s
  吞吐量: 85%

调优后:
  Minor GC: 1次/秒,平均20ms
  Full GC: 0.5次/小时,平均1.5s
  吞吐量: 96%

业务指标:
  接口响应时间: P99从500ms降到100ms
  GC停顿影响降低80%

3. 常见线上问题排查

3.1 问题排查总览

复制代码
线上问题分类:
  ├── CPU问题
  │   ├── CPU 100%
  │   └── CPU使用率低但响应慢
  │
  ├── 内存问题
  │   ├── 内存泄漏
  │   ├── 堆内存溢出
  │   └── 元空间溢出
  │
  ├── 线程问题
  │   ├── 线程死锁
  │   ├── 线程阻塞
  │   └── 线程池满
  │
  └── GC问题
      ├── Full GC频繁
      └── GC停顿时间长

4. CPU问题排查

4.1 CPU 100%问题

问题现象
bash 复制代码
# 查看CPU使用率
top

PID   USER  PR  NI  VIRT  RES  SHR S %CPU %MEM  TIME+  COMMAND
12345 java  20   0  8192m 4g  12m  S 400.0 50.0 10:25.67 java
排查步骤
第一步:找到占用CPU最高的进程
bash 复制代码
top -c
# 或
ps aux | sort -k3 -rn | head -5
第二步:找到进程中占用CPU最高的线程
bash 复制代码
# 方式1:使用top
top -Hp <pid>

# 方式2:使用ps
ps -mp <pid> -o THREAD,tid,time | sort -k2 -rn | head -5

# 示例输出
USER     %CPU PRI SCNT WCHAN  USER SYSTEM   TID     TIME
java     100   19    -     -     -      - 12358 00:05:23
java      95   19    -     -     -      - 12359 00:05:01

# 记录线程ID:12358
第三步:将线程ID转换为16进制
bash 复制代码
printf "%x\n" 12358
# 输出: 3046
第四步:查看线程堆栈
bash 复制代码
# 生成线程dump
jstack <pid> > thread.dump

# 搜索16进制线程ID
grep -A 50 "3046" thread.dump
第五步:分析堆栈信息
java 复制代码
"业务线程-1" #12358 prio=5 os_prio=0 tid=0x00007f8a2c001000 nid=0x3046 runnable
   java.lang.Thread.State: RUNNABLE
        at com.example.service.UserService.calculateHash(UserService.java:123)
        at com.example.service.UserService.processUser(UserService.java:89)
        - locked <0x00000000e0a12345> (a java.lang.Object)
        at com.example.controller.UserController.handle(UserController.java:45)

分析:
  1. 线程状态: RUNNABLE(正在执行)
  2. 代码位置: UserService.java:123的calculateHash方法
  3. 可能原因: 该方法存在死循环或耗时计算

4.2 实战案例:正则表达式导致CPU 100%

问题代码
java 复制代码
public boolean validateEmail(String email) {
    // 问题:正则表达式回溯导致CPU飙升
    String regex = "^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$";
    return email.matches(regex);
}

// 当email很长且格式错误时,正则引擎会大量回溯
// 输入: "aaaaaaaaaaaaaaaaaaaaaaaaa@"
// 导致CPU 100%
解决方案
java 复制代码
public boolean validateEmail(String email) {
    // 方案1:使用预编译的Pattern
    private static final Pattern EMAIL_PATTERN = Pattern.compile(
        "^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*\\.[a-zA-Z]{2,6}$"
    );

    public boolean validateEmail(String email) {
        if (email == null || email.length() > 254) {
            return false;  // 提前校验
        }
        return EMAIL_PATTERN.matcher(email).matches();
    }

    // 方案2:使用更简单的正则
    // 方案3:限制输入长度
}

4.3 快速排查命令脚本

bash 复制代码
#!/bin/bash
# cpu-check.sh - 快速定位CPU高的线程

PID=$1
if [ -z "$PID" ]; then
    echo "Usage: $0 <pid>"
    exit 1
fi

# 获取CPU占用最高的5个线程
echo "=== Top 5 CPU consuming threads ==="
ps -mp $PID -o THREAD,tid,time | sort -k2 -rn | head -6

# 生成线程dump
DUMP_FILE="thread_dump_$(date +%Y%m%d_%H%M%S).txt"
jstack $PID > $DUMP_FILE

echo ""
echo "=== Thread stack analysis ==="
# 分析每个高CPU线程
ps -mp $PID -o THREAD,tid | tail -n +2 | awk '{if($2>50) print $8}' | while read tid; do
    tid_hex=$(printf "%x" $tid)
    echo "Thread $tid (0x$tid_hex):"
    grep -A 20 "nid=0x$tid_hex" $DUMP_FILE | head -21
    echo ""
done

echo "Full thread dump saved to: $DUMP_FILE"

使用方法

bash 复制代码
chmod +x cpu-check.sh
./cpu-check.sh <java-pid>

5. 内存泄漏排查

5.1 内存泄漏的表现

复制代码
症状:
  1. 应用运行一段时间后,内存持续增长
  2. Full GC频繁,但回收效果不明显
  3. 最终导致OutOfMemoryError

5.2 排查步骤

第一步:监控堆内存趋势
bash 复制代码
# 使用jstat监控
jstat -gcutil <pid> 1000

S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00  99.99  45.32  78.23  95.21  92.12   123   1.234     5   1.567   2.801
0.00  99.99  67.45  82.34  95.21  92.12   124   1.245     5   1.567   2.812
0.00  99.99  23.12  85.67  95.21  92.12   125   1.256     5   1.567   2.823

分析:
  O列(老年代)持续增长: 78% → 82% → 85%
  Full GC后内存未明显下降 → 可能存在内存泄漏
第二步:生成堆转储
bash 复制代码
# 方式1:手动生成(只dump存活对象)
jmap -dump:live,format=b,file=heap.hprof <pid>

# 方式2:对比两次堆转储
jmap -dump:format=b,file=heap1.hprof <pid>
# 等待一段时间
jmap -dump:format=b,file=heap2.hprof <pid>

# 方式3:OOM时自动生成
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heapdump.hprof
第三步:使用MAT分析堆转储

MAT(Memory Analyzer Tool)使用步骤

复制代码
1. 打开堆转储文件
   File → Open Heap Dump → 选择heap.hprof

2. 查看Leak Suspects Report(泄漏嫌疑报告)
   MAT会自动分析并标注可能的泄漏点

3. 查看Dominator Tree(支配树)
   显示占用内存最多的对象

4. 查看对象引用链
   右键对象 → Path to GC Roots → exclude weak/soft references
   分析为什么对象无法被回收

5.3 实战案例:ThreadLocal导致内存泄漏

问题代码
java 复制代码
public class UserContext {
    // 问题:ThreadLocal未清理
    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    public static void setUser(User user) {
        userThreadLocal.set(user);
    }

    public static User getUser() {
        return userThreadLocal.get();
    }

    // 缺少清理方法
}

// 在Web应用中使用
@Controller
public class UserController {
    @RequestMapping("/api/user")
    public String handleRequest() {
        User user = new User();
        user.setData(new byte[1024 * 1024]);  // 1MB数据
        UserContext.setUser(user);

        // 处理业务...

        // 问题:请求结束后未清理ThreadLocal
        // Tomcat线程池复用线程 → ThreadLocal未清理 → 内存泄漏
        return "success";
    }
}
MAT分析结果
复制代码
Leak Suspects:
  Thread "http-nio-8080-exec-1" 占用 128MB
    └─ ThreadLocalMap
        └─ Entry[0]
            └─ User对象 (1MB)
        └─ Entry[1]
            └─ User对象 (1MB)
        ...
        └─ Entry[127]
            └─ User对象 (1MB)

分析:
  线程池中的线程未被回收
  每次请求创建的User对象通过ThreadLocal持有
  128个请求 × 1MB = 128MB泄漏
解决方案
java 复制代码
public class UserContext {
    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    public static void setUser(User user) {
        userThreadLocal.set(user);
    }

    public static User getUser() {
        return userThreadLocal.get();
    }

    // 添加清理方法
    public static void clear() {
        userThreadLocal.remove();
    }
}

@Controller
public class UserController {
    @RequestMapping("/api/user")
    public String handleRequest() {
        try {
            User user = new User();
            user.setData(new byte[1024 * 1024]);
            UserContext.setUser(user);

            // 处理业务...
            return "success";
        } finally {
            // 确保清理ThreadLocal
            UserContext.clear();
        }
    }
}

5.4 常见内存泄漏场景

1. 静态集合持有对象
java 复制代码
// 问题代码
public class CacheManager {
    private static Map<String, Object> cache = new HashMap<>();

    public void put(String key, Object value) {
        cache.put(key, value);  // 对象永远不会被回收
    }
}

// 解决方案:使用弱引用或定期清理
public class CacheManager {
    private static Map<String, SoftReference<Object>> cache = new ConcurrentHashMap<>();

    public void put(String key, Object value) {
        cache.put(key, new SoftReference<>(value));
    }

    public Object get(String key) {
        SoftReference<Object> ref = cache.get(key);
        return ref != null ? ref.get() : null;
    }
}
2. 监听器未注销
java 复制代码
// 问题代码
public class EventManager {
    private List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener);
    }

    // 缺少removeListener方法
}

// 解决方案
public class EventManager {
    private List<EventListener> listeners = new ArrayList<>();

    public void addListener(EventListener listener) {
        listeners.add(listener);
    }

    public void removeListener(EventListener listener) {
        listeners.remove(listener);
    }
}
3. 数据库连接未关闭
java 复制代码
// 问题代码
public List<User> queryUsers() {
    Connection conn = dataSource.getConnection();
    PreparedStatement ps = conn.prepareStatement("SELECT * FROM users");
    ResultSet rs = ps.executeQuery();

    List<User> users = new ArrayList<>();
    while (rs.next()) {
        users.add(mapToUser(rs));
    }
    return users;  // 未关闭连接
}

// 解决方案:使用try-with-resources
public List<User> queryUsers() {
    List<User> users = new ArrayList<>();

    try (Connection conn = dataSource.getConnection();
         PreparedStatement ps = conn.prepareStatement("SELECT * FROM users");
         ResultSet rs = ps.executeQuery()) {

        while (rs.next()) {
            users.add(mapToUser(rs));
        }
    } catch (SQLException e) {
        log.error("Query failed", e);
    }

    return users;
}

6. 线程死锁排查

6.1 死锁的表现

复制代码
症状:
  1. 应用hang住,无响应
  2. 部分接口超时
  3. CPU使用率不高,但业务不处理

6.2 排查步骤

第一步:生成线程dump
bash 复制代码
jstack <pid> > thread.dump
第二步:查找死锁信息
bash 复制代码
# jstack会自动检测死锁
grep -A 20 "Found one Java-level deadlock" thread.dump
第三步:分析死锁堆栈
复制代码
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f8a2c002345 (object 0x00000000e0a12345, a java.lang.Object),
  which is held by "Thread-2"

"Thread-2":
  waiting to lock monitor 0x00007f8a2c002678 (object 0x00000000e0a12678, a java.lang.Object),
  which is held by "Thread-1"

Java stack information:
"Thread-1":
        at com.example.service.OrderService.createOrder(OrderService.java:45)
        - waiting to lock <0x00000000e0a12345> (a java.lang.Object)
        - locked <0x00000000e0a12678> (a java.lang.Object)

"Thread-2":
        at com.example.service.InventoryService.updateInventory(InventoryService.java:67)
        - waiting to lock <0x00000000e0a12678> (a java.lang.Object)
        - locked <0x00000000e0a12345> (a java.lang.Object)

6.3 实战案例:经典死锁

问题代码
java 复制代码
public class DeadlockExample {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {
            System.out.println("Thread-1: 持有lock1,等待lock2");
            sleep(100);

            synchronized (lock2) {
                System.out.println("Thread-1: 获得lock2");
            }
        }
    }

    public void method2() {
        synchronized (lock2) {
            System.out.println("Thread-2: 持有lock2,等待lock1");
            sleep(100);

            synchronized (lock1) {
                System.out.println("Thread-2: 获得lock1");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();

        new Thread(() -> example.method1()).start();
        new Thread(() -> example.method2()).start();

        // 发生死锁!
    }
}
解决方案
java 复制代码
// 方案1:固定加锁顺序
public class FixedOrderLock {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void method1() {
        synchronized (lock1) {  // 总是先锁lock1
            synchronized (lock2) {
                // 业务逻辑
            }
        }
    }

    public void method2() {
        synchronized (lock1) {  // 同样先锁lock1
            synchronized (lock2) {
                // 业务逻辑
            }
        }
    }
}

// 方案2:使用tryLock(ReentrantLock)
public class TryLockExample {
    private final ReentrantLock lock1 = new ReentrantLock();
    private final ReentrantLock lock2 = new ReentrantLock();

    public void method1() throws InterruptedException {
        while (true) {
            if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            // 业务逻辑
                            return;
                        } finally {
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            }
            // 获取锁失败,稍后重试
            Thread.sleep(100);
        }
    }
}

// 方案3:使用lock ordering(基于hashCode)
public class HashCodeOrderLock {
    public void transfer(Account from, Account to, int amount) {
        Account first = from.hashCode() < to.hashCode() ? from : to;
        Account second = from.hashCode() < to.hashCode() ? to : from;

        synchronized (first) {
            synchronized (second) {
                from.debit(amount);
                to.credit(amount);
            }
        }
    }
}

7. OOM问题分析

7.1 OOM类型分类

1. Java heap space
复制代码
java.lang.OutOfMemoryError: Java heap space

原因:
  - 堆内存不足
  - 内存泄漏
  - 对象创建过多

排查:
  1. 分析堆转储文件
  2. 查看大对象
  3. 检查是否有内存泄漏
2. GC overhead limit exceeded
复制代码
java.lang.OutOfMemoryError: GC overhead limit exceeded

原因:
  - GC时间占比过高(超过98%)
  - 回收效果差(回收不到2%)

排查:
  1. 分析GC日志
  2. 检查是否有大量小对象
  3. 考虑增大堆内存
3. Metaspace
复制代码
java.lang.OutOfMemoryError: Metaspace

原因:
  - 动态生成大量类(CGLib、ASM等)
  - 类加载器泄漏

排查:
  1. 增大元空间: -XX:MaxMetaspaceSize=512m
  2. 检查动态代理使用
  3. 检查类加载器是否泄漏
4. Direct buffer memory
复制代码
java.lang.OutOfMemoryError: Direct buffer memory

原因:
  - NIO DirectByteBuffer分配过多
  - 直接内存未释放

排查:
  1. 增大直接内存: -XX:MaxDirectMemorySize=512m
  2. 检查DirectByteBuffer使用
  3. 确保显式释放: ((DirectBuffer)buffer).cleaner().clean()
5. unable to create new native thread
复制代码
java.lang.OutOfMemoryError: unable to create new native thread

原因:
  - 线程创建过多
  - 操作系统限制

排查:
  1. 检查线程数: jstack <pid> | grep "^\"" | wc -l
  2. 增大系统限制: ulimit -u
  3. 优化线程池配置
  4. 减小线程栈大小: -Xss256k

7.2 OOM实战案例

案例1:大文件上传导致堆OOM
java 复制代码
// 问题代码
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    byte[] bytes = file.getBytes();  // 将整个文件加载到内存

    // 处理文件...

    return "success";
}

// 当上传500MB文件时 → 堆内存不足 → OOM

解决方案

java 复制代码
// 方案1:流式处理
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    try (InputStream inputStream = file.getInputStream();
         FileOutputStream outputStream = new FileOutputStream(destFile)) {

        byte[] buffer = new byte[8192];  // 8KB缓冲区
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
    }
    return "success";
}

// 方案2:分片上传
// 前端将大文件分成多个小片段上传
案例2:缓存未设置过期导致OOM
java 复制代码
// 问题代码
public class CacheManager {
    private static Map<String, byte[]> cache = new ConcurrentHashMap<>();

    public void put(String key, byte[] data) {
        cache.put(key, data);  // 永不过期
    }
}

// 缓存数据持续增长 → 堆内存耗尽 → OOM

解决方案

java 复制代码
// 方案1:使用Guava Cache
private static LoadingCache<String, byte[]> cache = CacheBuilder.newBuilder()
    .maximumSize(10000)                      // 最大条目数
    .expireAfterWrite(30, TimeUnit.MINUTES)  // 写入后30分钟过期
    .build(new CacheLoader<String, byte[]>() {
        @Override
        public byte[] load(String key) {
            return loadData(key);
        }
    });

// 方案2:使用Caffeine
private static Cache<String, byte[]> cache = Caffeine.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(Duration.ofMinutes(30))
    .build();

8. 性能调优最佳实践

8.1 调优黄金法则

复制代码
1. 不要过早优化
   - 先保证功能正确
   - 再基于数据进行优化

2. 基于数据驱动
   - 监控指标(GC、内存、CPU)
   - GC日志分析
   - 性能测试数据

3. 一次只改一个参数
   - 对比优化前后效果
   - 确定参数影响

4. 生产环境验证
   - 先在测试环境验证
   - 灰度发布到生产环境
   - 监控关键指标

8.2 不同应用场景的推荐配置

场景1:Web应用(注重响应时间)
bash 复制代码
# 服务器: 16GB内存,8核CPU
# 特点: 请求量大,要求响应快

# JDK 8 - CMS
-Xms8g -Xmx8g
-Xmn3g
-Xss256k
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSParallelRemarkEnabled
-XX:MaxTenuringThreshold=6
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heapdump.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/logs/gc.log

# JDK 11+ - G1
-Xms8g -Xmx8g
-Xss256k
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:G1HeapRegionSize=16m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heapdump.hprof
场景2:批处理应用(注重吞吐量)
bash 复制代码
# 服务器: 32GB内存
# 特点: 大数据量处理,对停顿时间不敏感

-Xms24g -Xmx24g
-Xmn8g
-Xss256k
-XX:MetaspaceSize=512m
-XX:MaxMetaspaceSize=512m
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:ParallelGCThreads=8
-XX:MaxGCPauseMillis=500
-XX:GCTimeRatio=19  # 吞吐量95%
场景3:微服务应用(小堆内存)
bash 复制代码
# 服务器: 4GB内存
# 特点: 多实例部署,单实例内存小

-Xms2g -Xmx2g
-Xmn1g
-Xss256k
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200

8.3 JVM调优Checklist

复制代码
□ 堆内存配置
  □ Xms = Xmx(避免动态扩容)
  □ 堆大小占物理内存60%-80%
  □ 新生代大小合理(1/3到1/2)

□ GC收集器选择
  □ 堆<4GB → CMS
  □ 堆>8GB → G1
  □ 低延迟要求 → ZGC/Shenandoah

□ GC日志配置
  □ 开启详细GC日志
  □ 记录日期时间戳
  □ 日志轮转(避免日志文件过大)

□ OOM诊断配置
  □ 开启HeapDumpOnOutOfMemoryError
  □ 设置HeapDumpPath

□ 元空间配置
  □ MetaspaceSize = MaxMetaspaceSize
  □ 大小根据应用类数量设置

□ 监控指标
  □ GC频率和停顿时间
  □ 堆内存使用率
  □ CPU使用率
  □ 接口响应时间

9. 面试重点总结

9.1 高频面试题

Q1: 线上应用CPU 100%,如何排查?

答案

bash 复制代码
# 1. 找到占用CPU高的进程
top

# 2. 找到进程中占用CPU高的线程
top -Hp <pid>

# 3. 将线程ID转换为16进制
printf "%x\n" <tid>

# 4. 查看线程堆栈
jstack <pid> | grep -A 50 <tid-hex>

# 5. 分析代码
定位到具体代码行,分析是否有死循环、正则回溯、频繁GC等问题
Q2: 如何排查内存泄漏?

答案

复制代码
1. 监控堆内存趋势
   - jstat -gcutil <pid> 1000
   - 观察老年代是否持续增长

2. 生成堆转储
   - jmap -dump:live,format=b,file=heap.hprof <pid>

3. 使用MAT分析
   - Leak Suspects Report
   - Dominator Tree
   - Path to GC Roots

4. 定位代码
   - 查看哪些对象占用内存最多
   - 分析为什么无法被回收
   - 修复代码(清理ThreadLocal、移除监听器等)
Q3: Full GC频繁,如何优化?

答案

复制代码
1. 分析原因
   □ 老年代空间不足 → 增大堆内存
   □ 元空间不足 → 增大元空间
   □ 晋升过快 → 调整新生代大小/晋升年龄
   □ 大对象直接进入老年代 → 调整PretenureSizeThreshold

2. 优化措施
   - 增大堆内存: -Xmx
   - 增大新生代: -Xmn
   - 调整CMS触发阈值: -XX:CMSInitiatingOccupancyFraction=70
   - 降低晋升年龄: -XX:MaxTenuringThreshold=6

3. 考虑更换收集器
   - CMS → G1(堆>8GB)
   - G1 → ZGC(超低延迟要求)
Q4: 如何进行JVM调优?

答案

复制代码
调优步骤:
1. 确定目标
   - 降低延迟 vs 提高吞吐量 vs 节省内存

2. 收集数据
   - 开启GC日志
   - 监控关键指标(GC频率、停顿时间、内存使用)

3. 分析问题
   - GC日志分析(GCEasy)
   - 堆转储分析(MAT)
   - 线程分析(jstack)

4. 调整参数
   - 一次只改一个参数
   - 对比优化前后效果

5. 验证效果
   - 压力测试
   - 生产环境灰度验证
Q5: 生产环境推荐的JVM参数?

答案

bash 复制代码
# 通用配置(8GB堆,JDK 11+)
-Xms8g -Xmx8g
-Xss256k
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=45
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/logs/heapdump.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/logs/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=100M

关键点:
  1. Xms = Xmx(避免动态扩容)
  2. 开启HeapDumpOnOutOfMemoryError
  3. 开启详细GC日志
  4. 根据堆大小选择合适的收集器

9.2 实战技能清单

复制代码
□ 监控工具
  □ jps - 查看Java进程
  □ jstat - 查看GC统计
  □ jmap - 生成堆转储
  □ jstack - 生成线程dump
  □ jinfo - 查看JVM参数

□ 分析工具
  □ MAT - 堆转储分析
  □ GCEasy - GC日志分析
  □ VisualVM - 实时监控
  □ Arthas - 线上诊断

□ 问题排查
  □ CPU 100% → top + jstack
  □ 内存泄漏 → jmap + MAT
  □ 死锁 → jstack + 分析
  □ Full GC频繁 → GC日志分析

□ 性能调优
  □ 堆内存调优
  □ GC收集器选择
  □ GC参数调优
  □ 代码优化
相关推荐
ujainu2 小时前
CANN pto-isa:PTO 性能优化的指令调度与硬件特化
性能优化·ascend
青山师2 小时前
B+树与InnoDB索引深度解析:数据库索引的底层原理与工程实践
数据结构·数据库·b树·性能优化·b+树·索引优化·mysql性能
Dicky-_-zhang2 小时前
JWT令牌安全实践详解
java·jvm
tongluowan0073 小时前
jvm垃圾回收器 - ZGC
java·jvm·zgc·垃圾回收器
Dicky-_-zhang3 小时前
API安全设计与防护实战
java·jvm
接着奏乐接着舞4 小时前
vscode 给 Maven 启动的 JVM 加上 `-Dfile.encoding=UTF-8`
jvm·vscode·maven
Dicky-_-zhang4 小时前
微服务安全防护实战:OAuth2与JWT鉴权
java·jvm
超梦dasgg4 小时前
Java 生产环境 JVM 调优实战
java·开发语言·jvm
waitingforloveJJ4 小时前
计算机视觉算子库性能优化与实战
人工智能·计算机视觉·性能优化