How to Diagnose and Fix Native Memory Leaks in Java Applications
This guide explains why Java heap dumps miss native memory, introduces Native Memory Tracking (NMT) and tools like pmap and gdb, and provides practical code and JVM settings to detect, analyze, and prevent native memory leaks in Java programs.
When a Java application suffers from native memory leaks, a regular heap dump (e.g., via
jmap) will not capture the native allocations, making analysis difficult; for example, a process may show 20 GB RSS while the heap dump is only 2 GB.
This article summarizes the steps to investigate native memory leaks.
NMT (Native Memory Tracking)
Native Memory Tracking is a JVM feature that monitors native memory allocation and usage, helping to identify leaks and excessive consumption.
1.1 Enable NMT
Add the following JVM option:
-XX:NativeMemoryTracking=detailWays to add it:
1.1.1 Command line
java -XX:NativeMemoryTracking=detail -jar YourApp.jar1.1.2 Maven/Gradle
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-XX:NativeMemoryTracking=detail</argLine>
</configuration>
</plugin>
</plugins>
</build>1.2 Define a baseline
Run the following command to set a baseline for later comparison:
jcmd <PID> VM.native_memory baseline scale=MB1.3 Capture memory details
Take a snapshot of native memory and write it to a file:
jcmd <PID> VM.native_memory detail scale=MB > native_memory_detailThe
native_memory_detailfile contains entries such as reserved (memory reserved by the OS) and committed (currently used memory).
1.4 Capture memory diff
Periodically capture the diff to see which components increase committed memory:
jcmd <PID> VM.native_memory detail.diff scale=MB > native_memory_diffThe diff output shows lines like
Total: reserved=3111MB +521MB, committed=1401MB +842MB, where the
+521MBindicates the growth compared with the baseline.
After identifying the culprit, use
jcmd <PID> helpto explore more options, e.g., classloader statistics:
jcmd <PID> VM.classloader_stats
jcmd <PID> VM.class_hierarchypmap + gdb analysis
Use
pmap -x [PID]to view memory usage of the Java process. Sample output columns:
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 4 4 0 r-xp /usr/bin/pmap
...
00007f8b00000000 65512 40148 40148 rwx-- [ anon ]Columns meaning:
Address – start address of the memory region
Kbytes – size of the region in KB
RSS – resident set size (actual physical memory used)
Dirty – number of dirty pages
Mode – permissions (r=read, w=write, x=exec, s=shared, p=private)
Mapping – type or filename of the mapping
Dump a specific region with gdb:
gdb -p <pid>
(gdb) dump memory mem.bin 0x00007f8b00000000 0x00007f8b00000000+65512Extract readable strings:
cat mem.bin | stringsHow to avoid native memory leaks
3.1 Properly manage Direct Memory
public class DirectMemoryManager {
private static final long MAX_DIRECT_MEMORY = 1024 * 1024 * 1024; // 1GB
private static final AtomicLong usedDirectMemory = new AtomicLong(0);
public static ByteBuffer allocateDirect(int size) {
long current = usedDirectMemory.get();
if (current + size > MAX_DIRECT_MEMORY) {
throw new OutOfMemoryError("Direct memory limit exceeded");
}
ByteBuffer buffer = ByteBuffer.allocateDirect(size);
usedDirectMemory.addAndGet(size);
return buffer;
}
public static void releaseDirect(ByteBuffer buffer) {
if (buffer.isDirect()) {
usedDirectMemory.addAndGet(-buffer.capacity());
buffer.clear();
}
}
}3.2 Use a resource pool
public class DirectBufferPool {
private final Queue<ByteBuffer> pool = new ConcurrentLinkedQueue<>();
private final int bufferSize;
private final int maxPoolSize;
public DirectBufferPool(int bufferSize, int maxPoolSize) {
this.bufferSize = bufferSize;
this.maxPoolSize = maxPoolSize;
}
public ByteBuffer acquire() {
ByteBuffer buffer = pool.poll();
if (buffer == null) {
buffer = ByteBuffer.allocateDirect(bufferSize);
}
return buffer;
}
public void release(ByteBuffer buffer) {
if (buffer != null && buffer.isDirect() && pool.size() < maxPoolSize) {
buffer.clear();
pool.offer(buffer);
}
}
}3.3 Enable NMT monitoring (with performance cost)
Continuously run NMT to spot memory growth.
3.4 Periodic cleanup
public class NativeMemoryCleaner {
private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
public static void startPeriodicCleanup() {
scheduler.scheduleAtFixedRate(() -> {
System.gc(); // trigger GC to free Direct Memory
try {
Field cleanerField = Class.forName("java.nio.DirectByteBuffer").getDeclaredField("cleaner");
cleanerField.setAccessible(true);
// custom cleanup logic can be added here
} catch (Exception e) {
// handle exception
}
}, 0, 5, TimeUnit.MINUTES);
}
}3.5 Tune JVM parameters
# Limit Direct Memory size
-XX:MaxDirectMemorySize=1g
# Enable detailed GC logs
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
# Enable NMT
-XX:NativeMemoryTracking=detail
# Set reasonable thread stack size
-Xss256k3.6 Use monitoring tools
Leverage JMX or other observability platforms to track native memory usage in real time.
Big Data Technology Tribe
Focused on computer science and cutting‑edge tech, we distill complex knowledge into clear, actionable insights. We track tech evolution, share industry trends and deep analysis, helping you keep learning, boost your technical edge, and ride the digital wave forward.
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.