
年初在客户现场排查一个Java服务频繁Full GC的问题我第一句话问的就是“堆配了多大、用的什么收集器”结果对方把启动脚本翻出来一行行核对半天才确认参数。后来我养成一个习惯不管什么环境拿到Java进程先别猜直接上命令看JVM参数。Linux下查看应用JVM参数的方式其实不少但每种方式看到的“参数”含义不太一样适合的场景也不同。这篇文章就把我平时排查线上问题时实际用到的几种方法完整梳理一遍包括命令怎么敲、输出怎么读、有哪些坑希望对做Java运维和后端开发的朋友有用。1. 先搞清楚你到底想看哪一层的JVM参数1.1 三个容易混淆的“参数来源”很多新手刚接触JVM参数排查时会很困惑同样一个进程用jinfo看到的堆大小和启动脚本里写的-Xmx对不上用jcmd看到的结果又是另一副样子。这不是命令坏了而是“JVM参数”本就有多层含义。第一层是启动命令里的显式参数也就是你在启动脚本、Dockerfile、K8s Deployment里写的那一串比如-Xmx2g -XX:UseG1GC。这一层最好理解但也最容易误导人——因为JVM不会原样把这些参数当成最终生效值它还会叠加默认值、环境变量、动态调整结果。第二层是JVM实际生效的参数包含默认值。比如你只写了-Xmx2g那-XX:MaxMetaspaceSize可能是默认的无限大或某个平台默认值-XX:UseG1GC在JDK 11里本来就是默认你写和不写效果一样。这一层才是JVM真正“跑起来的状态”。第三层是运行时允许动态修改的Flag。JVM里有一部分参数被标记为manageable意思是进程跑着的时候可以动态改不用重启。比如-XX:HeapDumpOnOutOfMemoryError这类开关。如果不懂这个机制贸然用jinfo去改一些非manageable的参数就会碰到“改了没反应”或者直接报错的情况。1.2 参数优先级的隐藏规则除了层面不同参数来源本身还有优先级顺序。实际项目里经常出现这种情况启动脚本写了-Xmx2g但应用起来之后用jcmd一看堆上限变成了4g。多半是环境变量JAVA_TOOL_OPTIONS或_JAVA_OPTIONS里也塞了-Xmx。优先级从高到低大致是这样的_JAVA_OPTIONS环境变量最高其次是命令行参数再其次是JAVA_TOOL_OPTIONS。也就是说如果你在_JAVA_OPTIONS里配了-Xmx4g那么命令行里写-Xmx2g是盖不住它的。还有一个JDK_JAVA_OPTIONS它是JDK 9之后引入的优先级介于JAVA_TOOL_OPTIONS和命令行之间。这块规则在不同JDK版本略有差异排查时不要想当然最好把环境变量也打出来看一眼。2. jcmdJDK自带的“全能选手”2.1 一行命令快速上手如果你和我一样懒只想记一个命令那就记jcmd。它是JDK自带的诊断工具从JDK 7开始就有功能非常全。排查JVM参数时我一般先用这个命令看进程是否存活jcmd -l输出类似12345 org.example.Main前面是进程ID后面是主类名。拿到PID之后最常用的三个子命令是jcmd 12345 VM.flags jcmd 12345 VM.command_line jcmd 12345 GC.heap_infoVM.flags输出的是当前JVM生效的关键参数包括显式配置和非默认值。VM.command_line输出的是启动时完整的命令行能直接看到-Xmx、-Xms这些。GC.heap_info则是堆当前状态摘要经常在排查内存问题时和参数一起看。2.2 几个经常被忽略的jcmd子命令VM.flags默认只显示一部分参数如果你想看全部参数包括一堆没碰过的默认值可以加-alljcmd 12345 VM.flags -all这个输出会非常长里面有几百行Flag格式像这样bool UseG1GC : true {product} {default} uintx MaxHeapSize : 4294967296 {product} {ergonomic}注意:和的区别。:代表这个参数的值不是JVM默认值可能是命令行显式指定的也可能是JVM根据机器配置自动算出来的也就是{ergonomic}。就是JVM本来的默认值。排查“为什么我写的参数没生效”时看这个最直观。另外jcmd 12345 help能列出当前版本所有可用的子命令不用死记。直接执行jcmd 12345 help jcmd 12345 help VM.flags能拿到每个子命令的简要说明这在跨JDK版本工作时特别有用因为不同版本的JDK支持的jcmd子命令多少有点差异。我见过很多同事只知道jcmd pid help可以看命令列表但不知道子命令还能单独加help查看用法导致在低版本JDK上调试时经常碰壁。2.3 为什么我更喜欢用jcmd看参数jcmd比jinfo好用的点在于它的输出更结构化VM.flags直接标注了参数来源默认、手动、自适应一眼就能看出哪些参数是“配置出来的”、哪些是“JVM自己算出来的”。比如你启动脚本里只写了-Xmx2g但机器内存是64gVM.flags -all里MaxHeapSize可能显示的是:且来源是{ergonomic}——意思就是JVM根据机器配置自动算了个值而不是你配置的值。这种情况用jinfo有时会得到不一样的展示容易误导人。3. jinfo能看、有时还能改3.1 查看启动参数和生效值jinfo是另一个JDK原生工具核心作用有两个查看JVM参数、动态修改可管理的Flag。先看看查参数怎么用jinfo -flags 12345它会输出类似这样的内容Attaching to process ID 12345, please wait... Debugger attached successfully. Client compiler ... -XX:InitialHeapSize268435456 -XX:MaxHeapSize4294967296 ...输出里混着系统属性和JVM参数量也比较大。和jcmd VM.flags比jinfo更偏向“启动时指定的参数”但它不会像jcmd那样给你标注参数来源。想单独查某个参数可以直接用-flagjinfo -flag MaxHeapSize 12345输出-XX:MaxHeapSize4294967296这样比翻全量输出省事得多。查系统属性用jinfo -sysprops 123453.2 动态修改参数的边界jinfo真正厉害的地方在于支持运行时动态修改部分参数。但要注意不是所有参数都能改只有标记为manageable的Flag才行。比如你想临时打开OOM时自动导出堆转储jinfo -flag HeapDumpOnOutOfMemoryError 12345这个操作不重启进程就生效了对线上应急很有用。但有些参数虽然标了manageable改起来依然有副作用。比如-XX:PrintGCDetails这类日志开关还好但如果你去动-XX:UseConcMarkSweepGC这种收集器参数很多JDK版本会直接拒绝或者改了之后整个GC行为变得无法预测。想确认哪些参数可以动态修改可以用java -XX:PrintFlagsFinal -version 2/dev/null | grep manageable在JDK 8下会输出所有带{manageable}标记的参数列表。不过这个命令在JDK 9之后输出格式变化较大有的版本还需要额外处理不然会打印出巨量内容把终端刷爆。实际生产环境我一般只在应急时用jinfo改OOM转储开关和GC日志开关其他参数一律不敢动。3.3 jcmd与jinfo何时选哪个很多文章喜欢把jcmd和jinfo放在一起比较但实际工作中它们不是替代关系而是互补关系。我建议按这个原则来选只是“看一看”优先用jcmd VM.flags信息结构化程度高一眼能看到差异。想看单个参数具体值用jinfo -flag 参数名更轻量。要动态改参数只能选jinfo。要看堆、GC等运行状态jcmd里还有GC.heap_info、GC.class_histogram等一整套命令jinfo则没有这些。4. 不只是Java工具从操作系统视角确认参数4.1 ps命令依然是最快的兜底方案jcmd和jinfo虽然强大但也有前提目标Java进程能被当前用户attach。如果你是用root去看普通用户启动的Java进程或者反过来普通用户想看root启动的进程经常会碰到权限问题。这时候操作系统层面的命令反而最可靠。最常用的就是psps -ef | grep java或者只看命令行参数ps -ef | grep java | grep -v grepps -ef能看到启动时完整命令行包括-Xmx、-Xms、-XX这些参数。缺点也明显它只能看到“显式写在命令行里的参数”看不到JVM自动算出来的默认值。但作为“这个进程到底怎么启动的”这层信息ps是最接近事实的。4.2 /proc文件系统更深层的启动参数视图Linux的/proc文件系统把每个进程的信息都暴露出来了Java进程也不例外。想看进程启动时的完整命令行可以这样cat /proc/12345/cmdline不过输出里所有参数是用空字符\0分隔的直接cat会糊成一坨。需要转换一下cat /proc/12345/cmdline | tr \0 \n这样就能一行一个参数地看比ps -ef在某些场景下更清晰。/proc这个方式还有个好处即使进程不对当前用户开放完整attach权限只要你能读到/proc/pid/cmdline一般是同一用户或root就能拿到启动命令行。同样地查看进程的环境变量可以用cat /proc/12345/environ | tr \0 \n | grep -i java这招在排查JAVA_TOOL_OPTIONS、_JAVA_OPTIONS这类隐藏参数时特别有用因为这些环境变量不会出现在ps -ef的输出里但会影响最终生效的JVM参数。我曾经帮人排查过一个疑难问题无论启动脚本怎么写-Xmx进程实际堆大小始终是4g最后就是用/proc/pid/environ揪出了藏在容器镜像里的_JAVA_OPTIONS。4.3 容器环境下的特殊注意点现在Java应用大量跑在Docker和Kubernetes里查看JVM参数的方式要特别注意两点。第一容器里Java进程的PID在宿主机上也能看到但jcmd、jinfo这类工具依赖attach机制跨容器访问经常被拒绝。最稳妥的办法是进入容器执行kubectl exec -it pod-name -- jcmd 1 VM.flags或者用docker exec。容器内Java进程的PID通常是1但这个不绝对建议先用jps或ps确认。第二容器内存限制和JVM默认堆大小的关系很微妙。JDK 8u191之前JVM默认最大堆是宿主物理内存的1/4在容器里很容易出现“JVM觉得自己有64g内存可用实际Cgroup只给它4g”的情况结果一压测就OOMKilled。JDK 8u191之后默认开启UseContainerSupportJVM能感知容器限制。排查这类问题时除了看参数还要确认JDK版本和是否显式设置了-XX:UseContainerSupport或-XX:-UseContainerSupport。用ps和/proc只能看到半个真相容器场景必须结合Cgroup文件和JDK版本一起判断。5. 到底用哪个一张速查表解决选择困难为了节省大家现场翻文档的时间我把常见的“看JVM参数”诉求和推荐命令整理成了一张表都是我实际验证过可用的需求场景推荐命令输出特点快速确认Java进程是否存在jcmd -l或ps -ef | grep java前者更专业后者更通用查看启动时显式参数jcmd pid VM.command_line与启动脚本对应关系直接查看当前生效的非默认参数jcmd pid VM.flags标注了参数来源查看所有JVM参数含默认值jcmd pid VM.flags -all输出量大查看单个参数具体值jinfo -flag 参数名 pid最轻量动态修改可管理参数jinfo -flag [-]参数名 pid仅限manageable参数只看操作系统层的启动命令ps -ef或/proc/pid/cmdline不含默认值排查环境变量注入的参数cat /proc/pid/environ很容易被忽略导出全量Flag做对比分析java -XX:PrintFlagsFinal -version输出可作为基线查看堆当前使用状态jcmd pid GC.heap_info配合参数做内存分析这里单独说一下java -XX:PrintFlagsFinal -version这个命令。它不是在跑着的进程上执行而是用本机JDK模拟一次启动把所有Flag的最终值打印出来。可以用来对照“我当前这个JDK版本里某个参数的默认值是多少”方便判断跑着的进程里那些:到底是手工配的还是JVM自适应算的。在JDK 8里输出末尾有{default}、{product}、{ergonomic}这样的标注JDK 9之后格式略有变化但:和的区分逻辑基本一致。6. 实际排障中那些容易踩的坑6.1 用jps找不到进程很多教程上来就教你jps -lvm但实际生产环境里jps经常找不全Java进程。原因是jps默认只扫描/tmp/hsperfdata_username目录下的PerfData文件而有些Java进程由于启动方式特殊或权限隔离不会在这个目录里留下对应的文件。遇到这种情况先别怀疑JVM直接用ps -ef | grep java拿PID再用jcmd和jinfo去查。我自己的经验是把jps当成“锦上添花”的工具把ps当成“保底”的工具排查线上问题时直接用ps拿PID很少翻车。6.2 attach权限导致的各种拒绝访问用jcmd或jinfo连接Java进程时如果遇到“Unable to open socket file”或者“java.io.IOException: Permission denied”基本都是权限问题。Java的attach机制要求执行工具的用户和运行Java进程的用户一致或者有足够的权限。常见解法有三种切换到运行Java进程的相同用户再执行。使用sudo -u java_user指定用户执行。如果是容器docker exec进容器内部执行。另外如果Java进程启动时带了-XX:DisableAttachMechanism参数那任何外部attach都会被禁止工具会直接报错。这种一般出现在安全要求非常高的环境里需要你去看启动脚本才能发现。6.3 动态修改参数后没有生效jinfo -flag HeapDumpOnOutOfMemoryError pid执行成功了也返回了“Key to update”但进程真的OOM时却没有生成dump文件。这种问题我遇到过不止一次原因基本逃不出两种第一种改的Flag在当前的JVM实现里其实不是manageable你看着像是成功了但JVM内部根本不会响应。建议先用java -XX:PrintFlagsFinal确认Flag标记。第二种jinfo版本和Java进程的JDK版本不一致。比如用JDK 11的jinfo去连JDK 8的进程或者反过来很容易出现“显示成功但实际没变”的情况。这点在多个JDK版本混用的服务器上尤其常见务必用JAVA_HOME对应的jinfo。6.4 看到“默认值”以为是“配置错误”经常有人在启动脚本里没写-Xms然后用jcmd VM.flags看到InitialHeapSize有值就以为哪里偷偷配了参数。其实这是JVM根据-Xmx自动推导的比如默认-Xms是物理内存的1/64或者和-Xmx保持某个比例。类似的还有MaxNewSize、NewRatio都属于“JVM自动算出来的值”不是错误。判断标准就看VM.flags -all里是不是{ergonomic}只要来源是ergonomic就说明是JVM自己算的。这一块建议大家在测试环境多跑几次VM.flags -all把自己服务常用JDK版本下的默认参数摸底一遍真正出问题时就不会被默认值误导。再说个小技巧我平时排查问题时经常在启动命令里临时加-XX:PrintCommandLineFlags这样应用一启动日志第一行就会打印当前生效的关键命令行参数。配合jcmd和jinfo交叉验证基本能把一个Java进程的JVM参数配置情况摸得明明白白。尤其是团队里有人喜欢把参数散落在启动脚本、环境变量、容器配置多个地方时这个习惯能帮你省下大量扯皮时间。