ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JVM线上排查利器 jinfo:运行参数查看与动态修改实战

JVM线上排查利器 jinfo:运行参数查看与动态修改实战

排查线上JVM问题的时候,我最常用的工具排序里,jinfo一定排在前几名。很多人知道它,但用得很浅,无非就是敲一句jinfo -flags pid看看启动参数,或者干脆依赖启动脚本里的配置。可实际情况是,进程明明在跑,启动参数和你以为的完全是两码事;你调的某个参数,JVM可能压根没认;你想改的一个Flag,可能改了没有任何反应。这些问题,jinfo都能帮你查个底朝天。

jinfo是JDK自带的一个小工具,全称Java Configuration Info,一句话概括就是:在进程运行期间,查看和修改JVM的运行参数。它解决的核心痛点是——你手上只有一台正在运行的Java进程,既不想停服,又想知道它此刻到底是用什么参数跑的,甚至想在运行中微调个别参数。对于开发排查问题、运维诊断线上故障、以及平时想确认自己启动脚本有没有写对的人来说,jinfo都是最直接的那把钥匙。

这篇文章我打算把jinfo从命令格式到输出解读,从原理到实战,从动态改参数的边界到各种报错排查,完完整整捋一遍。全程都是我用过的场景和踩过的坑,不绕弯子,直接写干货。

1. 先把jinfo放在JVM工具箱里的位置搞清楚

1.1 JVM参数到底有几个来源

说到JVM运行参数,很多人第一时间想到的就是启动脚本里那一串-Xmx、-XX:MaxMetaspaceSize。但这只是来源之一。JVM运行时的参数值,其实是三股力量的合流:

  • 启动参数:启动命令里显式传入的-X、-XX、-D选项。
  • 默认值:你没有显式设置、但JVM根据版本、运行模式、机器内存自动推算出的值。比如-Xmx不设置时,JDK 8之后默认是物理内存的1/4,这个值不是Launch脚本里看不出来的。
  • 动态修改值:在进程运行期间,通过jinfo或jcmd临时改掉的参数值。

这三股力量叠加在一起,才是JVM真正的“运行时期参数面”。启动脚本只能证明你“想让它跑成什么样”,而jinfo告诉你的是“它现在到底跑成什么样”。这两者大多数时候一致,但一旦出现不一致,比如环境变量影响、启动脚本被二次封装、参数拼写错误被忽略等,没有jinfo你根本察觉不到。

我遇到过最典型的场景:某个同事在启动脚本里写了个-XX:MaxPermSize=256m,进程在JDK 8上跑了很久也没人觉得奇怪,因为JVM不会报错,只会默默忽略这个参数。直到某天用jinfo查活进程,才发现这个参数根本没生效。这种静默失效,只有jinfo能抓现行。

1.2 和jmap、jstat、jcmd的分工差异

很多文章讲JVM诊断工具时会一口气列出一堆,jps、jstat、jmap、jstack、jcmd、jinfo,看得人头晕。其实它们的分工很简单:

工具核心职责我什么时候用它
jps列出本机Java进程第一步,拿到pid
jstat看GC、类加载、JIT等运行时统计数据观察趋势,定位GC异常
jmap看堆信息、生成堆转储查内存对象分布、导出dump
jstack打印线程栈看线程死锁、线程阻塞
jcmd综合诊断,功能覆盖jinfo/jstat/jmap新版JDK里我喜欢当瑞士军刀用
jinfo查看/修改运行期参数只需要看参数配置或临时改参数时

jinfo的独门绝技就是针对参数这一个维度,做到快、准、狠。它不是去采样数据,也不是导出快照,而是直接和JVM内部的参数系统对话,读的是JVM底层的参数值,不是你启动脚本里的字符串。这也是为什么我一直建议,别拿启动脚本当唯一依据,脚本可能说谎,jinfo不会。

1.3 底层机制:它怎么读到你参数的

jinfo不是凭空看透进程的,它走的是JDK标准的Attach机制。也就是说,jinfo作为客户端进程,先通过操作系统进程信号和通信机制,向目标JVM发送一个attach请求,目标JVM在允许的情况下会打开一个内部通信通道,jinfo再通过这个通道向JVM内的一个专用代理线程发起参数查询。

这里有三个关键点:

第一,目标进程必须是Java进程,而且跑的是标准JDK,不能是精简过的JRE,否则没有attach能力。

第二,attach机制可能被关闭。比如Java进程启动时加了-XX:+DisableAttachMechanism这个参数,那外部工具包括jinfo在内全部无法连接,报错类型通常是"Unable to open socket file"。这是很多安全加固策略喜欢干的,记得排查时留个心眼。

第三,权限问题。jinfo想attach成功,启动jinfo的OS用户必须和目标Java进程的用户一致,或者有足够的权限。跨用户执行一般是不行的,这是操作系统层面的事情,不是jinfo能绕过的。

理解了这几点,后面遇到报错就容易判断了。

2. jinfo命令全参数解析与输出逐行解读

2.1 命令格式全景图

jinfo的命令格式很固定,没有太多花哨的选项。核心用法就下面几种,我按使用频率排个序:

jinfo <pid> # 显示全部系统属性和VM参数,默认输出 jinfo -flags <pid> # 只显示VM Flags参数 jinfo -sysprops <pid> # 只显示系统属性System Properties jinfo -flag <name> <pid> # 只显示指定参数当前值 jinfo -flag [+|-]<name> <pid> # 动态开启或关闭某个布尔参数 jinfo -flag <name>=<value> <pid> # 动态修改某个参数的值

特别注意,老版本JDK里,不带任何选项的jinfo <pid>会直接输出所有信息。而在JDK 9之后,不带选项的默认行为被移除了,会提示你显式指定-flags或-sysprops。这也是很多从JDK 8环境转过来的人,一执行jinfo 1234莫名其妙的直接报错的原因。

还有个容易被忽略的事情:jinfo历史上有过“实验性”的定位,有些发行版会把它放在JDK的私有目录里。如果你在命令行里直接敲jinfo提示找不到命令,检查一下JAVA_HOME/bin是不是在你的PATH里,或者直接使用$JAVA_HOME/bin/jinfo。

2.2 -sysprops:Java运行期的系统属性快照

先看-sysprops,它的输出是Java的System.getProperties()的全部内容,包含了一大堆java.home、java.version、os.name、user.dir、file.encoding之类的键值对。

有人觉得这没啥用,实际上在排查乱码、文件路径、时区问题时,这是最高效的取证工具。比如线上出现中文乱码,你第一反应是查编码,而jinfo -sysprops直接告诉你JVM当前实际使用的是什么默认字符集,是UTF-8还是GBK。如果系统环境变量LANG被改过,但JVM启动时读取的是旧值,你就能通过这里看出端倪。

再比如有些框架会读取user.timezone来决定日志时间格式,而user.timezone常常不是你在代码里设的,而是JVM启动时根据OS时区自动写的。日志时间对不上,拿jinfo -sysprops一查,立刻知道JVM内心以为自己在哪个时区。

还有java.class.path,有时候打包工具生成一个巨大的Class-Path,你想确认进程跑的到底是哪份jar,java.class.path里全都有。我处理过几次“改了代码但没生效”的投诉,最后都是通过这个属性发现,跑的根本不是新构建出来的jar包。

2.3 -flags:真正的VM参数生效值

-flags输出的是JVM内部的VM Flags,这部分才是真正决定JVM行为的东西。输出的每一行里,带有=号的是当前生效值,前缀有个-号的是非默认值(即显式设置过的),没有那个前缀的就是JVM默认值。

举例来说,输出可能是这样:

Attaching to process ID 21983, please wait... Command line: -Xms512m -Xmx2048m -XX:MetaspaceSize=256m -XX:+UseG1GC VM Flags: -XX:CICompilerCount=4 -XX:G1HeapRegionSize=1048576 -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=2147483648 -XX:MetaspaceSize=268435456 ...

第一行Command line就是启动命令行的原貌,这个很关键,它告诉我们进程当初真正被启动时的完整参数。第二段VM Flags是按字母序排列的所有参数当前值。注意看InitialHeapSize,单位是字节,536870912除以1024再除以1024就是512MB,正好对应的-Xms512m;MaxHeapSize是2147483648字节,即2048MB,对应-Xmx2048m。

我在实际看输出时,习惯先扫Command line,看显式参数有没有拼写错误、有没有没生效的参数;然后再扫VM Flags,看哪些参数被标记为非默认值。因为默认值太多了,你不可能一个个对,但是非默认值意味着这里一定有显式指定或者系统环境自动推断的特殊配置,重点盯这些就够了。

2.4 -flag name:单项参数的精确查询

单独查某项参数时用-flag加参数名,比如:

jinfo -flag MaxHeapSize 21983

输出是这样的:

-XX:MaxHeapSize=2147483648

注意参数名不区分大小写,MaxHeapSize、maxheapsize都行,JVM内部会做规范化处理。所有参数名前统一带-XX:前缀,这是jinfo输出格式的一部分。

对你关心的布尔参数,输出会是这样:

jinfo -flag UseG1GC 21983 -XX:+UseG1GC

+号表示布尔值为true,-号表示false。这个格式需要留意,因为脚本处理时,你可能要把+和-当作布尔标志去解析。

这个单项查询我觉得是排查问题时最实用的。遇到某个参数可疑,一条命令直接验证,不用在几百行的输出里来回翻。比如别人跟你说他设置了-XX:+PrintGCDetails,你一条jinfo -flag PrintGCDetails pid就知道真假。

2.5 留个底:jinfo只是快捷入口,如果要全量参数要看PrintFlagsFinal

这里有个绝大多数人不知道的边界,jinfo的-flags输出并不包含JVM所有内部参数的完整清单。JVM内部的参数有三四百个,其中有一部分是隐藏的、适用于开发者调试的参数,-flags默认不展示。

如果你想看JVM支持的所有参数的最终值,正确姿势是启动时加-XX:+PrintFlagsFinal,或者在运行期用jcmd:

jcmd <pid> VM.flags jcmd <pid> VM.flags -all

和jinfo的输出相比,jcmd给出的信息更结构化,VM.flags显示非默认值,VM.flags -all列出全部参数。不过jinfo基本够用了,只有当你需要查某个冷门参数是否存在时,才需要回头用jcmd。我的习惯是:日常快查用jinfo,完整扫描用jcmd,两个互补着用。

3. 实战:用jinfo摸清陌生实例和动态修改参数

3.1 三步套路:拿到一台陌生JVM实例后我做了什么

拿到一台没有文档、没有启动脚本备份的Java进程,想要搞清楚它的参数配置,我的固定套路是这样的:

第一步,jps定位pid:

jps -l

输出里能看到进程号和主类全名。挑出你要分析的那个pid。

第二步,jinfo -flags看启动参数和VM Flag:

jinfo -flags <pid>

重点看Command line那一行的完整启动命令,以及VM Flags里带-前缀的显式参数。如果启动命令里没有-Xmx,但VM Flags里MaxHeapSize又有明确的非默认值,说明有外面的包装脚本通过环境变量设置过,或者读的是JAVA_TOOL_OPTIONS。这东西的值甚至可能不在启动脚本里,而在某个全局环境变量配置里,jinfo一查就能暴露出来。

第三步,jinfo -sysprops看系统属性:

jinfo -sysprops <pid>

这一步的主要目的是确认工作目录、JVM版本、类路径、编码等运行环境细节。如果这个进程是某个框架启动的,java.class.path还能帮我推断它实际加载的是哪份构建产物。

这套三步法执行完,你对这个进程的参数画像基本上是完整清晰的。整个过程只用两条命令,几十秒就能搞定,完全不用动进程。

3.2 动态修改参数:哪些能改,哪些只能看

jinfo最让人兴奋的能力是运行期动态修改参数。先泼一盆冷水:不是所有参数都能改。JVM在实现时明确区分了“可管理参数”和“不可管理参数”。可管理参数在运行期修改后能立即生效,不可管理参数修改了也没用,甚至直接报错。

按照我的经验,常见的、确定可改的参数有:

参数典型用途修改示例
HeapDumpOnOutOfMemoryErrorOOM时是否生成堆dumpjinfo -flag +HeapDumpOnOutOfMemoryError 21983
HeapDumpPath堆dump文件路径jinfo -flag HeapDumpPath=/tmp/heap.hprof 21983
MaxHeapFreeRatioGC后被占用的最大空闲堆比例jinfo -flag MaxHeapFreeRatio=60 21983
MinHeapFreeRatioGC后剩余空闲堆的最小比例jinfo -flag MinHeapFreeRatio=20 21983
MaxGCPauseMillis目标最大GC暂停时间jinfo -flag MaxGCPauseMillis=200 21983

而像Xmx、Xms、MaxMetaspaceSize、SurvivorRatio这类参数,虽然你能用jinfo语法去写,但JVM会直接忽略或者报错,因为堆结构已经定型了,运行期根本没法改。我做了一个小实验确认过,对MaxMetaspaceSize执行修改,命令本身不报错,但你再查一次还是原值。

这个设计逻辑其实很合理:堆内存、元空间这些在JVM启动时就要做整体规划的,你要改必须重启;而GC的某些策略参数、dump开关这类偏策略性的配置,理论上可以在运行期调整,方便在不中断业务的情况下完成调优。

3.3 运行期开HeapDump这个经典场景,我帮你走一遍完整流程

假设有个服务已经跑起来了,但你没有在启动参数里加-XX:+HeapDumpOnOutOfMemoryError。现在你担心某个高峰期它可能OOM,又不想为此专门重启一次服务。这个时候用jinfo动态开启是最优雅的解法。

实际操作如下:

jps -l # 输出:3412 com.example.SomeService jinfo -flag +HeapDumpOnOutOfMemoryError 3412

执行之后没有输出是正常的,jinfo的大部分修改操作是静默成功。此时验证一下:

jinfo -flag HeapDumpOnOutOfMemoryError 3412 # 输出:-XX:+HeapDumpOnOutOfMemoryError

看到+号,说明已经成功开启了。之后如果再发生OOM,JVM就会自动在默认路径下生成堆转储文件。如果你想指定dump文件的路径,可以再执行:

jinfo -flag HeapDumpPath=/data/logs/heapdump.hprof 3412

注意一个细节:HeapDumpPath在有的JDK版本里是可改的,有的版本改了不生效(受限于文件系统路径和目录权限)。我见过最稳的做法是,在启动脚本里就配好路径,然后动态开启开关,这样既不用重启,路径也受控。

还有一个容易踩的坑:修改MaxHeapFreeRatio这类参数时要小心极端值。比如你把MaxHeapFreeRatio调到90以上,GC会倾向于让堆里保留大量空闲内存,可能在一次GC之后频繁触发下一次GC,引起CPU抖动。调这些参数最好配合jstat看GC曲线,别拍脑袋乱调。

3.4 动态修改的持久性问题:重启即消失

这个必须说清楚:jinfo的修改只在当前运行期有效,不会写进任何配置文件,进程一重启就恢复成原来的值。

我遇到过不止一次,同事说“我昨天用jinfo改了参数呀,怎么今天一看恢复原样了?”,原因就是中间有一次发布或者重启。本质是jinfo没有持久化的能力,它只改了内存里JVM的Flag值。

所以正确的使用心智是:jinfo适合临时应急和现场诊断,稳定的参数调整必须落到启动脚本、配置文件或发布系统里。动态修改的一个实际经验是:先用jinfo做验证性修改,确认参数对进程行为有正向作用,再把这个参数补进启动脚本,找时间发布,这样才能让调整结果持久化。

4. 每日排查必看:jinfo高频报错和疑难问题实录

4.1 权限、Attach机制与进程身份导致的连不上

你在生产环境执行jinfo <pid>,报错最多的就是这两类:

第一类:Unable to open socket file: target process not responding or HotSpot VM not loaded。这个报错信息很常见,翻译一下就是jinfo已经尝试通过attach机制连接目标进程,但目标进程没有正确响应。常见原因有几个:

  • pid不对,或者进程已经不是Java进程了。
  • 目标Java进程在容器里,而你在宿主机上执行jinfo,看到的pid是宿主机的pid,不是容器内的pid。
  • JVM启用了-XX:+DisableAttachMechanism,主动拒绝了外部attach。

排查建议很简单:先用jps -l确认你自己能列出的Java进程,再比对pid。如果jps能列出来但jinfo连不上,优先怀疑是DisableAttachMechanism;如果jps列不出来,那大概率是身份或容器隔离问题。

第二类:权限不足。你在管理员账号下敲jinfo,去连另一个普通用户启动的Java进程,通常会报权限错误。原因就是前面提到的attach机制需要和目标进程同用户或者有root之类的足够权限。在大型企业里,运维统一用一个账号跑服务,那你就要切换成那个账号再执行jinfo,或者让有权限的人代跑。

也可能是操作系统层面的进程跟踪权限受限。比如某些安全策略限制了非root用户对他人进程的ptrace权限,这时候jinfo连接会被拒绝,即使你在进程的同一个用户下也不行。这里需要查看OS的安全配置文件,适当调整后再试。

4.2 修改参数后没变化,是姿势错了还是真的不能改

这是被问得最多的问题:“为什么我jinfo改了参数,再查还是老样子?”

分两种情况看:

第一种,参数本身不可管理。比如MaxMetaspaceSize、Xms这类参数,JVM不提供运行期修改能力。你执行jinfo -flag MaxMetaspaceSize=512m 1234,命令很可能没有任何报错,但你再jinfo -flag MaxMetaspaceSize 1234一查,值纹丝不动。这不是命令没生效,是JVM内部直接忽略了。对照第3.2节的可改参数表,先识别你要改的参数是不是可管理参数,不可管理的趁早别折腾,直接改启动脚本等重启。

第二种,值格式写错了。比如布尔参数的正确改法就是+号和-号,你写了jinfo -flag UseG1GC=true 1234这种格式,JVM是接受还是不接受,不同版本行为不一样。在老版本里可能报错,在新版本里可能被解析成开启标志,但语义有歧义。我建议统一使用+/-语法,不要用=true/false,避免踩坑。

还有的大坑是你没看仔细。比如你以为-Xmx对应的是MaxHeapSize,但是有的JDK版本里你单独查-Xmx查不到东西,因为jinfo只认-XX风格的名字,-X风格的名字需要转成MaxHeapSize这种内部Flag名才能查。换句话说,jinfo -flag Xmx 1234大概率返回一个“unknown flag”的错误,因为正确的内部名字是MaxHeapSize。

4.3 如何脚本化地提取单个参数值

日常巡检如果频繁需要查参数,纯手敲jinfo很累,我是这样处理的。写一个简单脚本,先把jinfo输出抓取出来,然后用文本处理工具提取目标参数:

#!/bin/bash PID=$1 FLAG=$2 VALUE=$(jinfo -flag "$FLAG" "$PID" | sed 's/.*=//' ) echo "$FLAG = $VALUE"

比如执行./getflag.sh 3412 MaxHeapSize,输出就是MaxHeapSize = 2147483648。要注意的是,输出值默认是字节为单位,如果你想要MB,记得除以1048576。别直接用这个数字和启动脚本里的2048m做字符串匹配,单位不同会误导判断。

还有一点,很多参数有别名和单位,脚本处理时最好维护一张单位换算表。比如MaxHeapSize输出字节,MaxGCPauseMillis输出毫秒,MetaspaceSize也输出字节。把这些统一处理好,你的巡检脚本才有较长周期的复用价值。

4.4 容器环境下的jinfo使用注意事项

现在服务经常跑在容器里,JVM在容器里的行为和使用习惯有差异,jinfo也不例外。这里分享几个实际经验:

首先,容器内执行的pid和宿主机看到的pid不是同一个视角。你如果从宿主机直接jinfo,大概率连不上或者连错进程。正确做法是先进入容器,在容器内执行jps和jinfo,拿到的才是容器视角的pid。

docker exec -it <container> bash # 在容器内执行 jps -l jinfo -flags <pid>

其次,容器环境里经常做了用户隔离。容器内应用默认用root或者专用用户跑,你从宿主机切进去的时候身份是宿主机用户,需要docker exec配合-u参数切换成容器内应用用户,否则attach依旧被权限卡住。

再一个坑是容器内存限制。你在容器里查看MaxHeapSize时,如果发现值和宿主机物理内存比例对不上,或者你明明配了-Xmx却没有生效,大概率是因为容器里的JVM开启了容器感知模式,读取的是cgroup限制而不是物理内存。这种问题用jinfo能发现,但解决得回到启动参数和容器资源配置层面去对齐。

4.5 常见报错速查表

错误现象可能原因我的解决建议
Unable to open socket filepid错误、进程非Java、DisableAttachMechanism被开启先用jps确认pid;检查启动参数是否禁用了attach
Permission denied或Operation not permitted跨用户attach、OS进程跟踪权限受限切换成目标进程的用户;检查OS安全配置
java.lang.Exception: Unknown flag参数名拼写错误,或不是-XX风格参数先查jinfo -flags输出里的真实内部名,转换成正确格式再查
命令找不到jinfoJAVA_HOME/bin不在PATH中用绝对路径执行$JAVA_HOME/bin/jinfo,或补环境变量
修改布尔参数无反应参数本身非可管理参数,或使用=true格式改用+/-语法;对照可管理参数清单排查
JDK 9之后不带参数直接报错默认行为被移除,需要显式指定选项改为jinfo -flags <pid>或jinfo -sysprops <pid>

这张表是我把这些年jinfo报错记录整理出来的,基本覆盖了大多数日常问题。真正意义上的疑难杂症,追根溯源都是出在“目标进程不能被attach”这一个核心问题上,先把这条链路排干净,其他都好解决。

5. 几个值得养成的使用习惯,也是我踩坑换来的经验

最后分享一点长期实践下来的个人体会。jinfo是个小工具,但用它的心态会直接影响诊断效率。

先说习惯问题。我强烈建议把jinfo纳入JVM问题排查的第一步,无论你接下来要不要看堆、要不要看线程,先看一眼参数总没错。参数是一切诊断的地基,你连堆大小、GC器都不知道是什么,后面分析再多内存和线程问题都像是盲人摸象。

再说“实时查看”这件事。很多人习惯把启动脚本当权威,但脚本只是你写的意图,JVM实际生效值被很多外部因素影响。jinfo这项能力不可替代,因为它能直接还原运行期真相。尤其是接手一个老系统时,启动脚本可能已经换了三任负责人,每个人都往里面加参数,甚至会读到全局环境变量。这种情况下,启动脚本已经不可信,只有jinfo能给出一份值得信赖的运行期参数清单。

最后,jinfo动态修改参数的能力要把持住。我见过有人把生产服务的MinHeapFreeRatio调来调去,结果引发GC频率飙升,比调参数之前还惨。动态调整参数是为了临时验证,不是为了常态化运维。常态化调参一定要走配置管理和发布流程,否则一旦进程重启,所有临时修改全部清零,线上行为飘忽不定,最终受害的还是我们自己。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进