Skip to content

framefork/jvm-native-memory-tracking

Repository files navigation

JVM Native Memory Tracking

A Java library that collects JVM Native Memory Tracking (NMT) data via jcmd and exposes it as metrics through Micrometer and/or OpenTelemetry.

NMT provides visibility into the JVM's native memory usage beyond the Java heap -- thread stacks, GC overhead, compiled code, class metadata, and more. This library makes that data available as standard metrics for your monitoring stack.

Prerequisites

The JVM must be started with Native Memory Tracking enabled:

-XX:NativeMemoryTracking=summary

The jcmd tool must be available on the system (it's part of the JDK). If you're using a custom JRE built with jlink, make sure to include the jdk.jcmd module.

Minimum supported Java is 17.

Installation

Pick the module matching your metrics API:

Metrics API Artifact
Micrometer org.framefork:nmt-micrometer
OpenTelemetry org.framefork:nmt-opentelemetry
Core only org.framefork:nmt-core

Find the latest version in this project's GitHub releases or on Maven Central.

Gradle

implementation("org.framefork:nmt-micrometer:${version}")
// or
implementation("org.framefork:nmt-opentelemetry:${version}")

Maven

<dependency>
    <groupId>org.framefork</groupId>
    <artifactId>nmt-micrometer</artifactId>
    <version>${version}</version>
</dependency>

Spring Boot Auto-Configuration

Both nmt-micrometer and nmt-opentelemetry include Spring Boot auto-configuration that activates automatically when all conditions are met:

  • The corresponding metrics API is on the classpath (MeterRegistry or OpenTelemetry bean)
  • NMT is enabled on the JVM (-XX:NativeMemoryTracking=summary)
  • The jcmd executable is available

No additional configuration is needed. Just add the dependency and the NMT flag.

Manual Setup (without Spring Boot)

Micrometer

var collector = new CachingNmtDataCollector(
    new JcmdNmtDataCollector(new DefaultJcmdRunner())
);
var binder = new NmtMeterBinder(collector);
binder.bindTo(meterRegistry);

OpenTelemetry

var collector = new CachingNmtDataCollector(
    new JcmdNmtDataCollector(new DefaultJcmdRunner())
);
var metrics = new NmtOpenTelemetryMetrics(openTelemetry, collector);

// On shutdown:
metrics.close();

Metrics

Both modules register the same two gauges for each NMT memory category:

Metric name Description Unit
jvm.memory.nmt.committed Committed memory (physical/swap memory in use) bytes
jvm.memory.nmt.reserved Reserved memory (virtual address space reserved) bytes

Each metric is tagged/attributed with category identifying the NMT memory type (e.g., java_heap, gc, thread, code, class, compiler, metaspace, etc.).

The categories are extracted dynamically from jcmd output, so new categories added in future JDK versions will be picked up automatically without library changes.

Supported Versions

Spring Boot Micrometer OpenTelemetry API Status
3.2.x 1.12.x 1.31.x Tested
3.3.x 1.13.x 1.38.x Tested
3.4.x 1.14.x 1.43.x Tested
3.5.x 1.15.x 1.49.x Tested
4.0.x 1.16.x 1.55.x Tested

Module Structure

Module Description
nmt-core jcmd runner, NMT output parser, data model, caching
nmt-micrometer Micrometer MeterBinder integration + Spring Boot auto-configuration
nmt-opentelemetry OpenTelemetry gauge registration + Spring Boot auto-configuration

How It Works

  1. The library runs jcmd <pid> VM.native_memory summary scale=b as a subprocess
  2. The text output is parsed with regex into structured data (category name, reserved bytes, committed bytes)
  3. Results are cached with a 5-second TTL to avoid spawning jcmd too frequently
  4. Metric callbacks read from the cache, which is refreshed on the next collection cycle

jcmd Output Format

The jcmd <pid> VM.native_memory summary scale=b command produces text output like:

Native Memory Tracking:

Total: reserved=1607492304, committed=196489936
       malloc: 30716624 #15376
       mmap:   reserved=1576775680, committed=165773312

-                 Java Heap (reserved=104857600, committed=104857600)
                            (mmap: reserved=104857600, committed=104857600)

-                     Class (reserved=1073862359, committed=317143)
                            (classes #1093)
                            ...

-                    Thread (reserved=23194136, committed=1092120)
                            ...

The parser dynamically extracts category names and values using regex, so it is forward-compatible with new JDK versions that may introduce additional categories.

NMT Categories

The following categories are typically reported by the JVM (varies by JDK version):

jcmd label Metric tag value Description
Total total Sum of all categories
Java Heap java_heap Heap memory
Class class Class metadata
Thread thread Thread stacks and structures
Code code Generated/compiled code
GC gc Garbage collector data structures
GCCardSet gccard_set G1 card set remembered set
Compiler compiler JIT compiler
Internal internal VM internal use
Symbol symbol String table, symbol table
Native Memory Tracking native_memory_tracking NMT's own overhead
Shared class space shared_class_space CDS archive
Arena Chunk arena_chunk Arena allocations
Tracing tracing Tracing infrastructure
Logging logging Logging infrastructure
Arguments arguments VM arguments
Module module Module system
Safepoint safepoint Safepoint infrastructure
Synchronization synchronization Synchronization primitives
Metaspace metaspace Metaspace memory
String Deduplication string_deduplication G1 string deduplication
Object Monitors object_monitors Object monitors

Category labels are converted to lowercase snake_case for metric tag/attribute values. New categories added in future JDK versions are picked up automatically.

Reference: OpenJDK memory types in allocation.hpp

Caching

Since multiple metric callbacks fire independently during a single collection cycle (one per category per metric = ~46 calls), jcmd should only be called once per interval. The CachingNmtDataCollector stores the parsed result with a 5-second TTL. When any metric callback fires, it reads from the cache and only runs jcmd if the cache has expired.

Error Handling

  • Container shutdown (exit code 143): When the container is shutting down, jcmd may return exit code 143 (SIGTERM). This is caught and logged as a warning, and empty values are returned.
  • jcmd failures: Other errors are logged and result in empty metrics (zeros) rather than exceptions propagating to the metrics system.
  • NMT not enabled: If NMT is not enabled, jcmd returns "Native memory tracking is not enabled". The parser handles this gracefully and returns an empty summary.

Docker and Custom JRE Images

If you're building a custom JRE image with jlink, make sure to include the jdk.jcmd module:

jlink --add-modules ...,jdk.jcmd --output /opt/jre

Without this module, jcmd will not be available and the library will not activate (the Spring Boot auto-configuration checks for jcmd availability).

License

Apache License 2.0

About

A Java library that collects JVM Native Memory Tracking (NMT) data via jcmd and exposes it as metrics through Micrometer and/or OpenTelemetry.

Resources

License

Stars

Watchers

Forks

Contributors

Languages