Diagnosing Excessive Swap Usage in a SpringBoot Project: Memory Profiling and Native Memory Analysis
The article details a step‑by‑step investigation of a SpringBoot application that repeatedly triggered high swap usage, describing how JVM parameters, native memory tracking, gperftools, strace, and custom memory allocators were used to pinpoint and resolve off‑heap memory leaks caused by the Inflater implementation and glibc memory pools.
Background
The author migrated a project to the MDP framework (based on SpringBoot) and observed frequent "Swap area usage too high" errors. Although the JVM was configured with 4 GB heap, the actual physical memory consumption reached 7 GB.
JVM parameters used were:
-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=128m -XX:InitialCodeCacheSize=128m -Xss512k -Xmx4g -Xms4g -XX:+UseG1GC -XX:G1HeapRegionSize=4MInvestigation Process
Java‑level diagnostics
Enabled -XX:NativeMemoryTracking=detail and used jcmd <pid> VM.native_memory detail to view memory distribution, revealing that committed memory was lower than the physical usage because it excluded native code allocations.
Used pmap to see large 64 MB address spaces not reported by jcmd , indicating native allocations.
System‑level diagnostics
Employed gperftools ( https://github.com/gperftools/gperftools ) to monitor memory; observed malloc usage peaking at 3 GB then stabilising around 700‑800 MB.
Ran strace -f -e brk,mmap,munmap -p <pid> but did not see suspicious allocations.
Dumped memory with GDB ( gdb -pid <pid> ) and examined the dump using strings , finding JAR‑related data, suggesting the issue occurs during startup.
Repeated strace during application start‑up and identified many 64 MB mmap requests.
Mapped those requests to pmap output, confirming large anonymous mappings.
Thread analysis
Used jstack to locate the thread responsible for the mmap calls.
Discovered that MCC (Meituan Configuration Center) uses Reflections to scan all JARs, which triggers SpringBoot's ZipInflaterInputStream to allocate off‑heap memory via Inflater .
The Inflater's finalize method releases the native memory, but reliance on GC means the memory is not returned to the OS; glibc keeps it in per‑thread memory pools (64 MB each).
Custom allocator experiment
Created a simple malloc implementation using mmap and preloaded it via LD_PRELOAD . Instrumentation showed the application consistently used 700‑800 MB off‑heap memory, while the OS reported much higher resident set size due to lazy allocation and memory‑pool behaviour.
Tests with different allocators confirmed that glibc's arena allocation and tcmalloc's memory‑pool strategy cause apparent memory leaks.
Summary
The root cause was SpringBoot's default handling of JAR scanning, which allocated large off‑heap buffers that were only freed by GC finalization; glibc's memory pools prevented the OS from reclaiming the pages, leading to perceived memory leaks.
Fixes applied:
Configure MCC to scan only specific JARs, eliminating unnecessary allocations.
Upgrade SpringBoot to a version where ZipInflaterInputStream explicitly releases native memory.
Consider using alternative memory allocators or disabling per‑thread arenas when appropriate.
These changes resolved the swap‑usage issue and reduced the process's physical memory footprint.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.