How GraalVM Native Image’s New Native Memory Tracking Helps Diagnose Memory Usage
GraalVM Native Image now supports Native Memory Tracking (NMT), allowing developers to monitor off‑heap memory allocations, compare Java and native execution, integrate with JFR, and assess performance impact through detailed reports and experimental events.
1. Background
The term "native memory" (sometimes called off‑heap or unmanaged memory) refers to any memory not allocated on the Java heap. In a traditional JVM, native memory includes memory used by the JVM itself and memory allocated via
Unsafe#allocateMemory(long)or external libraries. In a Native Image, the SubstrateVM is written in Java, so most memory is allocated on the Java heap, but some components still use native memory outside the heap, which NMT aims to track.
2. Use Cases
NMT is useful because it reveals memory usage that heap dumps cannot show. For example, when an application's RSS is unexpectedly high, a heap dump may not explain the excess, indicating native memory as the culprit. NMT helps identify whether code changes or SubstrateVM runtime changes caused the extra native memory usage.
While most SubstrateVM memory is on the Java heap and visible in heap dumps, components such as garbage collection and JDK Flight Recorder still allocate native memory, making NMT valuable for comparing regular Java and Native Image performance.
3. Using NMT in Native Image
Enable NMT at build time with the flag
--enable-monitoring=nmt. By default NMT is not included in the image.
<code>native-image --enable-monitoring=nmt MyApp</code>At runtime, add
-XX:+PrintNMTStatisticsto dump the NMT report when the program exits.
<code>./myapp -XX:+PrintNMTStatistics</code>Sample report (truncated):
<code>Native memory tracking
Peak total used memory: 13632010 bytes
Total alive allocations at peak usage: 1414
Total used memory: 2125896 bytes
...
Unsafe currently used memory: 2099240 bytes
Unsafe currently alive allocations: 36
</code>The report shows instantaneous memory usage, counts, and peaks for each category, which differ from HotSpot categories.
4. NMT JFR Events
Native Image supports the OpenJDK JFR events
jdk.NativeMemoryUsageand
jdk.NativeMemoryUsageTotal. The former reports usage per category, while the latter reports total usage; neither includes count information.
Two additional custom events expose peak data:
jdk.NativeMemoryUsagePeakand
jdk.NativeMemoryUsageTotalPeak. In regular Java, users would use
jcmdto obtain peak usage, but
jcmdis not supported in Native Image.
4.1 Enabling JFR and NMT
Build with both JFR and NMT enabled:
<code>native-image --enable-monitoring=nmt,jfr MyApp</code>Start the application with Flight Recording:
<code>./myapp -XX:StartFlightRecording=filename=recording.jfr</code>4.2 Example
Using the
jfrtool to print the
jdk.NativeMemoryUsageevent:
<code>jfr print --events jdk.NativeMemoryUsage recording.jfr</code>Output example:
<code>jdk.NativeMemoryUsage {
startTime = 15:47:33.392 (2024-04-25)
type = "JFR"
reserved = 10.1 MB
committed = 10.1 MB
}
...</code>4.3 Showing Experimental JFR Events
These events are marked experimental and are hidden by default in VisualVM and JDK Mission Control. To display them, enable "Display experimental items" in VisualVM's Browser tab, or select "Include experimental events..." in JDK Mission Control preferences.
5. Implementation Details
NMT works by instrumenting calls to
malloc,
calloc,
realloc, and
mmap. The instrumentation adds a small header to each allocation, storing metadata that allows the NMT system to maintain an accurate view of native memory blocks. The recorded usage consists of a series of continuously updated counters, and reports provide a snapshot of native memory at the moment of generation.
6. Performance Impact
The overhead of NMT is minimal in most scenarios—approximately 16 bytes per allocation for the header. Benchmarks with a simple Quarkus native image show negligible impact on RSS, startup time, and response latency, while the image size grows by less than 3 KB.
<code>Measurement With NMT (NI) Without NMT (NI) With NMT (Java) Without NMT (Java)
RSS (KB) 53,030 53,390 151,460 148,952
Startup time (ms) 147 144 1,378 1,364
Avg response (us) 4,040 3,766 4,881 4,613
...</code>When JFR is also enabled, the performance impact becomes more noticeable, as shown in a second set of measurements.
<code>Measurement With NMT+JFR (NI) Without NMT+JFR (NI) With NMT+JFR (Java) Without NMT+JFR (Java)
RSS (KB) 72,366 52,388 191,504 149,756
Startup time (ms) 193 138 1,920 1,315
Avg response (us) 5,038 4,451 5,990 4,452
...</code>7. Limitations and Roadmap
Current NMT only tracks
malloc/
calloc/
reallocallocations; virtual‑memory tracking is not yet integrated. Calls made directly to
mallocfrom native libraries bypass NMT. Native Image also lacks a way to dump NMT reports at arbitrary points without JFR; a signal‑based dump is under investigation.
OpenJDK offers two NMT modes—summary (implemented) and detailed (not yet supported in Native Image). Counters are updated non‑atomically, which can cause temporary inconsistencies between total and peak measurements.
8. Conclusion
The newly added NMT feature in Native Image gives users visibility into native memory consumption, complementing JFR, JMX, heap dumps, and debugging information. Users are encouraged to try the feature and provide feedback via GraalVM’s GitHub issues.
Java Architecture Diary
Committed to sharing original, high‑quality technical articles; no fluff or promotional content.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.