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.