JVM Native Memory Tracking is a surprisingly powerful tool for understanding where your Java application is secretly leaking memory, often in places the garbage collector can’t even see.
Let’s see it in action. Imagine you’ve got a Java application and you suspect it’s using more native memory than it should. First, you need to enable it. You do this by adding a JVM argument when you start your application:
java -XX:NativeMemoryTracking=summary -jar myapp.jar
This tells the JVM to start tracking native memory allocations. The summary option is a good starting point, giving you an overview of different memory areas.
Now, to actually see the data, you’ll use a command-line tool. While the application is running, open another terminal and execute:
jcmd <pid> VM.native_memory summary
Replace <pid> with the actual process ID of your Java application. This command will dump the native memory summary to your console. You’ll see output like this, broken down by memory areas:
Native Memory Allocation Summary:
---------------------------------
Java Heap: 1048576 KB
[...]
Internal: 12345 KB
[...]
Code Cache: 67890 KB
[...]
Thread: 98765 KB
[...]
GC: 54321 KB
[...]
Compiler: 11223 KB
[...]
Other: 33445 KB
---------------------------------
Total Native Memory: 376543 KB
The real magic happens when you switch from summary to detail. This gives you a much more granular look, showing you the exact call sites responsible for allocations. To do this, you first need to dump the detailed information:
jcmd <pid> VM.native_memory detail.dump to_file=native_memory_detail.txt
Then, you can analyze the native_memory_detail.txt file. It will be extensive, but you’re looking for patterns. For instance, you might see a large number of allocations coming from a specific C library function or a particular Java class interacting with native code.
The primary problem Native Memory Tracking (NMT) solves is understanding memory usage outside the Java heap. The JVM itself, along with any native libraries your Java code calls (like those in java.nio, JNI code, or even internal JVM structures like thread stacks and code caches), all consume memory managed by the operating system, not the Java heap. If your application is running out of memory, but the heap usage reported by jstat or jconsole looks fine, the culprit is almost certainly native memory.
When you enable NativeMemoryTracking=detail, the JVM instruments its own native memory allocation functions. Every time a piece of native memory is requested (e.g., via malloc or mmap), the JVM intercepts this request. It records the size of the allocation, the type of memory being allocated (e.g., Java Heap, Thread, Code Cache), and crucially, the call stack at the point of allocation. This call stack allows you to trace the allocation back to the specific Java code or native library function that initiated it.
The detail output is structured to show you the total native memory usage, broken down by different categories. Within each category, it further subdivides by the "tracking tags" which represent different internal components of the JVM or libraries. The most useful part is the ability to see the call stack for specific allocations. You can often identify a leaky native library or a Java method that is repeatedly allocating large chunks of native memory without releasing them.
One subtle but critical aspect of NMT is how it handles memory allocated by JNI. When your Java code calls into native code via JNI, any memory allocated by that native code using standard C functions like malloc will be tracked if the JNI code correctly uses the JVM’s provided allocation functions or if the JVM’s internal mechanisms capture these allocations. However, if the native code uses its own memory management or third-party libraries that don’t integrate with the JVM’s tracking, NMT might not see those specific allocations. This is why analyzing the call stacks is so important; it reveals if the allocation originates from Java-land or from deep within a native library.
If you see unexpectedly high usage in the Internal or Other categories in the summary, switching to detail and then using jcmd <pid> VM.native_memory detail (without the dump option) to see the call stacks directly can pinpoint the source.
The next challenge you’ll likely face is when NativeMemoryTracking=detail itself starts consuming too much memory, especially on very large or busy applications, leading to performance degradation.