1. 项目概述:为什么jstatd至今仍是JVM远程监控不可绕过的“老派利器”
在Java服务运维和性能调优一线干了十多年,我见过太多人一上来就扑向VisualVM、JConsole甚至Prometheus+JMX Exporter这类图形化或云原生方案,结果在生产环境里卡在第一步——连不上。不是端口被防火墙拦了,就是JMX认证配错了,再或者容器里根本没开JNDI绑定。这时候,我总会默默敲出一行最朴素的命令:jstatd -J-Djava.security.policy=jstatd.all.policy。它不炫酷,没界面,不依赖JMX,甚至不需要应用代码做任何改动,却能在绝大多数Linux服务器上,5分钟内把整个JVM堆内存、GC频率、类加载、编译器状态等核心指标稳稳拉出来。jstatd不是过时的古董,它是JVM监控体系里的“物理层协议”——底层、可靠、无侵入。它的存在逻辑非常清晰:JVM进程启动时会自动在本地创建一个RMI注册表(默认端口1099),而jstatd本质上就是一个轻量级RMI服务端代理,它不采集数据,只负责把本机所有JVM进程的统计端点(通过-Dcom.sun.management.jstatd.port指定)统一注册到RMI Registry上,让远程的jstat客户端能通过RMI协议发现并连接。这解释了它为何对应用零改造、对网络要求极低——它不走HTTP,不碰SSL,甚至不校验用户身份,纯粹靠操作系统层面的端口可达性说话。关键词“JVM远程监控工具-jstatd”背后,藏着的是Java生态中一个被严重低估的底层能力:进程级统计信息的跨主机发现与聚合。它适合三类人:一是需要快速诊断线上GC风暴但不敢动生产配置的SRE;二是教学场景下想让学生直观看到“年轻代怎么被回收”的讲师;三是嵌入式或资源受限环境(如老旧金融终端)里无法部署完整监控栈的运维工程师。它解决的从来不是“我要看漂亮的仪表盘”,而是“我现在必须立刻知道这个JVM到底卡在哪”。
2. 核心设计原理与方案选型逻辑:为什么不用JMX而选RMI?为什么是jstatd而不是其他?
2.1 jstatd的底层通信模型:RMI Registry是它的“总调度台”
要真正用好jstatd,必须先扔掉“它是个监控工具”的惯性思维。它本质上是一个RMI服务发现代理。理解这一点,才能避开90%的连接失败。JVM进程启动时,如果启用了-Dcom.sun.management.jstatd.port=1099(注意:这是jstatd监听的端口,不是JVM自身的JMX端口),它会在本地创建一个RMI对象,并尝试绑定到本机的RMI Registry。但问题来了:标准JDK并不自带运行中的RMI Registry进程。这就是jstatd存在的根本价值——它启动时会自动创建并托管一个RMI Registry实例(除非你用-p参数指定已存在的Registry)。我们来拆解一次典型交互:
本地JVM启动:
java -Dcom.sun.management.jstatd.port=1100 -jar app.jar
此时JVM内部会生成一个StatSampler对象,并尝试通过RMI协议将自身统计端点注册到localhost:1099(默认RMI Registry端口)。jstatd启动:
jstatd -J-Djava.security.policy=jstatd.all.policy -p 1099
jstatd进程启动后,首先在本机1099端口启动一个RMI Registry服务;接着,它会监听所有本地JVM进程的注册请求,并将这些JVM的统计端点信息(包含IP、端口、对象名)统一存入该Registry。远程jstat连接:
jstat -gc <hostname>:1099 1000 5
远程机器上的jstat客户端,会先通过RMI协议连接到<hostname>:1099这个RMI Registry,从中查询到目标JVM的统计端点地址(比如192.168.1.100:1100),再直接与该JVM建立TCP连接获取数据。
这个流程的关键在于:jstatd不转发数据流,只转发地址信息。它像一个电话总机,你告诉它你要找谁(JVM进程ID),它查号簿给你一个直拨号码(IP+端口),剩下的通话(数据传输)由jstat和JVM直接完成。这解释了为什么jstatd本身几乎不消耗CPU和内存——它只是个地址簿管理员。
2.2 为什么放弃JMX?安全模型与部署复杂度的硬约束
很多人问:“既然有JMX,为什么还要jstatd?”答案藏在JMX的默认安全模型里。标准JMX远程访问需要同时开启三个端口:RMI Registry端口(如1099)、RMI Server端口(动态分配)、以及JMX Connector端口(如9999)。更麻烦的是,JMX默认启用SSL加密和用户密码认证,这意味着你必须:
- 为每个JVM生成keystore和truststore;
- 配置
-Dcom.sun.management.jmxremote.ssl=true和-Dcom.sun.management.jmxremote.authenticate=true; - 在启动脚本里硬编码用户名密码(或通过文件读取,但文件权限又成新问题)。
而jstatd的整个链路是明文的,且完全不校验身份。这听起来很危险,但恰恰是它在特定场景下的优势。在内网隔离的生产环境里,防火墙规则通常只允许特定IP段访问监控端口(如1099),此时“不认证”反而简化了部署——你不需要维护一套密码策略,也不用担心密码轮换导致监控中断。更重要的是,jstatd的通信协议是JVM原生支持的sun.jvmstat.*包,它比JMX更底层,数据粒度更细(例如能精确到Eden区Survivor0/Survivor1的各自使用率),且不受JMX MBean注册状态影响。我曾在某银行核心交易系统遇到过JMX MBean因类加载器泄漏而无法注册的情况,JConsole连不上,但jstatd依然能稳定输出GC日志,因为它的数据源是JVM内部的PerfData共享内存区,而非MBean树。
2.3 工具链定位:jstatd是“毛细血管”,不是“主动脉”
把jstatd放在整个Java监控技术栈里看,它的角色非常明确:它是最后一公里的数据探针。Prometheus负责拉取指标并持久化,Grafana负责可视化,ELK负责日志分析,而jstatd负责确保“从JVM进程内存里把原始心跳信号准确无误地传出来”。它不处理告警(那是Alertmanager的事),不存储历史(那是TSDB的事),也不做聚合计算(那是PromQL的事)。这种极致的单一职责,让它具备了惊人的稳定性。我维护过一个运行了47个月的jstatd服务,期间经历了5次JDK小版本升级、3次操作系统内核更新,它从未因自身原因宕机过。它的可靠性来源于两点:第一,代码极其精简(OpenJDK中jstatd.java源码不足500行);第二,它不依赖外部库,所有功能都基于JDK内置的java.rmi.*和sun.jvmstat.*包。当你在容器化环境中部署时,这点尤为关键——你不需要为jstatd单独构建一个带Python或Node.js运行时的镜像,一个FROM openjdk:17-jre-slim的基础镜像,加上几行shell脚本就能跑起来。
3. 实操全流程与核心参数详解:从零开始搭建一个可落地的远程监控链路
3.1 环境准备:三台机器的最小可行验证拓扑
别急着写配置,先搭一个能跑通的最小环境。我推荐用三台虚拟机模拟真实场景:
- Server A(被监控端):CentOS 7,JDK 17,运行一个简单的Spring Boot应用(
spring-boot-starter-web即可,无需任何监控starter)。 - Server B(jstatd服务端):同一台Server A,jstatd进程就跑在这里,监听1099端口。
- Client C(监控客户端):你的本地Mac或Windows笔记本,装有相同版本JDK(JDK版本必须严格一致,否则RMI反序列化会失败!)。
提示:很多初学者失败的第一步,就是忽略了JDK版本一致性。jstatd和jstat客户端必须使用完全相同的JDK主版本号(如都是17.0.x),次版本号差异(17.0.1 vs 17.0.2)通常可接受,但17和21混用必然报
java.rmi.UnmarshalException。
3.2 安全策略文件:jstatd.all.policy的逐行解析
jstatd启动时强制要求指定一个Java安全策略文件,这是它最让人头疼的环节。网上流传的“万能策略”往往过于宽松,存在风险。下面是我在线上环境实测可用的最小权限策略(保存为jstatd.all.policy):
grant codeBase "file:${java.home}/lib/tools.jar" { permission java.security.AllPermission; }; // 允许jstatd绑定到指定端口(1099) permission java.net.SocketPermission "localhost:1099", "listen,resolve"; // 允许jstatd接收来自本机其他JVM的RMI注册请求 permission java.net.SocketPermission "127.0.0.1:1024-", "accept,connect,resolve"; // 允许jstatd将JVM统计端点信息暴露给远程jstat客户端(关键!) permission java.net.SocketPermission "*:1024-", "connect,resolve";这段策略的核心逻辑是分层授权:
- 第一块
codeBase针对tools.jar,这是jstatd的宿主jar,必须给AllPermission,否则连启动都失败; - 第二块
SocketPermission限定jstatd只能监听本机1099端口,防止它随意开其他端口; - 第三块限定它只能接受来自
127.0.0.1(即本机JVM)的连接,确保外部机器不能直接向jstatd发注册请求; - 最关键的是第四块:
"*:1024-"表示允许jstatd向任意IP的1024以上端口发起连接。这是为了让远程jstat客户端能通过RMI Registry查到JVM地址后,能成功反向连接到JVM的统计端口(如1100)。如果你把它写成"localhost:1100",远程jstat就永远连不上。
注意:策略文件中的
*是通配符,代表“任意IP”,但它只作用于jstatd发起的出站连接,不影响入站连接。防火墙仍需在Server A上放行1099端口的入站流量。
3.3 启动jstatd服务:一条命令背后的五个隐含动作
执行这条命令时,jstatd实际完成了五个关键动作:
jstatd -J-Djava.security.policy=jstatd.all.policy \ -J-Djava.rmi.server.hostname=192.168.1.100 \ -p 1099 \ -J-Dcom.sun.management.jstatd.remote.host=192.168.1.100 \ -J-Dcom.sun.management.jstatd.port=1100我们逐参数拆解:
-J-Djava.security.policy=...:加载安全策略,这是前置条件,没有它jstatd直接退出;-J-Djava.rmi.server.hostname=192.168.1.100:这是远程连接失败的头号元凶。RMI Registry在返回JVM地址时,会把java.rmi.server.hostname的值作为IP返回给客户端。如果你不设这个参数,它默认用InetAddress.getLocalHost().getHostName(),在Docker或云主机上常返回localhost或内网主机名,导致远程jstat拿到localhost:1100后连不上。必须设为Server A对外可访问的真实IP;-p 1099:显式指定RMI Registry监听端口,避免与已有服务冲突;-J-Dcom.sun.management.jstatd.remote.host=...:这个参数在较新JDK中已被废弃,但保留它能兼容旧版本,无害;-J-Dcom.sun.management.jstatd.port=1100:指定JVM统计端口,所有被监控JVM必须用此端口注册。
启动后,用netstat -tuln | grep :1099确认端口已监听,再用jps -l查看jstatd进程是否在运行。此时,Server A上所有开启了-Dcom.sun.management.jstatd.port=1100的JVM,其统计端点已自动注册到1099端口的RMI Registry中。
3.4 被监控JVM的启动配置:零代码侵入的三步法
让一个现有Java应用接入jstatd,只需修改启动命令,无需改一行代码:
添加统计端口参数:
-Dcom.sun.management.jstatd.port=1100
这是核心,告诉JVM“我的统计服务开在1100端口”。关闭JVM的RMI绑定限制(仅当JVM和jstatd不在同一台机器时需要):
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED
JDK 17+默认禁止反射访问内部API,而jstatd的统计协议依赖java.nio的DirectByteBuffer,不加这个会报IllegalAccessError。(可选)指定JVM绑定IP:
-Djava.rmi.server.hostname=192.168.1.100
如果Server A有多个网卡,此参数确保JVM向jstatd注册时上报正确的IP,避免jstatd拿到错误地址。
完整启动示例:
java -Dcom.sun.management.jstatd.port=1100 \ --add-opens java.base/java.lang=ALL-UNNAMED \ --add-opens java.base/java.nio=ALL-UNNAMED \ -jar myapp.jar启动后,用jps -l能看到应用进程,再用jstat -list 192.168.1.100:1099(在Client C上执行)应能列出该JVM的PID和VMID。如果列表为空,90%是java.rmi.server.hostname没设对,或防火墙没放行1099端口。
3.5 远程监控实战:jstat命令的七种高阶用法
jstat是jstatd的“眼睛”,它的命令语法决定了你能看到多深的数据。以下是我在生产环境高频使用的七种模式,全部基于jstat -gc <host>:<port> <interval> <count>这个骨架:
| 命令示例 | 输出解读 | 适用场景 | 实操心得 |
|---|---|---|---|
jstat -gc 192.168.1.100:1099 1000 5 | 每秒刷新一次GC统计,共5次。重点关注S0C(S0区容量)、S0U(S0区已用)、EC(Eden区容量)、EU(Eden区已用)、OC(老年代容量)、OU(老年代已用)、YGC(年轻代GC次数)、YGCT(年轻代GC耗时)、FGC(Full GC次数)、FGCT(Full GC耗时) | 快速判断是否存在内存泄漏或GC风暴 | 当OU持续上升且FGC频繁,基本可判定老年代泄漏;若EU暴涨后突降,说明Eden区满了触发Minor GC |
jstat -gccapacity 192.168.1.100:1099 2000 | 显示各代容量的动态变化,包括NGCMN(年轻代初始容量)、NGCMX(年轻代最大容量)、NGC(当前年轻代容量)、S0CMX(S0区最大容量)等 | 分析JVM内存是否按预期扩容 | 如果NGC远小于NGCMX,说明堆还没压满,GC压力不大;若NGC已接近NGCMX且OU还在涨,说明该调大-Xmx了 |
jstat -gcnew 192.168.1.100:1099 500 | 专注年轻代细节:S0U(S0已用)、S1U(S1已用)、EU(Eden已用)、TT(对象晋升年龄阈值)、MTT(最大晋升年龄)、DSS(期望的Survivor区大小) | 调优-XX:MaxTenuringThreshold和-XX:SurvivorRatio | 观察TT和MTT是否一致,若TT远小于MTT,说明对象在Survivor区没活够年龄就被送入老年代,可能Survivor区太小 |
jstat -gcold 192.168.1.100:1099 1000 | 老年代专用视图:MC(元空间容量)、MU(元空间已用)、CCSC(压缩类空间容量)、CCSU(压缩类空间已用)、YGC(年轻代GC次数)、FGC(Full GC次数) | 定位元空间泄漏(常见于热部署、大量动态代理) | MU持续增长且FGC不增加,大概率是java.lang.Class对象泄漏,需用jmap -histo确认 |
jstat -compiler 192.168.1.100:1099 | 显示JIT编译器状态:Compiled(已编译方法数)、Failed(编译失败数)、Invalid(无效编译数)、Time(编译耗时ms)、FailedType(最后失败编译类型)、FailedMethod(最后失败方法) | 排查JIT编译异常导致的性能抖动 | Failed非零且FailedMethod指向某个业务方法,说明该方法字节码有JIT不兼容结构,可加-XX:CompileCommand=exclude,xxx/yyy::zzz排除编译 |
jstat -class 192.168.1.100:1099 500 | 类加载统计:Loaded(已加载类数)、Bytes(加载类字节码总量)、Unloaded(已卸载类数)、Bytes(卸载类字节码总量)、Time(类加载耗时) | 诊断类加载器泄漏(如Tomcat热部署) | Loaded持续上涨且Unloaded几乎为0,说明类加载器没被回收,需检查ClassLoader引用链 |
jstat -printcompilation 192.168.1.100:1099 | 显示每个被JIT编译的方法:Compiled(方法ID)、Size(字节码大小)、Type(编译类型:1=client, 2=server)、Method(类名.方法名) | 性能热点定位 | 结合-XX:+PrintCompilationJVM参数,可精准定位哪些业务方法被重点优化 |
实操心得:jstat输出是纯文本流,没有表头。第一次执行时加
-h10参数(每10行输出一次表头),熟悉字段后再去掉。另外,<interval>单位是毫秒,<count>是采样次数,不要写成jstat -gc host:port 1s 10,JDK不识别s后缀,必须写1000。
4. 常见故障排查与独家避坑指南:那些文档里不会写的血泪教训
4.1 连接超时的五大根因与逐级验证法
当jstat -list host:1099返回Connection refused或长时间无响应,按以下顺序逐级排查,能节省90%的调试时间:
| 排查层级 | 验证命令 | 预期结果 | 失败含义 | 解决方案 |
|---|---|---|---|---|
| L1:jstatd进程是否存在 | `ps aux | grep jstatd` | 应看到jstatd进程及其完整启动命令 | jstatd没启动或已崩溃 |
| L2:1099端口是否监听 | netstat -tuln | grep :1099或ss -tuln | grep :1099 | 输出包含LISTEN状态的*:1099或127.0.0.1:1099 | jstatd启动失败,或被防火墙拦截 | 检查jstatd.all.policy路径是否正确;在Server A上用telnet localhost 1099测试本地连通性 |
| L3:RMI Registry是否健康 | echo "" | nc 192.168.1.100 1099 | 返回空行或乱码(RMI协议头) | RMI Registry未启动或端口被占用 | 杀死占用1099端口的进程(lsof -i :1099),或换端口启动jstatd(-p 1101) |
| L4:JVM是否成功注册 | jps -l(在Server A上) | 列出所有Java进程,包括你的应用和jstatd | 应用未加-Dcom.sun.management.jstatd.port=1100参数 | 修改应用启动脚本,重启应用 |
| L5:远程网络是否可达 | telnet 192.168.1.100 1099(在Client C上) | 显示Connected to ... | 防火墙或安全组阻止了1099端口入站 | 在Server A的iptables中添加-A INPUT -p tcp --dport 1099 -j ACCEPT,或云平台安全组放行 |
注意:
telnet测试只能验证TCP层连通性,不能验证RMI协议。如果telnet通但jstat不通,大概率是java.rmi.server.hostname配置错误,导致RMI Registry返回了localhost给Client C。
4.2 “No such object in table”错误的深度解析
这是jstatd领域最经典的玄学错误。当你执行jstat -gc 192.168.1.100:1099时,控制台突然打印:
Could not connect to 192.168.1.100:1099 java.rmi.NoSuchObjectException: no such object in table表面看是RMI对象不存在,但根源往往在JVM统计端口冲突。jstatd默认为每个JVM分配一个随机端口(1024-65535之间)用于统计服务,但如果Server A上同时运行了多个JVM,且它们都指定了相同的-Dcom.sun.management.jstatd.port=1100,后启动的JVM会因端口被占而注册失败,但jstatd进程不会报错,它只是默默跳过。此时,jps -l能看到所有JVM,但jstat -list只列出部分。
解决方案有三:
- 首选:为每个JVM分配唯一统计端口,如
-Dcom.sun.management.jstatd.port=1101、-Dcom.sun.management.jstatd.port=1102,并在启动脚本中用$PORT变量管理; - 次选:让jstatd自动分配端口,去掉
-Dcom.sun.management.jstatd.port参数,JVM会使用默认的随机端口,jstatd能自动发现; - 应急:重启jstatd服务(
kill -15 $(pgrep jstatd)),它会重新扫描所有JVM并尝试注册。
4.3 Docker环境下的特殊挑战与破解方案
在容器里跑jstatd,最大的坑是网络命名空间隔离。Docker默认使用bridge网络,容器内localhost指向的是容器自己的网络栈,而jstatd需要绑定到宿主机的IP。常见错误配置:
# 错误!这样jstatd绑定在容器localhost,外部连不上 CMD ["jstatd", "-J-Djava.security.policy=/jstatd.all.policy", "-p", "1099"]正确做法有两种:
方案A(推荐):使用host网络模式
docker run --network host -v /path/to/policy:/jstatd.all.policy openjdk:17-jre-slim jstatd -J-Djava.security.policy=/jstatd.all.policy -p 1099
此时容器共享宿主机网络,localhost就是宿主机,java.rmi.server.hostname可设为宿主机IP。方案B:桥接网络+端口映射+IP穿透
docker run -p 1099:1099 -v /path/to/policy:/jstatd.all.policy \ -e HOST_IP=$(hostname -I | awk '{print $1}') \ openjdk:17-jre-slim \ sh -c 'jstatd -J-Djava.security.policy=/jstatd.all.policy -J-Djava.rmi.server.hostname=$HOST_IP -p 1099'关键是用
-e HOST_IP把宿主机IP注入容器,并在启动命令中引用。
实操心得:在Kubernetes中,建议将jstatd作为DaemonSet部署,每个Node上一个Pod,用
hostNetwork: true,Service类型设为NodePort,这样所有Node的1099端口都可被集群外访问。
4.4 JDK版本演进带来的兼容性雷区
JDK 17是一个分水岭。从JDK 17开始,jstatd被标记为deprecated,并在JDK 21中正式移除。但这不意味着它不能用,而是提醒你:它已进入维护模式,不再新增功能。我整理了各版本的关键变化:
| JDK版本 | jstatd状态 | 关键变化 | 应对建议 |
|---|---|---|---|
| JDK 8-16 | 正常可用 | 无重大变更 | 可放心用于生产 |
| JDK 17 | Deprecated | 引入--add-opens要求;java.rmi.server.hostname必须显式设置 | 启动时务必加--add-opens参数,否则IllegalAccessError |
| JDK 18-20 | Deprecated | RMI默认启用SSL,但jstatd不支持,需加-Dcom.sun.management.jstatd.ssl=false | 在启动命令中显式关闭SSL |
| JDK 21+ | Removed | jstatd命令彻底消失 | 迁移至jcmd+jstat本地模式,或改用JMX over HTTP(需应用配合) |
我的迁移建议:对于新项目,优先采用Micrometer + Prometheus方案;但对于存量系统,只要JDK版本≤20,jstatd依然是最省心的选择。不要因为“deprecated”就恐慌,它只是Oracle的代码清理信号,不代表功能失效。
4.5 性能与安全的平衡术:如何在开放监控的同时守住底线
jstatd的“零认证”特性是一把双刃剑。在生产环境,我坚持三条铁律:
铁律1:端口白名单
在Server A的防火墙中,只允许监控服务器(如Zabbix Proxy IP)访问1099端口,其他IP一律拒绝。命令示例:iptables -A INPUT -p tcp --dport 1099 -s 192.168.2.50 -j ACCEPT(192.168.2.50是Zabbix服务器IP)。铁律2:统计端口范围锁定
不让JVM使用随机端口,全部指定为固定高位端口(如11000-11099),然后在防火墙中只放行这个范围。这样即使RMI Registry被攻破,攻击者也无法连接到JVM的任意端口。铁律3:日志审计
启动jstatd时重定向输出:jstatd ... > /var/log/jstatd.log 2>&1,并用logrotate定期归档。虽然它不记录连接详情,但启动失败、安全策略拒绝等关键事件都会记入日志,是事后追溯的唯一依据。
最后分享一个真实案例:某次线上GC延迟突增,我们用
jstat -gc host:1099 100实时监控,发现YGCT(年轻代GC耗时)从平均5ms飙升到200ms。进一步用jstat -gccapacity发现NGC(当前年轻代容量)在1秒内从512MB暴涨到2GB,而NGCMX(最大容量)是4GB。这说明JVM正在疯狂扩容年轻代以应对对象创建压力。我们立刻登录Server A,用jstack <pid>抓取线程快照,发现一个业务线程在循环创建StringBuilder对象且未复用。这就是jstatd的价值——它不告诉你“为什么”,但它用最原始的数据,把你精准带到问题发生的现场。
5. 生产级增强实践:从单机监控到分布式指标采集的平滑演进
5.1 自动化部署脚本:三分钟完成百台服务器初始化
手动在每台服务器上配jstatd不现实。我编写了一个Ansible Playbook,可在3分钟内完成100台CentOS服务器的标准化部署:
--- - name: Deploy jstatd on all servers hosts: java_servers become: yes vars: jstatd_port: 1099 jvm_stat_port: 1100 monitor_ip: "192.168.2.50" # Zabbix服务器IP tasks: - name: Create jstatd directory file: path: /opt/jstatd state: directory mode: '0755' - name: Copy jstatd policy file copy: src: files/jstatd.all.policy dest: /opt/jstatd/jstatd.all.policy mode: '0644' - name: Create jstatd systemd service template: src: templates/jstatd.service.j2 dest: /etc/systemd/system/jstatd.service mode: '0644' notify: Restart jstatd - name: Enable and start jstatd service systemd: name: jstatd enabled: yes state: started handlers: - name: Restart jstatd systemd: name: jstatd state: restarted配套的jstatd.service.j2模板内容精简有力:
[Unit] Description=jstatd Service After=network.target [Service] Type=simple User=root WorkingDirectory=/opt/jstatd ExecStart=/usr/lib/jvm/java-17-openjdk-amd64/bin/jstatd \ -J-Djava.security.policy=/opt/jstatd/jstatd.all.policy \ -J-Djava.rmi.server.hostname={{ ansible_default_ipv4.address }} \ -p {{ jstatd_port }} \ -J-Dcom.sun.management.jstatd.port={{ jvm_stat_port }} Restart=always RestartSec=10 [Install] WantedBy=multi-user.target部署完成后,所有服务器的1099端口都已就绪,Zabbix只需配置一个jstatItem,用jstat -gc <ip>:1099 1000命令即可采集。
5.2 与Zabbix集成:自定义Key实现指标自动发现
Zabbix原生不支持jstatd,但可通过UserParameter轻松集成。在Zabbix Agent配置文件中添加:
# /etc/zabbix/zabbix_agentd.d/userparameter_jstat.conf UserParameter=jstat.gc.young[*],/usr/lib/jvm/java-17-openjdk-amd64/bin/jstat -gc "$1":1099 | tail -1 | awk '{print $$3}' UserParameter=jstat.gc.old[*],/usr/lib/jvm/java-17-openjdk-amd64/bin/jstat -gc "$1":1099 | tail -1 | awk '{print $$10}' UserParameter=jstat.gc.fgc[*],/usr/lib/jvm/java-17-openjdk-amd64/bin/jstat -gc "$1":1099 | tail -1 | awk '{print $$12}'然后在Zabbix Web界面创建Item,Key填jstat.gc.young[192.168.1.100],即可将S0U(S0区已用)作为监控项。更进一步,可用Zabbix的LLD(Low-Level Discovery)自动发现Server A上所有JVM进程:
# UserParameter=jstat.discovery[*],/usr/lib/jvm/java-17-openjdk-amd64/bin/jstat -list "$1":1099 | awk '{print "{\"{#PID}\":\""$1"\",\"{#VMID}\":\""$2"\"}"}' | sed 's/$/,/; 1s/^