JVM调优详解(二)
目录
JVM内存分配详解
一、参数解析
二、详细计算过程(MB单位)
三、内存分布可视化
四、关键配置解析
五、监控脚本详解
六、参数优化建议
七、配置验证流程
JVM内存分配详解
root 40014 1 24 11:43 pts/0 00:01:53 /usr/local/jdk/jdk1.8.0_161/bin/java -Dname=ldey-admin.jar -Duser.timezone=Asia/Shanghai -Xms1024m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -Xloggc:/usr/local/ldey/jar/logs/ldey-admin-gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:NewRatio=1 -XX:SurvivorRatio=6 -XX:MaxTenuringThreshold=15 -XX:+UseParallelGC -XX:+UseParallelOldGC -jar /usr/local/ldey/jar/ldey-admin.jar --spring.profiles.active=druid-prod
jstat -gc 40014
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
#每秒统计垃圾回收的堆信息,打印10次,
jstat -gc 40014 1000 10
- pid: java进程号
- interval: 间隔时间,单位为秒或毫秒
- count: 打印次数,不填则默认一直打印
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
43520.0 42496.0 25192.3 0.0 438272.0 400934.9 524288.0 49220.6 86272.0 82255.7 10752.0 9929.8 10 0.292 0 0.000 0.292
如果需要转换为MB或GB,可以:
- 手动计算:KB值/1024=MB
- 使用awk处理:
jstat -gc <pid> 1000 1 | awk 'NR==1 {print $0} NR>1 {for(i=1;i<=NF;i++) {if($i~/^[0-9]/) printf "%.1f ", $i/1024; else printf "%s ", $i} printf "\n"}'
jstat -gc 是 JVM 内置的监控工具,用于显示堆内存各区域的实际容量和使用量(KB单位)。
基本用法: jstat -gc <pid> [interval] [count]
<pid>
:Java 进程 IDinterval
:采样间隔(毫秒)count
:采样次数
命令参数详解:
列名 | 全称 | 说明 | 单位 |
S0C | Survivor 0 Capacity | 新生代 Survivor 0区容量 | KB |
S1C | Survivor 1 Capacity | 新生代 Survivor 1区容量 | KB |
S0U | Survivor 0 Used | 新生代 Survivor 0区已使用 | KB |
S1U | Survivor 1 Used | 新生代 Survivor 1区已使用 | KB |
EC | Eden Capacity | 新生代 Eden区容量 | KB |
EU | Eden Used | 新生代 Eden区已使用 | KB |
OC | Old Capacity | 老年代容量 | KB |
OU | Old Used | 老年代已使用 | KB |
MC | Metaspace Capacity | 元空间容量 | KB |
MU | Metaspace Used | 元空间已使用 | KB |
CCSC | Compressed Class Space Capacity | 压缩类空间容量 | KB |
CCSU | Compressed Class Space Used | 压缩类空间已使用 | KB |
YGC | Young GC Count | 年轻代GC次数 | 无 |
YGCT | Young GC Time | 年轻代GC累计时间 | 秒 |
FGC | Full GC Count | Full GC次数 | 无 |
FGCT | Full GC Time | Full GC累计时间 | 秒 |
GCT | Total GC Time | GC总时间 | 秒 |
jstat -gcutil 是 JVM 监控工具,用于显示垃圾回收统计信息的百分比格式,比 jstat -gc 更直观。
基本用法: jstat -gcutil <pid> [interval] [count]
<pid>
:Java 进程 IDinterval
:采样间隔(毫秒)count
:采样次数
列名 | 全称 | 含义 | 正常范围 |
| Survivor 0 使用率 | Survivor 0 区已用百分比 | 0%-100% |
| Survivor 1 使用率 | Survivor 1 区已用百分比 | 0%-100% |
| Eden 使用率 | Eden 区已用百分比 | <90%(避免频繁GC) |
| Old 使用率 | 老年代已用百分比 | <75%(预警阈值) |
| Metaspace 使用率 | 元空间已用百分比 | <80% |
| Compressed Class Space 使用率 | 压缩类空间使用率 | <80% |
| Young GC Count | 年轻代GC次数 | - |
| Young GC Time | 年轻代GC总时间(秒) | - |
| Full GC Count | Full GC次数 | 越少越好 |
| Full GC Time | Full GC总时间(秒) | - |
| Total GC Time | GC总时间(秒) | - |
一、参数解析
-Xms1024m -Xmx1024m # 固定堆内存为1024MB(避免动态扩展)
-XX:NewRatio=1 # 新生代与老年代1:1分配
-XX:SurvivorRatio=6 # Eden区与单个Survivor区6:1分配
-XX:MaxTenuringThreshold=15 # 对象经历15次GC才晋升老年代
二、详细计算过程(MB单位)
总堆内存分配
初始值和最大值相同都为1024MB
即总堆内存 = Xms值 = Xmx值 = 1024MB
新生代与老年代划分
新生代 = 总堆 / (NewRatio + 1) = 1024 / (1 + 1) = 512MB
老年代 = 总堆 - 新生代 = 1024 - 512 = 512MB
新生代内部区域计算
总比例份数 = SurvivorRatio + 2 = 6 + 2 = 8份
Eden区 = 新生代 × (SurvivorRatio/总份数) = 512 × (6/8) = 384MB
单个Survivor区 = 新生代 × (1/总份数) = 512 × (1/8) = 64MB新生代 = Eden区 + Survivor 0 区 + Survivor 1区 = 384 + 64 + 64 = 512MB
元空间内存
元空间初始 = 128MB(-XX:MetaspaceSize)
元空间最大 = 512MB(-XX:MaxMetaspaceSize)
三、内存分布可视化
理论计算:
堆内存 (1024MB)
├─ 新生代 (512MB)
│ ├─ Eden区 (384MB) # 新对象分配区域
│ ├─ Survivor0 (64MB) # From Survivor
│ └─ Survivor1 (64MB) # To Survivor
└─ 老年代 (512MB) # 长期存活对象实际分配:
堆内存 (1024MB)
├─ 新生代 (512MB)
│ ├─ Eden区 (428.5MB) # 新对象分配区域
│ ├─ Survivor0 (42.5MB) # From Survivor
│ └─ Survivor1 (41.0MB) # To Survivor
└─ 老年代 (512MB) # 长期存活对象
特别注意:JVM 的内存分配可能与理论计算不完全一致,这通常是由以下几个原因导致的
1.JVM 内存对齐和最小/最大限制
JVM 在分配内存时可能会进行内存对齐(Memory Alignment),以确保性能优化。例如:某些 JVM 实现会向上取整到特定倍数(如 1MB、4MB 等),导致实际分配的内存略大于计算值。Survivor 区的最小限制:即使 SurvivorRatio=6,某些 JVM 可能会强制 Survivor 区至少占用一定比例(如 5%),而不是严格按照 6:1:1 分配。
2.自适应调整(Adaptive Sizing)
如果启用了 -XX:+UseAdaptiveSizePolicy(Parallel GC 默认开启),JVM 可能会动态调整 Eden 和 Survivor 的比例,以优化 GC 性能。这可能导致:Eden 区比计算值更大,Survivor 区更小。老年代的实际占用可能因晋升策略而变化。
3.元空间(Metaspace)的影响
虽然 -Xmx 和 -Xms 控制堆内存,但 Metaspace(类元数据) 是独立分配的:如果 Metaspace 占用过高,可能会影响可用堆内存(但通常不会直接影响 NewRatio 分 配)。可以使用 jstat -gc 查看 Metaspace 的实际使用情况。
4.垃圾收集器(GC)的特定行为
不同的 GC 对内存分配策略不同:Parallel GC(-XX:+UseParallelGC):默认开启 UseAdaptiveSizePolicy,可能导致 Survivor 区大小动态变化。
G1 GC(-XX:+UseG1GC):没有固定的 NewRatio,而是按 Region 动态分配。
CMS(-XX:+UseConcMarkSweepGC):可能更严格遵循 NewRatio,但仍可能受 JVM 优化影响。
5.系统限制和 JVM 版本差异
操作系统内存限制:如果物理内存不足,JVM 可能无法分配全部 -Xmx 内存。不同 JVM 版本(如 OpenJDK vs. Oracle JDK)可能有不同的内存分配策略。
四、关键配置解析
SurvivorRatio优化
原始值30:1:1 → 调整后6:1:1
Survivor区从16MB → 64MB(理论值)效果:减少对象过早晋升老年代
晋升阈值
-XX:MaxTenuringThreshold=15
- 对象需经历15次Minor GC才晋升
- 配合更大的Survivor区显著降低老年代压力
GC策略
-XX:+UseParallelGC -XX:+UseParallelOldGC
- Parallel Scavenge + Parallel Old组合
- 吞吐量优先,适合后台处理型应用
五、监控脚本详解
#!/bin/sh
pid=`ps -ef | grep ldey | grep -v grep | awk '{print $2}'`
# 默认1秒刷新一次
interval=1000
# 打印次数
count=10jstat -gc $pid $interval $count | awk '
BEGIN {# 列宽对齐的MB单位表头header=" S0C_MB S1C_MB S0U_MB S1U_MB EC_MB EU_MB OC_MB OU_MB MC_MB MU_MB CCSC_MB CCSU_MB YGC YGCT_秒 FGC FGCT_秒 GCT_秒"gsub(/_[A-Z秒]+/, "\033[1;36m&\033[0m", header) # 给单位加蓝色print header
}
NR>1 {# 设置颜色:Survivor黄,Eden蓝,Old红,元空间紫printf "\033[33m%8.1f\033[0m \033[33m%8.1f\033[0m ", $1/1024, $2/1024;printf "\033[32m%8.1f\033[0m \033[32m%8.1f\033[0m ", $3/1024, $4/1024;printf "\033[34m%9.1f\033[0m \033[34m%9.1f\033[0m ", $5/1024, $6/1024;printf "\033[31m%10.1f\033[0m \033[31m%10.1f\033[0m ", $7/1024, $8/1024;printf "\033[35m%8.1f %8.1f %8.1f %8.1f\033[0m ", $9/1024, $10/1024, $11/1024, $12/1024;printf "%6d \033[1;33m%8.3f\033[0m %6d \033[1;31m%8.3f\033[0m %8.3f\n", $13, $14, $15, $16, $17
}'jstat -gcutil $pid $interval $count | awk '
BEGIN {# 列宽对齐的MB单位表头header=" S0_% S1_% E_% O_% M_% CCS_% YGC YGCT_秒 FGC FGCT_秒 GCT_秒"gsub(/_[%秒]+/, "\033[1;36m&\033[0m", header) # 给单位加蓝色print header
}
NR>1 {# 设置颜色:Survivor黄,Eden蓝,Old红,元空间紫printf "\033[33m%8.1f\033[0m \033[33m%8.1f\033[0m ", $1, $2;printf "\033[32m%8.1f\033[0m \033[32m%8.1f\033[0m ", $3, $4;printf "\033[34m%9.1f\033[0m \033[34m%9.1f\033[0m ", $5, $6;printf "\033[31m%10.1f\033[0m \033[31m%10.1f\033[0m ", $7, $8;printf "%6d \033[1;33m%8.3f\033[0m %8.3f\n", $9, $10, $11
}'# 获取新生代 Eden 区使用率
EDEN_USAGE=$(jstat -gcutil $pid 1000 1 | awk 'NR==2 {print $3}')
if (( $(echo "$EDEN_USAGE > 80" | bc -l) )); thenecho "警告:新生代Eden区使用率超过 80%,当前值: ${EDEN_USAGE}%"
elseecho "新生代Eden区使用率正常: ${EDEN_USAGE}%"
fi# 获取新生代 Survivor 区使用率(取 S0 和 S1 的最大值)
SURVIVOR_USAGE=$(jstat -gcutil $pid 1000 1 | awk 'NR==2 {print ($1 > $2 ? $1 : $2)}')# 判断是否 > 80%
if (( $(echo "$SURVIVOR_USAGE > 80" | bc -l) )); thenecho "警告:新生代Survivor区使用率超过 80%,当前值: ${SURVIVOR_USAGE}%"
elseecho "新生代Survivor区使用率正常: ${SURVIVOR_USAGE}%"
fi# 判断老年代的使用率
OLD_USAGE=$(jstat -gcutil $pid 1000 1 | awk 'NR==2 {print $4}')
if (( $(echo "$OLD_USAGE > 80" | bc -l) )); thenecho "警告:老年代使用率超过 80%,当前值: ${OLD_USAGE}%"
elseecho "老年代使用率正常: ${OLD_USAGE}%"
fi
执行效果示例:
六、参数优化建议
新生代Eden区监控
# 获取新生代 Eden 区使用率
EDEN_USAGE=$(jstat -gcutil $pid 1000 1 | awk 'NR==2 {print $3}')
if (( $(echo "$EDEN_USAGE > 80" | bc -l) )); thenecho "警告:新生代Eden区使用率超过 80%,当前值: ${EDEN_USAGE}%"
elseecho "新生代Eden区使用率正常: ${EDEN_USAGE}%"
fi
新生代Survivor区监控
# 计算Survivor使用率
# 获取新生代 Survivor 区使用率(取 S0 和 S1 的最大值)
SURVIVOR_USAGE=$(jstat -gcutil $pid 1000 1 | awk 'NR==2 {print ($1 > $2 ? $1 : $2)}')# 判断是否 > 80%
if (( $(echo "$SURVIVOR_USAGE > 80" | bc -l) )); thenecho "警告:新生代Survivor区使用率超过 80%,当前值: ${SURVIVOR_USAGE}%"
elseecho "新生代Survivor区使用率正常: ${SURVIVOR_USAGE}%"
fi
# 若持续 > 80%,需考虑增大SurvivorRatio
老年代区监控
# 判断老年代的使用率
OLD_USAGE=$(jstat -gcutil $pid 1000 1 | awk 'NR==2 {print $4}')
if (( $(echo "$OLD_USAGE > 80" | bc -l) )); thenecho "警告:老年代使用率超过 80%,当前值: ${OLD_USAGE}%"
elseecho "老年代使用率正常: ${OLD_USAGE}%"
fi
GC日志分析
# 检查Full GC频率
grep "Full GC" ldey-admin-gc.log | wc -l
七、配置验证流程
启动应用后立即检查
jmap -heap <PID>
压测期间监控
watch -n 1 'jstat -gc <pid> | awk "...MB转换脚本..."'
长期运行分析
# 使用GCViewer分析日志
gcviewer ldey-admin-gc.log
该配置通过精细的内存划分,在吞吐量和内存效率之间取得平衡,特别适合中等规模的Spring Boot应用。监控脚本可清晰直观的展现JVM内存区域的占用情况,而更好的调整,以助于让JVM运行达到最优,在实际运行中建议结合业务峰值持续优化。