——从零掌握JVM监控三剑客
基础三剑客:
-dump:live
防STW)。进阶工具:
内存分析:
自动化监控:
jstat
指标,阈值触发堆转储与告警。hprof
并压缩存档。安全防护:
jmxremote.password
)、权限控制、IP白名单。chmod 750 $JAVA_HOME/bin/jmap
。容器化适配:
-XX:MaxRAMPercentage=70.0
替代固定-Xmx
。jstat
内存使用率检测。OOM问题:
性能瓶颈:
场景 | 首选工具 | 替代方案 |
---|---|---|
生产环境实时诊断 | Arthas | jcmd + 脚本 |
内存泄漏分析 | MAT + OQL | jhat(基础排查) |
Native内存泄漏 | Valgrind + jemalloc | pmap 对比采样 |
容器内诊断 | jattach + 预装诊断镜像 | kubectl exec + jcmd |
gc.log
、jstat -gcutil
、APM指标决策。-XX:SurvivorRatio
),对比监控。注:工具是手段,核心是对JVM机制的理解。掌握G1
的SATB算法、ZGC
的颜色指针等底层原理,方能真正游刃有余。
年轻代 (Young Generation)
老年代 (Old Generation)
元空间 (Metaspace)
public class ObjectLife {
public static void main(String[] args) {
// 阶段1:对象诞生于Eden
byte[] obj1 = new byte[2 * 1024 * 1024];
// 阶段2:经历Minor GC后进入Survivor
System.gc();
// 阶段3:长期存活晋升至Old Gen
for(int i=0; i<15; i++){
System.gc();
Thread.sleep(100);
}
// 阶段4:触发Full GC
byte[] obj2 = new byte[4 * 1024 * 1024];
}
}
监控工具验证:
# 使用jstat观察内存变化
jstat -gc <pid> 1000 10
输出字段解析:
S0C S1C S0U S1U EC EU OC OU MC MU
512.0 512.0 0.0 0.0 33280.0 10240.0 87552.0 0.0 4480.0 778.4
GC类型 | 分区特点 | 监控要点 |
---|---|---|
Serial | 传统三代结构 | Young/Old区大小比例 |
CMS | 老年代使用空闲列表 | 内存碎片率 |
G1 | 等大小Region(1-32MB) | 大对象专属Humongous Region |
ZGC | 虚拟内存映射(无物理分区) | 内存重映射次数 |
-XX:PretenureSizeThreshold
(默认1MB)直接进入老年代// 模拟不可复现的内存泄漏
public class GhostCrash {
private static Map<User, byte[]> cache = new WeakHashMap<>();
public static void main(String[] args) {
// 当User对象被外部强引用时发生泄漏
User user = new User("admin");
cache.put(user, new byte[1024 * 1024 * 100]); // 缓存100MB数据
// 业务代码运行后user未释放...
}
}
现象特征:
byte[]
占用量与代码逻辑不符工具价值:
jstat -gcutil
实时监控内存波动jmap -histo
快速定位异常对象线上典型场景:
排查流程:
jstat -gccause 1s
观察GC触发原因jstack -l > threadDump.log
抓取线程快照VM Thread
占用CPU过高(GC线程)# 通过JMX连接可能引发的安全问题
-Dcom.sun.management.jmxremote.port=7091
-Dcom.sun.management.jmxremote.authenticate=false # 关闭认证
潜在风险:
容器化困境:
命令行优势:
kubectl exec <pod-name> -- jstack <pid> > dump.log
监控方式 | CPU占用率 | 内存开销 | 数据精度 |
---|---|---|---|
VisualVM | 3.2% | 300MB | 1秒级采样 |
JMC Flight Recorder | 1.1% | 150MB | 毫秒级采样 |
jstat命令 | 0.3% | 0MB | 实时数据 |
# 监控脚本自动触发堆转储
import subprocess
import psutil
def check_memory(pid):
output = subprocess.check_output(f"jstat -gcutil {pid}", shell=True)
old_gen_usage = float(output.splitlines()[1].split()[3])
if old_gen_usage > 80.0:
subprocess.run(f"jmap -dump:live,file=/tmp/heap_{pid}.hprof {pid}", shell=True)
send_alert("内存告警!已自动捕获堆转储")
for proc in psutil.process_iter():
if "java" in proc.name():
check_memory(proc.pid)
实现效果:
核心价值:
典型应用场景:
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
参数拆解:
参数 | 作用 | 示例值 |
---|---|---|
-option |
监控维度(必选) | -gcutil |
-t |
显示时间戳 | 无附加参数 |
-h |
每输出N行显示一次表头 | -h5 |
vmid |
目标JVM进程ID | 12345 |
interval |
采样间隔(秒/毫秒) | 1000 或 1s |
count |
总采样次数(可选) | 10 |
常用Option清单:
选项 | 监控重点 |
---|---|
-class |
类加载/卸载统计 |
-gc |
各分区容量及使用量 |
-gccause |
GC原因(最近一次/当前) |
-gcutil |
各分区使用率百分比 |
-gcutil
输出解读jstat -gcutil 12345 1s
输出示例:
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 99.80 68.43 85.92 95.11 91.45 1349 63.451 12 8.134 71.585
字段解析:
-gccause
实战技巧jstat -gccause 12345 3s 5
输出示例:
LGCC GCC
Allocation Failure No GC
关键字段:
Allocation Failure
:新生代空间不足System.gc()
:代码中显式调用GCMetadata GC Threshold
:元空间扩容触发某订单服务频繁出现1秒以上的卡顿,怀疑Full GC导致。
jstat -gcutil -t -h10 12345 1000 60
Timestamp S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
1024.3 0.00 99.99 15.67 98.34 95.11 91.45 1350 63.451 13 8.134 71.585
1025.3 0.00 99.99 16.23 98.34 95.11 91.45 1350 63.451 13 8.134 71.585
1026.3 0.00 99.99 99.88 98.34 95.11 91.45 1350 63.451 13 8.134 71.585
1027.3 99.99 0.00 0.00 99.99 95.11 91.45 1351 63.478 14 8.456 71.934
异常信号:
通过jmap
进一步分析老年代对象:
jmap -histo:live 12345 | head -n 20
发现OrderDetailDTO
对象实例异常多,存在缓存未设置TTL的问题。
-XX:NewRatio=2
→ -XX:NewRatio=1
O列稳定在75%-80%,FGC频率降至每天1次
#!/bin/bash
PID=$1
THRESHOLD=90 # 老年代使用率阈值
while true; do
OLD_GEN=$(jstat -gcutil $PID | awk '{print $4}')
if (( $(echo "$OLD_GEN > $THRESHOLD" | bc -l) )); then
echo "[$(date)] 老年代使用率超过阈值: ${OLD_GEN}%" >> gc_alert.log
# 触发堆转储或发送告警
fi
sleep 5
done
脚本功能:
jstat
是实时监控GC行为的瑞士军刀-gcutil
与-gccause
快速定位GC诱因三大核心能力:
典型应用场景:
jmap [option] <pid>
常用Option列表:
参数 | 作用 |
---|---|
-heap |
打印堆配置信息(GC算法、堆大小) |
-histo[:live] |
生成对象直方图(存活对象) |
-dump:format=b,file=filename |
生成堆转储文件 |
-clstats |
类加载器统计信息 |
OutOfMemoryError: Java heap space
jstat
监控显示老年代内存持续增长步骤1:生成存活对象直方图
jmap -histo:live 12345 | head -n 20
关键输出:
num #instances #bytes class name
----------------------------------------------
1: 258,342 207179296 [B # byte数组
2: 880,129 42246192 java.util.LinkedList$Node
3: 502,341 36168552 java.lang.reflect.Method
异常信号:[B
(byte数组)占用近200MB
步骤2:生成堆转储文件
jmap -dump:live,format=b,file=heap.hprof 12345
文件分析工具:
步骤3:MAT分析定位
heap.hprof
文件Leak Suspects Report
ThreadLocal
中缓存了未释放的字节缓冲区public class CacheManager {
private static ThreadLocal<ByteBuffer> bufferCache = new ThreadLocal<>();
public static void initBuffer() {
bufferCache.set(ByteBuffer.allocate(1024 * 1024)); // 每个线程缓存1MB
}
// 缺少remove()清理逻辑!
}
修复方案:
public static void releaseBuffer() {
bufferCache.remove(); // 在finally块中调用
}
import subprocess
import os
def analyze_heap(pid):
# 生成转储文件
dump_file = f"heap_{pid}_{int(time.time())}.hprof"
subprocess.run(f"jmap -dump:live,format=b,file={dump_file} {pid}", shell=True)
# 上传到分析服务器
os.system(f"scp {dump_file} user@analysis-server:/dumps/")
# 触发MAT自动分析
ssh_cmd = f"mat/ParseHeapDump.sh {dump_file} -leak_suspects"
subprocess.run(f'ssh user@analysis-server "{ssh_cmd}"', shell=True)
./ParseHeapDump.sh heap.hprof org.eclipse.mat.api:suspects
<problem>
<description>
<class name="java.lang.ThreadLocal" />
<accumulated_objects> 1,024 accumulated_objects>
<accumulated_size> 1.2 GB accumulated_size>
description>
problem>
转储文件敏感信息:
-live
选项只转储存活对象服务暂停风险:
转储方式 | 暂停时间(8GB堆) |
---|---|
直接转储 | 3-10秒 |
使用-live 选项 |
可能延长50% |
转储时机:
-XX:+HeapDumpOnOutOfMemoryError
)存储优化:
# 使用gzip压缩转储文件(压缩率通常超过70%)
jmap -dump:file=/dev/stdout 12345 | gzip > heap.hprof.gz
jmap
是内存分析的终极武器核心价值:
典型应用场景:
jstack [option] <pid>
常用Option列表:
参数 | 作用 |
---|---|
-F |
强制抓取线程快照(当JVM无响应时) |
-m |
包含Native方法栈(混合模式) |
-l |
显示锁的附加信息(如持有/等待的锁) |
状态 | 含义 |
---|---|
RUNNABLE | 正在执行或等待CPU时间片(可能消耗CPU) |
BLOCKED | 等待进入同步块(如synchronized 竞争) |
WAITING | 无限期等待(如Object.wait() 无超时) |
TIMED_WAITING | 带超时的等待(如Thread.sleep(5000) ) |
- waiting to lock <0x0000000716a388c0> (a java.lang.Object)
- locked <0x0000000716a388d0> (a java.util.HashMap)
现象:某服务CPU持续100%,但请求量正常。
诊断步骤:
top -H -p <pid> # 显示线程CPU占用
发现线程ID 4567占用98% CPU。
printf "%x\n" 4567 # 输出:11d7
jstack -l 12345 > thread_dump.log
"http-nio-8080-exec-5" #32 daemon prio=5 os_prio=0 tid=0x00007f48740d8000 nid=0x11d7 runnable [0x00007f482d7f7000]
java.lang.Thread.State: RUNNABLE
at com.example.LoopService.infiniteLoop(LoopService.java:17)
根因:LoopService
中存在未设置退出条件的循环。
模拟代码:
public class DeadLockDemo {
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) {
try { Thread.sleep(100); }
catch (InterruptedException e) {}
synchronized (lockB) {} // 等待lockB
}
}).start();
new Thread(() -> {
synchronized (lockB) {
synchronized (lockA) {} // 等待lockA
}
}).start();
}
}
jstack输出:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f86d4004f58 (object 0x000000076ab270c0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f86d4006328 (object 0x000000076ab270d0, a java.lang.Object),
which is held by "Thread-1"
自动检测:jstack会直接标记死锁的线程及锁资源!
#!/bin/bash
PID=$1
INTERVAL=60 # 每60秒抓取一次
while true; do
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
jstack -l $PID > thread_dump_$TIMESTAMP.log
gzip thread_dump_$TIMESTAMP.log
sleep $INTERVAL
done
import subprocess
import re
def check_deadlock(pid):
output = subprocess.check_output(f"jstack -l {pid}", shell=True).decode()
if "Found one Java-level deadlock" in output:
deadlock_info = re.search(r"Found (.*)deadlock", output).group(0)
send_alert(f"Deadlock detected: {deadlock_info}")
check_deadlock(12345)
敏感信息泄露:线程名可能包含业务数据(如用户ID)
Thread.currentThread().setName("OrderProcessing-User_12345"); // 需脱敏
性能影响:频繁执行jstack -F
可能导致服务暂停
快照时间点:
jstack -F
日志关联:
# 结合GC日志时间戳分析
jstack -l 12345 > thread_$(date +%s).log
Native线程:
jstack -m 12345 # 查看JNI调用的Native栈
jstack
是线程问题的“X光检测仪”典型现象:
java.lang.OutOfMemoryError: Java heap space
500
错误,伴随GC overhead limit exceeded
警告关键信息收集清单:
基础环境信息:
uname -a # 系统版本
java -version # JDK版本
free -m # 内存总量
服务状态快照:
jps -l # 获取Java进程PID
jstat -gcutil <pid> 1000 5 # 实时GC监控(1秒间隔,采样5次)
日志提取:
grep -A 10 "OutOfMemoryError" /var/log/app/error.log
执行命令:
jstat -gc <pid> 1000 10
输出解析:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
5120.0 5120.0 0.0 0.0 33280.0 33280.0 87552.0 87552.0 31616.0 29943.8 4352.0 3993.6 15 0.248 5 1.547 1.795
关键指标:
结论:老年代内存无法回收,存在内存泄漏。
安全转储命令(避免服务卡死):
jmap -dump:live,format=b,file=heap_oom.hprof <pid>
紧急情况强制转储(服务已无响应时):
jmap -F -dump:format=b,file=heap_oom.hprof <pid>
转储文件分析准备:
# 压缩转储文件(通常可缩小70%)
gzip heap_oom.hprof
# 传输到分析环境
scp heap_oom.hprof.gz user@analysis-server:/data
抓取线程快照:
jstack -l <pid> > jstack_oom.log
分析重点:
Found one Java-level deadlock
BLOCKED
状态线程数量waiting on condition
且栈中含java.lang.Object.wait
打开hprof文件:
File → Open Heap Dump → 选择heap_oom.hprof
初步分析:
路径分析:
泄漏代码定位:
|- com.example.CacheManager (0x7d8c3f88)
|- java.util.HashMap (0x7d8c3fa0)
|- [Entry objects] 100,000个
模式 | MAT特征 | 修复方案 |
---|---|---|
静态集合累积 | HashMap/HashSet占内存80%+ | 添加LRU淘汰策略 |
未关闭资源 | FileInputStream/Connection未释放 | try-with-resources重构 |
缓存未清理 | Guava Cache未设过期时间 | 配置expireAfterWrite |
问题代码:
public class CacheManager {
private static Map<String, Object> cache = new HashMap<>();
public static void add(String key, Object value) {
cache.put(key, value); // 无上限,无过期
}
}
修复方案:
public class SafeCacheManager {
private static Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterAccess(1, TimeUnit.HOURS)
.softValues()
.build();
public static void add(String key, Object value) {
cache.put(key, value);
}
}
压力测试:
jmeter -n -t oom_test.jmx -l result.jtl
内存监控:
# 实时观察老年代内存
jstat -gcutil <pid> 1000
回归分析:
场景:定时检测老年代内存使用率,超过阈值自动触发堆转储并发送告警。
Shell实现:
#!/bin/bash
PID=$(jps -l | grep MyApp | awk '{print $1}')
THRESHOLD=90 # 老年代使用率阈值
DUMP_DIR="/data/heapdumps"
while true; do
# 获取老年代内存使用率
OLD_USAGE=$(jstat -gc $PID | tail -1 | awk '{print $8}' | cut -d '.' -f1)
if [ $OLD_USAGE -ge $THRESHOLD ]; then
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# 生成堆转储
jmap -dump:live,format=b,file=$DUMP_DIR/heap_$TIMESTAMP.hprof $PID
gzip $DUMP_DIR/heap_$TIMESTAMP.hprof
# 发送告警
curl -X POST -H "Content-Type: application/json" \
-d '{"msg": "OOM预警,已生成堆转储:heap_'$TIMESTAMP'.hprof.gz"}' \
http://alert-server:8080/notify
fi
sleep 300 # 5分钟检测一次
done
Python增强版(支持趋势分析):
import subprocess
import time
import requests
def get_old_gen_usage(pid):
output = subprocess.check_output(f"jstat -gc {pid}", shell=True).decode()
lines = output.strip().split('\n')
last_line = lines[-1].split()
return float(last_line[8]) # 老年代使用率
def main():
pid = subprocess.check_output("jps -l | grep MyApp | awk '{print $1}'", shell=True).decode().strip()
threshold = 90
history = []
while True:
usage = get_old_gen_usage(pid)
history.append(usage)
if len(history) > 6: # 30分钟窗口(6次检测)
history.pop(0)
avg_usage = sum(history) / len(history)
if avg_usage >= threshold:
# 触发转储和告警
timestamp = time.strftime("%Y%m%d_%H%M%S")
subprocess.run(f"jmap -dump:live,format=b,file=/data/heapdumps/heap_{timestamp}.hprof {pid}", shell=True)
requests.post("http://alert-server:8080/notify",
json={"msg": f"持续高内存使用率:{avg_usage:.2f}%,堆转储已生成"})
time.sleep(300)
if __name__ == "__main__":
main()
启用认证:
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.password.file=/etc/jmx_passwd
-Dcom.sun.management.jmxremote.access.file=/etc/jmx_access
配置权限文件:
jmx_access:
monitorRole readonly
controlRole readwrite \
create javax.management.monitor.*,javax.management.timer.* \
unregister
jmx_passwd(权限600):
monitorRole SecurePass123!
controlRole StrongerPass456!
网络隔离:
# 使用iptables限制访问来源
iptables -A INPUT -p tcp --dport 7091 -s 10.10.1.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 7091 -j DROP
方案1:禁用调试权限
# 在JVM启动参数中限制
-XX:+DisableAttachMechanism
方案2:Linux Capabilities控制
# 移除jmap的ptrace权限
setcap cap_sys_ptrace=ep $JAVA_HOME/bin/jmap
# 仅允许特定用户执行
chmod 750 $JAVA_HOME/bin/jmap
错误配置:
CMD ["java", "-Xmx4g", "-jar", "app.jar"] # 硬编码堆大小,可能超出容器限制
正确配置:
# 动态计算堆大小(容器内存的70%)
CMD ["java", "-XX:MaxRAMPercentage=70.0", "-XX:+UseContainerSupport", "-jar", "app.jar"]
验证命令:
docker run -m 2g <image>
# 进入容器查看实际堆大小
jcmd 1 VM.flags | grep MaxHeapSize
资源限制配置:
resources:
limits:
memory: "4Gi"
cpu: "2"
requests:
memory: "3Gi"
cpu: "1"
就绪探针+内存检查:
readinessProbe:
exec:
command:
- /bin/sh
- -c
- |
if [ $(jstat -gc 1 | awk 'FNR == 2 {print $8}') -gt 90 ]; then
exit 1 # 内存超限时标记为未就绪
fi
initialDelaySeconds: 30
periodSeconds: 10
调试镜像构建:
FROM eclipse-temurin:17-jdk
RUN apt-get update && apt-get install -y \
procps \ # top/ps命令
lsof \ # 文件句柄检查
net-tools \ # 网络诊断
tcpdump \ # 抓包分析
&& rm -rf /var/lib/apt/lists/*
# 集成Arthas
ADD https://arthas.aliyun.com/arthas-boot.jar /opt/
诊断命令示例:
kubectl exec -it <pod> -- java -jar /opt/arthas-boot.jar # 启动Arthas
kubectl exec -it <pod> -- tcpdump -i eth0 port 8080 -w /tmp/dump.pcap # 抓包
问题原因:
jmap
生成堆转储时,会触发Full GC并暂停所有应用线程(Stop-The-World),若堆内存过大或对象过多,可能导致服务短暂无响应。
解决方案:
低峰期执行转储:
# 通过定时任务在业务低峰期执行
0 3 * * * jmap -dump:live,format=b,file=/data/heap_$(date +\%Y\%m\%d).hprof <pid>
使用安全模式:
# 添加`:live`参数仅转储存活对象,减少扫描范围
jmap -dump:live,format=b,file=heap.hprof <pid>
替代工具:
GDB(仅限Linux):
gcore <pid> # 生成核心转储
jhsdb jmap --binaryheap --pid <pid> # 从核心转储提取堆信息
Eclipse MAT渐进式分析:
./ParseHeapDump.sh heap.hprof org.eclipse.mat.api:suspects
JVM参数优化:
# 启用并行Full GC(仅适用于G1)
-XX:+ParallelRefProcEnabled
诊断步骤:
指标对比:
内存类型 | 监控命令 | 泄漏特征 |
---|---|---|
堆内存 | jstat -gc / jmap |
老年代使用率持续上升,Full GC无效 |
Native内存 | jcmd |
提交内存(Committed)远超堆内存总量 |
Native内存分析工具:
Valgrind(C/C++层):
valgrind --leak-check=full --show-reachable=yes java -jar app.jar
jemalloc:
LD_PRELOAD=/usr/lib/libjemalloc.so.2 JAVA_OPTS="-XX:NativeMemoryTracking=detail"
JNI代码检查:
排查以下JNI函数调用:
NewGlobalRef() 未配对的 DeleteGlobalRef()
GetPrimitiveArrayCritical() 未释放的 ReleasePrimitiveArrayCritical()
操作系统工具:
pmap -x <pid> # 查看进程内存映射
cat /proc/<pid>/smaps # 分析匿名内存块(anon)
全命令行解决方案:
jhat基础分析:
jhat -port 7000 heap.hprof # 启动HTTP服务
curl http://localhost:7000 # 查看类直方图
Eclipse MAT命令行:
# 生成泄漏 suspects 报告
./ParseHeapDump.sh heap.hprof org.eclipse.mat.api:suspects
# 生成对象直方图
./ParseHeapDump.sh heap.hprof org.eclipse.mat.api:overview
# OQL查询(示例:查找大数组)
./ParseHeapDump.sh heap.hprof "SELECT * FROM \"[B\" WHERE @retainedHeapSize > 1048576"
VisualVM远程分析:
# 在本地VisualVM中加载远程hprof文件
jvisualvm --openfile heap.hprof
自定义脚本分析(Python示例):
import hprottools
dump = hprottools.parse("heap.hprof")
# 统计前10大对象
for cls in dump.classes.sort_by("instances_size")[-10:]:
print(f"{cls.name}: {cls.instances_size // 1024} KB")
自动化报告生成:
# 使用MAT生成HTML报告并压缩
./ParseHeapDump.sh heap.hprof org.eclipse.mat.api:overview -z=report.zip
-dump:live
+定时任务,极端场景用GDB提取核心转储jstat
,Native内存用jcmd
+Valgrindjhat
+Python脚本实现全链路诊断jcmd <pid> help # 查看所有支持的命令
常用命令:
VM.native_memory
:Native内存分析GC.heap_info
:堆内存统计Thread.print
:线程快照(类似jstack)VM.flags
:查看/修改JVM参数动态修改日志级别(无需重启):
jcmd <pid> VM.log output=file=gc.log,level=debug,tags=gc
Native内存泄漏检测:
jcmd <pid> VM.native_memory summary.diff # 间隔采样对比
输出示例:
Total: reserved=5GB +245MB, committed=3GB +128MB
- Java Heap: reserved=4GB, committed=2GB
- Thread: reserved=128MB +64MB, committed=64MB +32MB # 线程泄漏嫌疑
jinfo -flags <pid> # 所有JVM参数
jinfo -sysprops <pid> # 系统属性
jinfo -flag MaxHeapSize <pid> # 查询特定参数值
启用GC日志:
jinfo -flag +PrintGCDetails <pid>
jinfo -flag -Xloggc:/path/to/gc.log <pid>
风险操作(需-XX:+WriteableFlags):
jinfo -flag MaxHeapFreeRatio=70 <pid> # 调整堆空闲比例
特性 | JConsole | VisualVM |
---|---|---|
内存分析 | 基础堆/非堆监控 | 支持堆转储分析 + OQL查询 |
线程分析 | 死锁检测 | 线程状态时间线 + CPU热点方法 |
扩展性 | 内置MBean浏览器 | 插件生态(BTrace、Sampler等) |
适用场景 | 快速概览 | 深度诊断 |
VisualVM远程连接:
java -Dcom.sun.management.jmxremote.port=7091 \
-Dcom.sun.management.jmxremote.ssl=false \
-Dcom.sun.management.jmxremote.authenticate=false \
-jar app.jar
持续记录低开销诊断数据:
# 开启记录(默认60秒)
jcmd <pid> JFR.start name=myrecording settings=profile
# 导出记录
jcmd <pid> JFR.dump name=myrecording filename=recording.jfr
分析指标:
# 在Kubernetes中采集JFR数据
kubectl exec <pod> -- jcmd 1 JFR.start duration=60s filename=/tmp/recording.jfr
kubectl cp <pod>:/tmp/recording.jfr ./
# 下载最新版
wget https://github.com/async-profiler/async-profiler/releases/download/v2.0/async-profiler-2.0-linux-x64.tar.gz
# CPU采样(每秒99次)
./profiler.sh -e cpu -d 30 -f cpu_flamegraph.html <pid>
火焰图解读:
docker run --rm -it --cap-add=perf_event \
-v /path/to/profiler:/profiler \
-v /tmp:/target \
your-java-app
# 在容器内执行
/profiler/profiler.sh -d 30 -o flamegraph -f /target/flame.html 1
# 1. 反编译类
jad com.example.MyService
# 2. 修改代码后编译
mc -c <classloader哈希> /tmp/MyService.java -d /tmp
# 3. 热替换
retransform /tmp/MyService.class
# 统计方法调用耗时
watch com.example.MyService * '{params, returnObj, throwExp}' \
-x 3 -n 5 --success-exit
输出示例:
ts=2023-10-01 10:00:00; [cost=12ms] result=@ArrayList[
@String[param1],
@Integer[100],
null
]
是否需要实时监控?
├─ 是 → Arthas/VisualVM
├─ 否 → 转储分析(MAT/JMC)
是否需要低开销?
├─ 是 → JFR/Async Profiler
├─ 否 → Heap Dump + OQL
环境限制?
├─ 无GUI → jcmd + 脚本
├─ 容器 → jattach + Arthas
下一步行动建议:
延伸学习:
主题 | 关键工具/技术 | 核心收获 |
---|---|---|
OOM问题排查 | jstat/jmap/jstack + MAT | 掌握内存泄漏定位三板斧 |
自动化监控 | Shell/Python脚本 + 定时任务 | 实现异常自愈,降低人工干预 |
安全防护 | JMX认证 + 权限控制 | 防止敏感数据泄露,满足合规要求 |
容器化适配 | cgroup感知 + K8s探针 | 避免内存超限导致的OOMKilled |
高级诊断 | Arthas + Valgrind | 无侵入式诊断,覆盖Java/Native全栈 |
数据驱动:
jstat
/gc.log
/APM指标决策,拒绝“玄学调优”层次化思维:
代码优化 → JVM参数 → OS配置 → 硬件升级
平衡之道:
个人实践:
团队协作:
社区贡献:
“纸上得来终觉浅,绝知此事要躬行。”
愿你在JVM的星辰大海中,找到属于自己的性能优化之道!
全系列终
但探索永无止境
期待与你在更高处重逢!