提到 JVM 排查工具大家随口就是 jstat、jstack、jmapjinfo 反而常常被忽略。它属于 JDK 自带的诊断命令核心能力正好是标题里这九个字实时查看和修改 JVM 配置参数也就是你可以不重启、不停机对一个正在运行的 Java 进程做参数级操作。为什么值得单独聊聊因为线上环境里最贵的往往不是机器而是“重启一次”的代价——连接中断、缓存失效、注册中心摘流这些动作很多时候比故障本身还伤。我身边不少运维和开发朋友日常查 GC 先 jstat、查线程先 jstack真要改某个 JVM 参数的时候却默认以为只能改脚本重启压根没想到还有个 jinfo。这篇文章准备从命令用法、底层机制、线上实战、边界坑点四个方面把 jinfo 讲透。适合刚接触 JVM 诊断的 Java 开发者也适合常年和线上打交道的老手查漏补缺。1. jinfo 到底是干什么的一个被忽略的 JVM 配置操作面板1.1 jinfo 能回答的三个问题接手一个别人留下的 Java 服务如果启动脚本没归档、文档也语焉不详你通常会遇到三个挠头的问题这进程启动时到底加了哪些 -X 和 -XX 参数它当前实际生效的堆大小、GC 参数是不是和预期一致以及在不重启的前提下某个参数到底能不能临时改一改这三个问题正好落在 jinfo 的能力范围里。简单说jinfo 的作用可以拆成两条线查看和修改。查看线负责把进程当前的 JVM 标志VM flags、系统属性System properties和完整命令行拉出来修改线则针对单个标志做开、关、赋值的动态操作。用一个表可以看得很清楚你想做的事命令格式输出重点查看当前进程所有生效参数jinfo -flags pidNon-default VM flags、Command line查看某一个具体参数的值jinfo -flag 参数名 pid形如-XX:PrintGCDetails查看系统属性jinfo -sysprops pidjava.home、user.dir、user.timezone 等开一个布尔开关jinfo -flag 参数名 pid成功无输出回读确认关一个布尔开关jinfo -flag -参数名 pid成功无输出回读确认给参数赋一个值jinfo -flag 参数名值 pid成功无输出回读确认同时输出系统属性和参数jinfo pid两部分全量输出注意这里说的参数名不是启动命令里的-Xmx512m那种写法而是 JVM 内部的规范名称。比如-Xmx512m在 JVM 内部对应的是MaxHeapSize想用 jinfo 查看时得写jinfo -flag MaxHeapSize pid写jinfo -flag Xmx pid是查不到的。这个细节后面专门说。1.2 和 jstat、jstack、jmap 的分工如果把这几个 JDK 自带工具放在一起看定位其实特别清晰。jps 用来找进程jstat 负责看类加载、GC 次数和内存池占用jmap 用来打堆 dump 或查看堆直方图jstack 输出线程栈排查阻塞和死锁而 jinfo 管的则是“这台运行中的 JVM 到底是什么配置、能不能调”。它们之间没有太多重叠更像是一个配合使用的工具箱。打个比方jstat 是仪表盘告诉你车速、水温、转速jstack 是车内影像告诉你哪个座位的人卡住了jmap 是拆车取样把发动机内部结构拍出来分析而 jinfo 是那个可以让你在行驶过程中调节巡航速度、开关辅助功能的控制面板。关键差别就在“行驶过程中”——jinfo 不要求你把车停下来点火重启这是它在线上场景里最值钱的点。1.3 为什么它经常被忽略按理说这样的工具应该人人皆知但现实中 jinfo 的使用率明显低于 jstat 和 jstack。我猜原因有三个一是很多新手了解 JVM 诊断工具时资料通常按 jps、jstat、jmap、jstack 的顺序讲到 jinfo 往往只剩一句话带过二是在 JDK 9 之后官方把 jinfo 变成了 jcmd 的薄包装thin wrapper很多人干脆直接拥抱 jcmd自然就不再用 jinfo三是它支持的“动态修改”有严格限制动不动报 Invalid flag尝试受挫后大家容易把它划入“不好用”的类别。但我自己用下来的感受是在只需要“查一个参数、改一个开关”的场景里jinfo 的命令语义比 jcmd 简洁得多尤其适合写进脚本和快速排障。至于它到底能改哪些参数、为什么有的改不了咱们往下看。2. 五步实操从确认 PID 到动态改参数的完整命令链2.1 第一步先拿到目标进程的 PID所有 jinfo 操作的第一步都是确定 PID。最快的办法是jps -l它会列出当前机器上所有已启动的 Java 进程以及对应的主类或 jar 包$ jps -l 21891 /opt/app/app.jar 21992 sun.tools.jps.Jps如果机器上进程很多或者 jps 因为权限、容器等问题列不全就用ps -ef | grep java过滤或者通过端口反查ss -ltnp | grep 端口号看到 PID 之后再去和进程信息核对。特别提醒一句如果目标服务跑在容器里从宿主机直接执行 jinfo 往往会因为 PID namespace 不同而指错进程正确姿势是先进入容器执行或者在宿主机上找到容器真正对应的进程 ID。多个同名实例同时运行的时候建议养成“先用 jps -l 或者端口反查锁定 PID再执行操作”的习惯。我见过有人因为在多个实例之间复制了错误的 PID导致参数改到了别的服务头上虽然这类操作大多能现场回读发现但还是小心为妙。2.2 第二步用 jinfo -flags 看全量参数拿到 PID 之后先做全量快照。执行$ jinfo -flags 21891 Attaching to process ID 21891, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.202-b08 Non-default VM flags: -XX:CICompilerCount3 -XX:ConcGCThreads2 -XX:InitialHeapSize536870912 -XX:MaxHeapSize1073741824 -XX:MaxNewSize357564416 -XX:MinHeapDeltaMilliseconds... -XX:UseG1GC Command line: -Xms512m -Xmx1024m -XX:UseG1GC -Dspring.profiles.activeprod -jar app.jarJDK 8 下输出很典型分两段Non-default VM flags显示当前生效且和默认值不一样的标志Command line显示启动时原始命令行。为什么要区分因为 JVM 在启动过程中会发生参数联动比如你只写了-Xms512m -Xmx1024m内部会推导出 InitialHeapSize、MaxHeapSize还可能连带调整新生代参数只看 Command line 看不到完整生效配置只看 Non-default 又容易忽略参数来源。两个对照着看才是完整的现状。JDK 之外的进程如果输出格式略有不同也不要慌新版 JDK 下 jinfo -flags 输出的一般是类似Flags:的一段本质上同样是当前生效标志的汇总。2.3 第三步用 jinfo -flag 查单个参数全量输出信息量大直接查单个参数更高效。比如想知道当前用的堆上限是不是 Xmx 指定的值执行$ jinfo -flag MaxHeapSize 21891 -XX:MaxHeapSize1073741824这里就得说回前面的参数名问题。JVM 参数有两种层面shell 层面的启动参数-Xmx1024m、-XX:UseG1GC以及 HotSpot 内部登记在全局标志表里的正式名称MaxHeapSize、UseG1GC。jinfo 只认后者。启动参数里的速记符号会在 JVM 初始化时被展开翻译所以你想查堆上限应该用 MaxHeapSize查元空间上限用 MaxMetaspaceSize查年轻代最大比例之类用 MaxNewSize。布尔型参数查出来是-XX:参数名或-XX:-参数名的形式加号表示开减号表示关数值型参数则是-XX:参数名值。看到这个输出你就知道当前进程的真实运行配置到底是什么了而不必依赖启动脚本是不是被人改过。2.4 第四步动态修改参数的三连操作修改参数是 jinfo 最有魅力也最容易翻车的地方。基本操作分三种布尔开关用加号或减号赋值型参数用等号。# 开一个开关 jinfo -flag PrintGCDetails 21891 # 关一个开关 jinfo -flag -PrintGCDetails 21891 # 给参数赋值 jinfo -flag MaxHeapFreeRatio40 21891命令执行完通常没有任何提示这不代表没生效而是意味着调用成功。正确做法是修改后马上回读验证$ jinfo -flag PrintGCDetails 21891 -XX:PrintGCDetails这里有一个非常关键的概念必须明确jinfo 的修改只对当前运行中的进程实例有效重启之后就没了。它适合临时救急、现场验证、短期观察不适合做长期配置变更。如果你想把某个参数固化下来正确动作是修改启动脚本、容器启动配置或配置中心而不是指望 jinfo 一改永逸。2.5 第五步不加选项的 jinfo 和 -sysprops有时候一个服务行为很怪但又说不出具体哪不对直接执行jinfo pid会把系统属性和 VM flags 一次性打出来信息量最大但也是真的大。我更推荐先用jinfo -sysprops pid有目的地看系统属性$ jinfo -sysprops 21891 | grep -E java.home|user.timezone|java.io.tmpdir|java.class.path这类信息在排查“配置文件改了半天不生效”的场景里特别好用。比如怀疑时区不对直接看 user.timezone怀疑临时目录权限导致上传失败看 java.io.tmpdir怀疑 classpath 里混入旧版本 jar看 java.class.path。注意一点输出里可能包含路径、环境变量等敏感信息粘贴到工单或聊天工具时适当打码。3. 参数的“可改性”从哪来HotSpot 标志机制和生效规则3.1 HotSpot 里参数不是平级的一堆变量很多刚接触的人以为 JVM 参数就是配置文件里的 key-value想改就改。实际上 HotSpot 对每个全局标志都有严格的属性分类源码里的标志表会标注它是 product、develop、diagnostic、experimental、manageable 中的哪一类。平时我们通过-XX:参数名值设置的大多是 product 和 manageable而 diagnostic 和 experimental 需要先通过-XX:UnlockDiagnosticVMOptions这类“解锁”开关才允许设置。真正允许在运行期动态修改的是其中一类manageable可管理标志。这类标志在设计之初就考虑了运行期调整HotSpot 在底层会通过 VM 线程完成同步修改保证线程安全不会因为一个外部命令写了半截导致 JVM 状态异常。剩下的标志哪怕你写的参数名完全正确jinfo 也会拒绝执行直接告诉你 Invalid flag。3.2 哪些常见参数能改哪些只能看我把自己实测过、日常高频用到的参数整理成一张表方便对照类别常见参数说明可动态开关PrintGC、PrintGCDetails现场开启 GC 日志可动态开关HeapDumpOnOutOfMemoryError现场开启 OOM dump通常可动态赋值MaxHeapFreeRatio、MinHeapFreeRatio影响堆伸缩上下限通常可动态指定HeapDumpPath部分 JDK 版本支持现场改路径只能启动时指定MaxHeapSize、InitialHeapSize堆容量选型不能办只能启动时指定UseG1GC、UseParallelGC 等垃圾收集器选型不能办只能启动时指定NewRatio、SurvivorRatio内存结构调整基本都不能办只能启动时指定MaxTenuringThreshold多数环境下不支持动态改这张表不是绝对真理因为不同 JDK 版本对某些参数的管理属性做过调整。最靠谱的判断方法永远是现场试一下执行jinfo -flag 参数名 pid如果命令成功且回读为说明能改如果抛出 IOException、提示 Invalid flag那就是不能改没有第二种解读。3.3 生效时机和范围就算参数属于可管理集合修改后的生效也不是“下一秒所有线程立刻切换”那么机械化。拿 GC 日志开关举例你打开 PrintGCDetailsJVM 并不会立刻打印一行“日志已开启”而是从下一次 GC 事件开始按新开关打印堆伸缩比例参数 MaxHeapFreeRatio 同理它会在 GC 周期里做堆容量决策时被读取。所以刚改完发现输出没变化先别急着怀疑改失败了观察一两个 GC 周期再下结论。另一个容易忽略的性质是修改范围。jinfo 改的是对应 Java 进程 Native 层的标志位应用代码里 System.getProperties() 也拿不到这部分内容两者是两条平行的线。而且所有修改都不持久化进程一退出就归零。这也解释了为什么 jinfo 适合做“临时操作”不适合做“标准配置”。3.4 看懂 jinfo 的报错实操中常见的报错基本就三类。第一类是参数名写错比如大小写不对、把MaxHeapSize写成maxheapSize通常会提示 Unknown flag 或 no such flag处理方式是先用jinfo -flags核对准确名称。第二类是参数不可改服务进程直接返回 Invalid flag说明这个参数没有 manageable 属性别试图通过换代码或换工具硬来老老实实重启或者走正式变更。第三类是 attach 失败报错可能涉及 permission denied、无法打开 socket 文件等这通常不是参数问题而是进程权限、容器限制或安全策略导致连接不上下一节专门讲。4. 线上救急场景三个我实际用 jinfo 解决问题的例子4.1 忘记开 GC 日志临时打开 PrintGCDetails有一次帮同事排查一个 Java 服务的频繁 FullGCjstat 显示老年代回收极其频繁但启动脚本当初图省事没加 GC 日志参数。按传统思路要么重启加参数要么干瞪眼。重启对于有状态的实例来说代价不小我直接执行jinfo -flag PrintGCDetails 21891 jinfo -flag PrintGCDetails 21891第一条开启第二条验证输出变成-XX:PrintGCDetails从下一个 GC 周期起日志就开始吐了。这里有一个实操细节要知道如果启动时已经配了-Xloggc:/data/logs/gc.log打开 PrintGCDetails 后日志会继续写到这个文件里如果启动时没配-XloggcJDK 8 会把 GC 日志输出到 stdout跟业务日志混在一起。所以动这个开关前先看一眼jinfo -flags的输出里有没有 Xloggc心里有数再动手避免日志被淹没找不到。这种操作本质是“先止血再根治”。临时打开拿到证据后还是要把 GC 日志参数固化到启动脚本或容器配置里同时建议一并配好 OOM dump 相关参数。4.2 内存持续上涨临时开启堆转储保留事故现场另一个高频场景是内存快扛不住了但你觉得离 OOM 还有一段时间希望万一真 OOM 能留下现场文件。这时候改启动脚本重启显然不划算jinfo 可以在运行中把 OOM dump 开关打开jinfo -flag HeapDumpOnOutOfMemoryError 21891 jinfo -flag HeapDumpPath/data/dump 21891 jinfo -flag HeapDumpOnOutOfMemoryError 21891三条命令分别完成开启自动 dump、指定 dump 目录、回读确认。之后如果进程发生 OutOfMemoryErrorJVM 会按 HeapDumpPath 指定的目录生成文件目录模式下文件名一般是java_pid进程号.hprof。注意先确认目标目录存在、可写、磁盘空间够否则 dump 也会失败。需要强调的是这个开关只管“下一次 OOM”。如果当前内存水位已经非常危险别指望它代替主动手段该上 jmap 抓现场、该扩容扩容、该重启重启开关只是兜底。4.3 容器内存吃紧现场收敛堆伸缩空档有的服务堆最大值给得很大但实际负载忽高忽低JVM 默认的堆伸缩策略会在空闲时保留较多内存容器内存配额本来就紧很容易触发整机 OOM。这种时候在启动层面把 MaxHeapFreeRatio 从默认 70 调低可以明显缓解但改启动脚本又得等重启窗口。jinfo 现场调整就很合适jinfo -flag MaxHeapFreeRatio40 21891 jinfo -flag MinHeapFreeRatio20 21891MinHeapFreeRatio 和 MaxHeapFreeRatio 控制的是 GC 后堆空闲比例的“下限”和“上限”配合影响 JVM 是否决定扩展或收缩堆。注意赋值范围要合法且 Min 不能大于 Max修改后回读确认观察几轮 GC 再看内存曲线。这个操作只改变当前实例的伸缩策略长期还是应该回到启动脚本层面统一管理否则下次重启又会恢复默认。4.4 用 -sysprops 快速判断配置来源问题还有一种不算“救急”但很常用的场景Java 应用读到的配置和预期不一致。常见例子是明明改了环境变量应用却拿旧值改了时区配置日志时间戳还是偏的。与其翻日志猜原因不如直接用 jinfo 看系统属性jinfo -sysprops 21891 | grep -E user.timezone|java.io.tmpdir如果 user.timezone 还是系统默认值说明应用并没有读取你期望的配置路径如果 java.io.tmpdir 指向一个不存在或不可写的目录上传文件报错也就顺理成章了。这类排查往往比在代码里打日志更快尤其适合那些没有 debug 条件的生产环境。5. jinfo 的边界与坑哪些情况不能用、换了 jcmd 又如何5.1 JDK 9 之后的 jinfo 其实是一层壳JDK 9 发布之后jinfo 底层发生了比较大的变化官方把它的实现改成了 jcmd 的 thin wrapper也就是说你敲jinfo -flags pid本质上调用的是 jcmd 的参数输出能力只是保留了 jinfo 这么个更简短的命令入口。这带来一个结果JDK 8 下很顺手的用法在 JDK 9 上行为可能略有差异比如输出格式、报错文案不一样。好在核心能力没丢下面是 jinfo 和 jcmd 的映射关系jinfo 命令等价的 jcmd 命令jinfo -flags pidjcmd pid VM.flagsjinfo -sysprops pidjcmd pid VM.system_propertiesjinfo -flag name pidjcmd pid VM.set_flag name truejinfo -flag -name pidjcmd pid VM.set_flag name falsejinfo -flag nameval pidjcmd pid VM.set_flag name val所以在 JDK 9 环境里如果你想看所有参数的当前值我会推荐直接用jcmd pid VM.flags -all这个能力 jinfo 没有。但日常快速改一个开关我还是习惯敲 jinfo短、好记、不容易写错。5.2 容器和权限导致的 attach 失败jinfo 和 jcmd、jmap、jstat 这类工具依赖 JVM 的 attach 机制进程之间需要建立连接而这个连接在实际生产环境里相当挑剔。常见的失败情形有这么几种目标 JVM 以其他用户身份运行而当前用户权限不够部署平台对进程做了 ptrace 限制或者容器没有放开相应能力attach 过程中需要写临时文件而 /tmp 不可写。这些场景下 jinfo 可能直接报 permission denied或者连接超时。遇到这种问题别在参数上纠结先解决通路。进入容器、切换用户、或者在运维平台上执行通常都能绕过去。这里也提醒一句如果线上 JVM 启动时加了-XX:-UsePerfData这类安全加固参数jps 可能直接找不到进程jstat 也读不到数据相关工具都会连带受影响jinfo 同样可能碰壁。这属于“为了安全锁死了诊断通道”的取舍平时知道有这么回事就行。5.3 远程模式不是你想的那种远程看 jinfo 帮助文档会发现它还支持一种远程用法格式类似jinfo [option] [server_id]hostname。很多朋友以为这是像 SSH 一样连过去就能用其实不是。这个远程模式连接的是目标 JVM 上由 SA 调试服务jsadebugd 之类暴露的调试通道需要在目标机器提前启动并配置好对应的调试代理生产环境出于安全考虑基本不会开。真要在远端操作 JVM常规做法还是通过跳板机、运维平台或已统一部署的 Agent 体系完成而不是指望 jinfo 内置远程管理。所以我的建议是把 jinfo 当成本地、单机、命令行场景的利器远程场景交给公司已有的运维体系别试图发明新用法。5.4 操作纪律改前留证、改后验证、随手恢复因为 jinfo 改参数太“容易”反而更容易出问题。我自己实践下来有几条纪律是必须遵守的。改之前先执行一次jinfo -flags pid把当前参数快照保存下来万一改错了还能知道原本是什么一次只改一个参数避免多个变量同时变化导致无法判断因果改完立刻回读验证确认输出符合预期用完记得恢复或者至少记录在案避免下次排查时参数集合和启动脚本不一致造成“灵异问题”。对于核心业务、高可用要求的系统这类操作最好纳入变更审批流程至少留个操作记录。还有一条是心态上的jinfo 能改的参数在 JVM 全部标志里只是很小一部分这是 HotSpot 有意为之的安全设计。遇到改不了的参数正确的态度是“这个参数本来就不该运行期动”而不是研究怎么绕过限制。我见过有人试图通过反射、Native 内存改写等方式强行修改不可管理参数结果把 JVM 搞崩的例子这完全得不偿失。5.5 什么时候该放弃 jinfo最后说说什么时候别用 jinfo。如果你想做的事需要修改堆大小上限 Xmx、更换垃圾收集器、调整新生代比例这类结构性配置jinfo 帮不了你老老实实走变更流程重启。如果你需要看线程栈、打堆快照、跑 GC root 分析jinfo 也不是干这个的转头去找 jstack、jmap、jcmd。在线上稳定运行的系统上任何诊断工具的定位都是“低成本获取信息”而不是替代变更流程本身。我自己现在的工作流已经比较固定jps 找进程jinfo -flags 拍快照单个参数用 jinfo -flag 查和改遇到 JDK 9 想看全量参数就切 jcmd。说实话jinfo 能改的参数就那么多但恰恰是 GC 日志、OOM dump、堆伸缩这几项高频需求让它在实战里几乎没有替代品。如果你也想把它纳入自己的工具箱建议先在测试环境开几个开关、故意改几个不可改参数亲眼看一下成功和失败的两种输出慢慢就有感觉了。祝大家线上不宕机参数不乱改。