Memory Usage Optimization In Spring Boot
1. Introduction
A basic Spring Boot application with an embedded Tomcat server would consume 100 MB of memory when launched. JVM memory consists of Heap
, Metaspace
, Overhead
, and Native
memories.
Heap
is used for object storage and garbage collection. The garbage collector automatically manages memory by reclaiming space from unused objects. The heap is the largest chunk of memory and might allocate between50%
and80%
of the total available system memory for the JVM.Metaspace
is used to store metadata about classes. It replacesPermGen
since Java 8+Overhead
is used for thread management, etc.Native
is used to interact with the operating system.
In this example, I will explain why this happens and examine ways to reduce memory usage without impacting application functionality.
2. Setup
2.1 Spring Boot 3.x Web Application
In this step, I will create a Gradle Java 21 Spring Boot 3 project via Spring IO Initializr with the following common libraries:
- Spring Web
- Lombok Developer Tools
- Validation I/O
- Spring Security
- Spring Boot Actuator Ops
- Spring Session for Spring Data Redis Web
- Config Client Spring Cloud Config
Unzip the generated zip file and output the generated build.gradle
.
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '3.4.10' id 'io.spring.dependency-management' version '1.1.7' } group = 'org.jcg.zheng.demo21G' version = '0.0.1-SNAPSHOT' description = 'Demo memory usage project for Spring Boot' java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } ext { set('springCloudVersion', "2024.0.2") } dependencies { implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.cloud:spring-cloud-config-server' implementation 'org.springframework.session:spring-session-data-redis' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } } tasks.named('test') { useJUnitPlatform() }
2.2 Install VisualVM
In this step, I will install the VisualVM tool as it isn’t included in JDK21.
- Go to the official site: https://visualvm.github.io/ and download the latest release (ZIP for Windows, tar.gz for Linux/macOS)
- Extract and Install
2.3 Start the Spring Boot Application
In this step, I will start the Spring Boot application created at step 2.1 without any modification via the “gradlew bootRun
” command and capture the server log.
gradlew bootRun
C:\MaryZheng\workspace\sbmemory21g>gradlew bootRun Starting a Gradle Daemon, 2 stopped Daemons could not be reused, use --status for details > Task :bootRun . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.4.10) 2025-09-21T17:42:07.553-05:00 INFO 19768 --- [sbmemory21g] [ main] o.j.z.d.s.Sbmemory21gApplication : Starting Sbmemory21gApplication using Java 21.0.7 with PID 19768 (C:\MaryZheng\workspace\sbmemory21g\build\classes\java\main started by zzhen in C:\MaryZheng\workspace\sbmemory21g) 2025-09-21T17:42:07.555-05:00 INFO 19768 --- [sbmemory21g] [ main] o.j.z.d.s.Sbmemory21gApplication : No active profile set, falling back to 1 default profile: "default" 2025-09-21T17:42:08.185-05:00 INFO 19768 --- [sbmemory21g] [ main] o.s.cloud.context.scope.GenericScope : BeanFactory id=2723bc09-5e66-38ec-96ea-3c4721fe1b70 2025-09-21T17:42:08.382-05:00 INFO 19768 --- [sbmemory21g] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) 2025-09-21T17:42:08.394-05:00 INFO 19768 --- [sbmemory21g] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2025-09-21T17:42:08.395-05:00 INFO 19768 --- [sbmemory21g] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.46] 2025-09-21T17:42:08.431-05:00 INFO 19768 --- [sbmemory21g] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2025-09-21T17:42:08.432-05:00 INFO 19768 --- [sbmemory21g] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 847 ms 2025-09-21T17:42:08.794-05:00 WARN 19768 --- [sbmemory21g] [ main] .s.s.UserDetailsServiceAutoConfiguration : Using generated security password: b102c837-4262-4e5f-b418-f3a6798998f6 This generated password is for development use only. Your security configuration must be updated before running your application in production. 2025-09-21T17:42:08.805-05:00 INFO 19768 --- [sbmemory21g] [ main] r$InitializeUserDetailsManagerConfigurer : Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager 2025-09-21T17:42:08.875-05:00 INFO 19768 --- [sbmemory21g] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 1 endpoint beneath base path '/actuator' 2025-09-21T17:42:08.953-05:00 INFO 19768 --- [sbmemory21g] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/' 2025-09-21T17:42:08.963-05:00 INFO 19768 --- [sbmemory21g] [ main] o.j.z.d.s.Sbmemory21gApplication : Started Sbmemory21gApplication in 1.703 seconds (process running for 2.002) 80% EXECUTING [1h 58m 25s]
The server log shows the Spring Boot 3.4.10 web application is started.
3. Monitor Via VisualVM
Launch VisualVM and monitor the basic Spring Boot application started at step 2.3.
- The
pid
is19768
. - The current committed heap size is
104857600B
. It is between-Xms
and-Xmx
. - The actual used heap size is ranged from
25MB
to90MB
. - The max heap size is
4G
and can be set by the-Xmx
- The
Metaspace
size average is42MB
. - The total loaded classes count is
10505
.
So, this Spring boot application consumes about 67MB-132MB memory. Here are the main reasons why it takes that much memory:
- Spring Boot Framework overhead – spring context initialized thousands of classes, dependency injection graph resolved, and stored reflection metadata.
- Embedded Servlet Container – Tomcat threads, request/response buffers, servlet mappings, and with default 200 threads pool.
- Logging – Logging initialization and configuration.
- Annotation scanning for
@Configuration
,@Component
,@Entity
, and@Service
can add lots of objects. - Spring Boot Actuator includes health checks and metrics.
- Jackson/JSON libraries cache type metadata.
4. Monitor via Commands
4.1 The Jcmd command
You can get the memory usage via the jcmd
command.
jcmd 19768 GC.heap_info
C:\Program Files\Java\jdk-21\bin>jcmd 19768 GC.heap_info 19768: garbage-first heap total 102400K, used 62525K [0x0000000704e00000, 0x0000000800000000) region size 2048K, 22 young (45056K), 4 survivors (8192K) Metaspace used 40041K, committed 40640K, reserved 1114112K class space used 5635K, committed 5888K, reserved 1048576K
- Line 3:
Heap
size:102400k
, used heap size is62525K
- Line 5:
Metaspace
used size:40041K
.
Here is another example with the jcmd
command and listing the first 10 items.
jcmd 13352 GC.class_histogram
C:\MaryZheng\workspace\sbmemory21g>jcmd 13352 GC.class_histogram 13352: num #instances #bytes class name (module) ------------------------------------------------------- 1: 68467 8375992 [B (java.base@21.0.7) 2: 66137 1587288 java.lang.String (java.base@21.0.7) 3: 10956 1294000 java.lang.Class (java.base@21.0.7) 4: 33264 1064448 java.util.concurrent.ConcurrentHashMap$Node (java.base@21.0.7) 5: 12388 667024 [Ljava.lang.Object; (java.base@21.0.7) 6: 5335 656432 [I (java.base@21.0.7) 7: 13316 532640 java.util.LinkedHashMap$Entry (java.base@21.0.7) 8: 7755 496320 java.util.LinkedHashMap (java.base@21.0.7) 9: 6564 473048 [Ljava.util.HashMap$Node; (java.base@21.0.7) 10: 334 327328 [Ljava.util.concurrent.ConcurrentHashMap$Node; (java.base@21.0.7)
4.2 The Jmap Command
The jmap
command outputs the retained size and order by the descending order.
jmap -histo
C:\MaryZheng\workspace\sbmemory21g>jmap -histo 13352 num #instances #bytes class name (module) ------------------------------------------------------- 1: 119432 12819960 [B (java.base@21.0.7) 2: 111350 4506600 [Ljava.lang.Object; (java.base@21.0.7) 3: 62023 2480920 java.util.TreeMap$Entry (java.base@21.0.7) 4: 102408 2457792 java.lang.String (java.base@21.0.7) 5: 10491 1645864 [I (java.base@21.0.7) 6: 11163 1318736 java.lang.Class (java.base@21.0.7) 7: 36762 1176384 java.util.concurrent.ConcurrentHashMap$Node (java.base@21.0.7) 8: 3451 1109904 [C (java.base@21.0.7) 9: 11883 1045704 java.lang.reflect.Method (java.base@21.0.7) 10: 16221 1038144 java.util.LinkedHashMap (java.base@21.0.7)
4.3 The Jstat Command
The jstat
command outputs the memory usage for the gc
process every 10 seconds.
jstat -gc 1000
C:\MaryZheng\workspace\sbmemory21g>jstat -gc 13352 1000 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT 0.0 2048.0 0.0 23.0 47104.0 0.0 26624.0 20984.7 42112.0 40993.7 6144.0 5744.2 11 0.042 2 0.047 4 0.004 0.094 0.0 2048.0 0.0 23.0 47104.0 0.0 26624.0 20984.7 42112.0 40993.7 6144.0 5744.2 11 0.042 2 0.047 4 0.004 0.094 0.0 2048.0 0.0 23.0 47104.0 2048.0 26624.0 20984.7 42112.0 40993.7 6144.0 5744.2 11 0.042 2 0.047 4 0.004 0.094
- S0C/S0U, S1C/S1U: Survivor spaces capacity/used
- EC/EC: Eden capacity/used
- OC/OU: Old generation capacity/used
- MC/MU: Metaspace capacity/used
- CCSC/CCSU: Compressed class space capacity/used
4.4 Find the Total Physical Memory
In this step, I will use the systeminfo
command to find my PC’s total physical memory.
systeminfo | find “Total Physical Memory”
C:\MaryZheng\workspace\sbmemory21g>systeminfo | find "Total Physical Memory" Total Physical Memory: 16,069 MB C:\MaryZheng\workspace\sbmemory21g>
My PC’s total physical memory is 16GB, so the MaxHeapSize
cannot exceed 4GB(1/4 of total physical memory).
5. Memory Usage Optimization
In this step, I will adjust the Spring Boot web application’s JVM memory options, Tomcat thread max count, Spring Bean lazy initialization and compare its memory footprint to the data captured at step 3 and 4.
5.1 Adjust JVM Memory Options
Please refer to my other article for adjusting the JVM memory options. Here are common JVM memory options:
-Xms#
sets the initial heap size when the JVM starts.-Xmx#
sets the maximum heap size for the JVM. Setting both initial and maximum heap size to the same value can avoid the overhead for heap resizing, but it can waste memory if not used.-Xmn#
sets the size of the young generation (part of the heap).-Xss#
sets the stack size for each thread.-XX:+HeapDumpOnOutOfMemoryError
generates a heap dump when anOutOfMemoryError
occurs.-XX:HeapDumpPath={path_to_dump_file}
specifies the path location.-XX:+UseG1GC
: Use the G1 garbage collector.-XX:+UseConcMarkSweepGC
: Use the Concurrent Mark-Sweep (CMS) garbage collector.-XX:+UseParallelGC
: Use the parallel garbage collector.-XX:+AggressiveOpts
: Enable aggressive optimization settings for performance.-XX:MaxInlineLevel
: Control the maximum number of methods that can be inlined.-XX:MaxMetaspaceSize
: Set the maximum size of the Metaspace after JDK8.-XX:ReservedCodeCachedSize
: Reserve code cached size in bytes.-XX:+UseContainerSupport
: This is enabled by default since JDK10-XX:MaxRAMPercentage
: Configure the memory with percentage instead of hard code value.
5.2 Adjust Tomcat Thread Configuration
In this step, I will adjust the Spring Boot Application Tomcat server configuration by setting the max Tomcat thread count to 4 (the Spring Boot application has a default max thread count of 200).
application.properties
spring.application.name=sbmemory21g //default 200 server.tomcat.threads.max=4 //default 10 server.tomcat.threads.min-space=2
After updating the Tomcat threads setting, start the Spring Boot application and capture its memory footprint via both VisualVM and the command line outlined at step 3 and 4.
jcmd GC.heap_info output
C:\MaryZheng\workspace\sbmemory21g>jcmd 14200 GC.heap_info 14200: garbage-first heap total 83968K, used 32453K [0x0000000704e00000, 0x0000000800000000) region size 2048K, 3 young (6144K), 1 survivors (2048K) Metaspace used 41358K, committed 42048K, reserved 1114112K class space used 5840K, committed 6144K, reserved 1048576K
Compare its Heap, Metaspace, threads, classes loaded to the data captured at step 3.
Basic Spring Boot with Default 200 threads | Updated Spring Boot with 4 Threads | Comparison Results | |
Heap Used | 62525K | 32453K | reduced by 48% |
Metaspace | 40041K | 41358K | increased by 3% |
Total Thread Started | 34 | 28 | reduced |
Total Classes Loaded | 10505 | 10488 | reduced |
It trades memory footprint with throughput by reducing the thread count.
5.3 Adjust Lazy Bean Initialization
In this step, I will adjust the Spring Boot Application lazy bean configuration.
application.properties
spring.main.lazy-initialization=true
After updating the Tomcat thread setting, start the Spring Boot application and capture the memory usage.
application.properties.java
C:\MaryZheng\workspace\sbmemory21g>jcmd 25168 GC.heap_info 25168: garbage-first heap total 98304K, used 53585K [0x0000000704e00000, 0x0000000800000000) region size 2048K, 16 young (32768K), 3 survivors (6144K) Metaspace used 38358K, committed 39040K, reserved 1114112K class space used 5376K, committed 5696K, reserved 1048576K
Compare its Heap, Metaspace, thread, classes loaded to the data captured at step 3.
Basic Spring Boot with Default Setting | Updated Spring Boot Lazy Bean Initialization | Comparison Results | |
Heap Used | 62525K | 53585K | reduced by 15% |
Metaspace | 40041K | 38358K | reduced by 4% |
Total Thread Started | 34 | 28 | -4 |
Total Classes Loaded | 10505 | 9744 | reduced by 7% |
It trades memory footprint with latency by lazy initializing beans.
5.4 Disable Unused Spring Feature
In this step, I will adjust the Spring Boot Application configuration to disable unused features.
application.properties.java
spring.jmx.enabled=false spring.main.banner-mode=off
After updating the Tomcat thread setting, start the Spring Boot application and capture its memory usage.
jcmd 28564 GC.heap_info
C:\MaryZheng\workspace\sbmemory21g>jcmd 28564 GC.heap_info 28564: garbage-first heap total 98304K, used 52560K [0x0000000704e00000, 0x0000000800000000) region size 2048K, 26 young (53248K), 2 survivors (4096K) Metaspace used 38385K, committed 39168K, reserved 1114112K class space used 5388K, committed 5760K, reserved 1048576K
Compare its memory, metaspace, thread, classes loaded to the data captured at step 3.
Basic Spring Boot with Default Setting | Updated Spring Boot Disable JMX | Comparison Results | |
Heap Used | 62525K | 52560K | reduced by 15% |
Metaspace | 40041K | 38385K | reduced by 4% |
Total Thread Started | 34 | 28 | -4 |
Total Classes Loaded | 10505 | 9765 | reduced by 7% |
6. Container-Friendly Practices
There are several categories based on container-friendly guidelines when configuring JVM options.
-XX:+UseContainerSupport
to respect container memory limit.- Configure memory with
-XX:MaxRAMPercentage=75.0
instead of hardcoding-Xmx
. - Consider
G1GC
(default in Java 9+) orZGC
for low-latency apps. - Adjust GC threads if the container CPU is small
-XX:ParallelGCThreads=2
-XX:ConcGCThreads=2
.
- Avoid Large Heap Defaults.
- Control Async/Scheduled thread pools by setting the boundary, e.g. max pool size. max queue capacity, etc.
- Use smaller docker image
- Graceful shutdown and set shutdown timeout
7. Other Optimization Techniques
When tuning a Spring Boot web application, there are many optimization techniques beyond just memory and thread settings. They fall into several categories:
- Database optimization – proper indexes, batch operations, caching, connection pool.
- Caching – Http caching and application-level caching.
- Security Optimizations – minimize Spring security filters in the chain.
- Code-level Best practices – avoid heavy object creation, use DTO instead of entities for API response, minimize reflection, reuse expensive resources, e.g.
ObjectMapper
.
8. Conclusion
In this example, I created a simple Java Spring Web Application and monitored its memory usage via both VisualVM tool and jcmd
, jstat
commands. I repeatedly monitor the memory footprint for the same Spring Boot web application program after changing its thread pool, lazy-bean initialization, and disable JMX configuration. Please remember everything comes with price. The reduced memory footprint was traded with throughput and/or latency. Don’t over-optimization the setting.
9. Download
This was an example of a Java Spring Boot Web project.
You can download the full source code of this example here: How to Reduce Spring Boot Memory Usage?