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参数调优
□ 代码优化