Java - Java Native Memory Tracking (NMT)
Java applications use memory for much more than storing objects in the heap. While most developers focus on heap memory and garbage collection, the Java Virtual Machine (JVM) also allocates memory outside the heap. This memory is known as native memory, and it is used for various internal JVM operations. If native memory usage grows unexpectedly, an application may run out of system memory even when the Java heap appears healthy.
Java Native Memory Tracking (NMT) is a diagnostic feature provided by the JVM that helps developers monitor and analyze how native memory is being allocated and used. It provides detailed reports that categorize memory consumption, making it easier to identify memory leaks, excessive resource usage, and inefficient JVM configurations.
What is Native Memory?
Native memory is the memory allocated directly from the operating system rather than from the Java heap. Unlike heap memory, native memory is managed by the JVM and the operating system instead of the Java Garbage Collector.
Native memory is used for several purposes, including:
-
JVM internal data structures
-
Thread stacks
-
Class metadata
-
Just-In-Time (JIT) compiled code
-
Native libraries
-
Direct ByteBuffers
-
Garbage Collector structures
-
JNI (Java Native Interface) operations
Since native memory exists outside the Java heap, increasing the heap size does not necessarily solve native memory issues.
Why Native Memory Tracking is Important
Applications sometimes experience memory-related failures even though heap memory usage appears normal. In such situations, the actual problem may be excessive native memory allocation.
Native Memory Tracking helps developers:
-
Detect native memory leaks
-
Understand JVM memory consumption
-
Monitor memory allocation categories
-
Optimize JVM startup parameters
-
Troubleshoot OutOfMemoryError caused by native memory exhaustion
-
Improve application performance
How Native Memory Tracking Works
When enabled, NMT records every native memory allocation made by the JVM. It organizes allocations into logical categories and generates reports that summarize memory usage.
The reports show:
-
Total reserved memory
-
Total committed memory
-
Memory allocated by each JVM component
-
Growth of memory usage over time
Developers can compare reports captured at different times to identify memory increases.
NMT Modes
Native Memory Tracking can operate in different modes.
Off
In this mode, Native Memory Tracking is disabled.
-XX:NativeMemoryTracking=off
This is the default mode for many JVM configurations.
Summary Mode
Summary mode provides a high-level overview of native memory usage.
-XX:NativeMemoryTracking=summary
This mode has relatively low overhead and is suitable for production environments when basic monitoring is required.
Detail Mode
Detail mode records every allocation and provides detailed information.
-XX:NativeMemoryTracking=detail
This mode consumes more memory and CPU resources because every allocation is tracked.
Enabling Native Memory Tracking
To enable Native Memory Tracking, start the application with the following JVM option:
java -XX:NativeMemoryTracking=summary MyApplication
or
java -XX:NativeMemoryTracking=detail MyApplication
After starting the application, NMT can be accessed using the jcmd utility.
Viewing Native Memory Usage
First, identify the Java process.
jps
Example:
12345 MyApplication
Now request the memory report.
jcmd 12345 VM.native_memory summary
For detailed information:
jcmd 12345 VM.native_memory detail
The JVM generates a complete report showing native memory consumption.
Understanding Reserved and Committed Memory
One of the most important concepts in NMT is the difference between reserved memory and committed memory.
Reserved Memory
Reserved memory is the address space that the JVM reserves for future use.
It is not necessarily using all of this memory immediately.
Example:
Reserved = 4 GB
This means the JVM has reserved 4 GB of virtual address space.
Committed Memory
Committed memory is the portion of reserved memory that is currently allocated and backed by physical memory.
Example:
Committed = 1.5 GB
Only 1.5 GB is actively being used.
Understanding this distinction helps developers avoid confusing reserved memory with actual memory consumption.
Common Memory Categories
NMT organizes memory into several categories.
Java Heap
Represents memory used for Java objects.
Example:
Java Heap
Reserved: 2048 MB
Committed: 1024 MB
Class Memory
Stores metadata about loaded classes.
Includes:
-
Class definitions
-
Constant pools
-
Method information
Large enterprise applications may load thousands of classes, increasing this memory usage.
Thread Memory
Each Java thread requires its own native stack.
Example:
200 Threads
1 MB Stack Each
Total Thread Memory = 200 MB
Applications creating excessive threads consume large amounts of native memory.
Code Cache
Stores machine code generated by the Just-In-Time compiler.
When Java methods are compiled, executable native code is placed here.
Heavy applications often have large code caches.
GC Memory
Garbage Collectors require native memory for their internal data structures.
Examples include:
-
Marking tables
-
Region information
-
Reference queues
Different garbage collectors use different amounts of native memory.
Compiler Memory
Memory used by the JIT compiler while compiling Java bytecode into machine code.
Internal Memory
Memory used internally by JVM components.
Examples include:
-
Symbol tables
-
Runtime structures
-
JVM bookkeeping
Symbol Memory
Stores strings representing:
-
Method names
-
Field names
-
Class names
Applications loading many classes consume more symbol memory.
Native Libraries
Native libraries loaded through JNI also consume native memory.
Examples include:
-
Database drivers
-
Graphics libraries
-
Compression libraries
Baseline and Memory Comparison
NMT allows developers to create a baseline.
jcmd 12345 VM.native_memory baseline
Later, compare the current memory usage.
jcmd 12345 VM.native_memory summary.diff
This feature helps identify which memory categories have grown over time.
Example:
Thread Memory
Before:
150 MB
After:
310 MB
This indicates that additional threads have been created.
Detecting Memory Leaks
Suppose an application creates Direct ByteBuffers continuously but never releases them.
Over time:
Heap Memory
Stable
Native Memory
Continuously Increasing
Without NMT, developers may mistakenly believe that garbage collection is malfunctioning.
With NMT, the growing native memory category becomes visible, helping pinpoint the actual issue.
Native Memory and Direct ByteBuffers
Direct ByteBuffers allocate memory outside the Java heap.
Example:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
This creates a 1 MB native memory allocation.
Creating thousands of Direct ByteBuffers without proper cleanup can exhaust native memory while heap usage remains low.
NMT helps monitor such allocations.
Example NMT Report
A simplified report might look like:
Native Memory Tracking:
Total:
Reserved = 4096 MB
Committed = 1850 MB
Java Heap
Reserved = 2048 MB
Committed = 1024 MB
Class
Reserved = 200 MB
Committed = 150 MB
Thread
Reserved = 350 MB
Committed = 350 MB
Code
Reserved = 250 MB
Committed = 180 MB
GC
Reserved = 400 MB
Committed = 120 MB
Internal
Reserved = 120 MB
Committed = 26 MB
From this report, developers can identify which components consume the most native memory and determine whether the allocation is expected.
Best Practices for Using NMT
-
Enable Summary mode for routine diagnostics due to its lower performance overhead.
-
Use Detail mode only when investigating complex native memory issues.
-
Regularly compare baseline and current reports to detect gradual memory growth.
-
Monitor thread counts, as each thread consumes native stack memory.
-
Avoid creating unnecessary Direct ByteBuffers or native resources.
-
Keep the JVM updated, as newer releases often improve native memory management.
-
Use NMT alongside other tools such as Java Flight Recorder (JFR), VisualVM, and JConsole for comprehensive performance analysis.
Advantages of Native Memory Tracking
-
Helps detect native memory leaks.
-
Provides a detailed breakdown of JVM memory usage.
-
Distinguishes between heap and native memory consumption.
-
Supports baseline comparisons for identifying memory growth.
-
Assists in diagnosing OutOfMemoryError caused by native memory exhaustion.
-
Improves JVM tuning and performance optimization.
Limitations of Native Memory Tracking
-
Introduces some runtime overhead, particularly in Detail mode.
-
Tracks only JVM-managed native memory; memory allocated entirely outside JVM control may not be fully visible.
-
Requires JVM startup options and cannot be enabled after the application has started.
-
Reports can be complex to interpret for beginners.
Conclusion
Java Native Memory Tracking is a powerful JVM diagnostic feature that provides deep visibility into memory allocated outside the Java heap. It enables developers to understand how different JVM components consume native memory, identify memory leaks, analyze allocation patterns, and troubleshoot issues that traditional heap analysis cannot reveal. By using NMT effectively alongside other JVM monitoring tools, developers can optimize application performance, improve stability, and resolve complex memory-related problems in production environments.