Backend Development 12 min read

Root Cause Analysis of High Native Memory Usage in a Spring Boot Application

After migrating a project to the MDP framework based on Spring Boot, the system repeatedly reported excessive swap usage; the investigation revealed that native memory allocated by the Spring Boot classloader’s Reflections scanning and InflaterInputStream caused 700 MB–800 MB of off‑heap memory to remain unreleased, which was eventually resolved by limiting the scan path and updating Spring Boot.

Architect's Tech Stack
Architect's Tech Stack
Architect's Tech Stack
Root Cause Analysis of High Native Memory Usage in a Spring Boot Application

The author was asked to investigate a sudden increase in swap usage after moving a project to the MDP framework (Spring Boot based). Although the JVM was configured with a 4 GB heap, the process consumed up to 7 GB of physical memory.

Initial diagnostics used Java‑level tools (jcmd with -XX:NativeMemoryTracking=detail ) to display native memory distribution, revealing that the committed memory reported by jcmd was smaller than the actual physical usage, suggesting native code allocations.

System‑level tools were then employed: pmap showed many 64 MB address ranges not accounted for by jcmd; gperftools was used to monitor allocations, indicating that memory peaked at ~3 GB before dropping to 700‑800 MB; strace traced brk , mmap , and munmap system calls but did not reveal suspicious allocations.

Further analysis with gdb and jstack identified the thread responsible for the large native allocations. The culprit was the MCC (Meituan Configuration Center) component that used the Reflections library to scan all JAR packages. During scanning, Spring Boot’s ZipInflaterInputStream (which wraps Inflater ) allocated off‑heap memory via native malloc but relied on the Java finalizer to release it.

Because the finalizer runs only after GC, the off‑heap memory remained allocated for a long time. Moreover, the underlying glibc memory allocator kept the freed pages in its per‑thread memory pool (≈64 MB per thread), so the OS did not reclaim the memory, giving the impression of a leak.

To verify the hypothesis, the author replaced Spring Boot’s inflater with the JDK’s implementation and observed that the memory issue disappeared. A custom malloc implementation (shown below) was also built and preloaded via LD_PRELOAD to compare allocation behavior.

#include<sys/mman.h>
#include<stdlib.h>
#include<string.h>
#include<stdio.h>

void* malloc(size_t size) {
    long* ptr = mmap(0, size + sizeof(long), PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0);
    if (ptr == MAP_FAILED) return NULL;
    *ptr = size;
    return (void*)(&ptr[1]);
}

void* calloc(size_t n, size_t size) {
    void* ptr = malloc(n*size);
    if (!ptr) return NULL;
    memset(ptr, 0, n*size);
    return ptr;
}

void* realloc(void* ptr, size_t size) {
    if (size == 0) { free(ptr); return NULL; }
    if (!ptr) return malloc(size);
    long* plen = (long*)ptr; plen--;
    long len = *plen;
    if (size <= len) return ptr;
    void* rptr = malloc(size);
    if (!rptr) { free(ptr); return NULL; }
    rptr = memcpy(rptr, ptr, len);
    free(ptr);
    return rptr;
}

void free(void* ptr) {
    if (!ptr) return;
    long* plen = (long*)ptr; plen--;
    long len = *plen;
    munmap((void*)plen, len + sizeof(long));
}

Tests showed that the custom allocator kept the off‑heap memory between 700 MB and 800 MB, while the OS reported a much larger resident set due to the way mmap reserves virtual pages and glibc’s memory pools.

The final solution was to configure MCC to scan only specific JARs instead of the entire classpath, which eliminated the excessive native allocations. Updating to Spring Boot 2.0.5 (which now releases the inflater’s native memory explicitly) also resolves the issue.

In summary, the apparent memory leak was caused by off‑heap allocations performed during package scanning, delayed GC finalization, and the glibc memory‑pool behavior that retained pages after free. Proper configuration and upgrading the framework prevent the problem.

JavaMemory LeakSpring BootNative MemoryPerformance Debugginggperftools
Architect's Tech Stack
Written by

Architect's Tech Stack

Java backend, microservices, distributed systems, containerized programming, and more.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.