在监控图上看到 JVM 内存池用量和线程数,很容易把数字当作“线程刚创建时立即上报”或“分配一个 4 MiB 数组就增加 4 MiB”。指标并不这样工作:JMX 平台 MXBean 持有可查询状态,OpenTelemetry 异步 instrument 在 reader 采集时查询它;不同时间窗的状态与标签必须分开看。

版本边界是完整 JDK 21.0.12、Java SDK 1.31.0(固定源码 SHA c25c0a0ee0da01ab2f74ba83052d1c249ed57020)、instrumentation 的 runtime-telemetry-java8 library 1.31.0-alpha(固定源码 SHA 97d87f3f3b2bc61aa396d71a98387a1e857618af)。新增 JAR 的本机 SHA-256 见 writing-plans/opentelemetry-java/VERSIONS.md。这个 library 从当前 JVM 的平台 MXBean 取值,实验没有启动独立远程 JMX collector,也没有挂 Agent。

来源与采集时刻不是同一件事

固定 instrumentation SHA 的 MemoryPools.java L53–69 从 ManagementFactory.getMemoryPoolMXBeans() 注册 process.runtime.jvm.memory.usage;MemoryPools.java L106–134 在回调中对每个 pool 读取 usage,带 pool 和 type 标签;null usage 和 -1 值被跳过。 Threads.java L41–64 读 ThreadMXBean 的 daemon 数与总数,再记两个 {thread} 单位的非单调点(daemon=true/false)。上游 ThreadsTest.java L30–61 用模拟 MXBean 验证 2 daemon、5 non-daemon;它是静态读过的上游测试,不是本文本机运行结果。

固定 SDK SHA 的 InMemoryMetricReader.java L70–90 的 create() 是默认累计视图;PeriodicMetricReader.java L45–56 的周期 exporter reader 是另一个组件,本篇没有创建它。把“数据来自 MXBean”与“何时读 MXBean”拆开,就能避免把指标的 epoch 错当成分配发生时刻。

三个同步窗口

自写的 examples/opentelemetry-java/sdk-labs/src/test/java/org/example/otel/E04Test.java 注册 MemoryPools 与 Threads observers,仅用测试 provider 的内存 reader。一个 non-daemon worker 创建并触碰 4 MiB 数组,在 CountDownLatch 屏障等待;主线程 await 有 5 秒截止,第三窗口前释放 worker 并在 5 秒内 join。每次 collect 立即执行回调,无 sleep。

窗口 worker 状态 non-daemon 点 本机内存池观察
before 尚未启动 1 memory.usage 有点
held 仍在等待屏障 2 本次 8 个池点,至少一个 type=heap 且带 pool
after 已释放并 join 1 再次采集,不断言固定数值
1
2
3
4
5
6
7
var before = reader.collectAllMetrics();
worker.start();
assertTrue(ready.await(5, TimeUnit.SECONDS));
var held = reader.collectAllMetrics();
release.countDown();
worker.join(5000);
var after = reader.collectAllMetrics();

三个窗口的线程指标每次各有 daemon=true/false 两点,scope 为 io.opentelemetry.runtime-telemetry-java8,unit {thread},temporality CUMULATIVE;held 的内存池 usage 单位 By,点时间位于该次 collect 的开始与结束时刻之间。代码断言的是线程数变化、池点来源及标签、时间窗顺序,没有断言 heap 恰好增长 4 MiB。evidence/E04/RUN.md 给出完整命令、退出码与原始摘要。

易误读的边界

  • 一次 reader.collectAllMetrics() 是采集快照,不等于内存/线程状态改变时立即推送;选用周期 reader 的时序还需要另行验证。
  • JVM 的 GC、内存池数量和 getUsage() 值随 JDK、收集器与其他线程变化。本机看到 8 个池点不意味着每个进程一定 8 个,也不能把一次 GC 波动当性能基线。
  • process.runtime.jvm.memory.usage 的 pool、type 标签来自 JVM 的 MemoryPoolMXBean;它不是 HTTP 请求或业务租户维度。没有验证外部 JMX、Agent、Collector、Metrics OTLP 或后端存储。

两道练习

  1. 修改 E04Test,移除 Threads.registerObservers(sdk) 后重新采集,断言线程指标消失而内存池指标仍存在;这能区分“注册观察回调”与“读了一遍 JVM 线程数”。
  2. 构造反例:在 held 窗口后释放数组引用并允许 GC,但不要把某一次内存池值差写成稳定分配成本。记录 GC 收集器与池名称、采集窗口,并说明为何 getCollectionUsage()==null 时无点。

导航与参考资料