How we reduced the memory consumption of spring boot application over 40% for the development environment

Shoeb Ahmed Tanjim
4 min readDec 28, 2023

The base spring boot application takes an affordable amount of memory. But when we start adding more dependencies, the memory consumption also gets higher. But if you are developing a monolithic application, in most cases it’s still affordable. But if you are developing microservices and you need to run a couple of services on your local machine, then it can become a nightmare. It can slow down your PC thus slowing your productivity.

Spring boot & JVM comes with some default configurations that are good for most cases. These default configurations are powerful enough to support some production environments as well. But if you can tune some of the configurations for your local development, you can reduce the memory consumption significantly. I’m not an expert in JVM & spring boot, I just want to share my experience in this article.

But before starting we need to know…

Who consumes the memory?

Well, JVM. But how?

To know in-depth, we need to know the internals of JVM which is out of the scope of this article. But roughly, total consumed memory by JVM = Heap + Meta Space + Stack per thread x threads + Others.

JVM Memory Usage
JVM Memory Usage

To reduce the memory consumption, we need to tell the JVM explicitly.

Requirements

  • Docker & docker-compose
  • Java Version: 17 (But any version between 8 to latest should work expect some of the old patch of Java 8)
  • Spring Boot

Let’s configure the parameters first

Create a file called dev.jvm.conf and put the values. (We will explain the value later)

# dev.jvm.conf
# Override Application Property
SERVER_TOMCAT_ACCEPT_COUNT=3
SERVER_TOMCAT_MAX_CONNECTIONS=3
SERVER_TOMCAT_THREADS_MAX=3
SERVER_TOMCAT_THREADS_MIN_SPARE=1
SPRING_MAIN_LAZY_INITIALIZATION=true

# Set JVM Parameters
JAVA_TOOL_OPTIONS=-XX:+UseSerialGC -Xss512k -XX:MaxRAM=200m

Now we will pass these environment variables into the container using docker compose.

# docker-compose.yml
services:
service1:
image: service1:dev
env_file:
- dev.jvm.conf

service2:
image: service2:dev
env_file:
- dev.jvm.conf

Now run `docker compose up` and you should see the difference.

Now, let’s talk about the configurations

Before getting started, keep in mind that reducing some of the value doesn’t have direct impact on reducing the memory usage for local environment. Because we will not get that many requests in local environment. We are adding a threshold, so that even in the local environment, if we start getting more requests, it will limit that. It will eventually help to limit the memory usage.

SERVER_TOMCAT_ACCEPT_COUNT: This property sets the maximum queue length for incoming connection requests when all possible request processing threads are in use. When the server is under heavy load and all worker threads are busy, incoming requests are placed in a queue. If the queue becomes full, additional connection requests will be rejected. The default value is 100.

SERVER_TOMCAT_MAX_CONNECTIONS: This property defines the maximum number of connections that can be handled concurrently by the Tomcat server. The default value is 8192.

SERVER_TOMCAT_THREADS_MAX: This property controls the maximum number of request processing threads that the Tomcat server will create. The default value is 200.

SERVER_TOMCAT_THREADS_MIN_SPARE: This property would set the minimum number of spare threads for the embedded Tomcat server. Default is 10.

SPRING_MAIN_LAZY_INITIALIZATION: Setting the property value to true means that all the beans in the application will use lazy initialization. It will help you to improve the startup time.

JAVA_TOOL_OPTIONS: Using this property we are passing some extra parameters to the JVM. Let’s talk about each of them.

  • -XX:+UseSerialGC: This will perform garbage collection inline with the thread allocating the heap memory instead of a dedicated GC thread(s).
  • -Xss512k: This will limit each threads stack memory to 512KB instead of the default 1MB
  • -XX:MaxRAM=200m: This will limit the maximum memory usage to 200MiB. Note that, heap and non-heap managed memory will be within the limits of this value. In your case, the required memory might be different. In that case, you need to find the required memory and then set the value according to the requirements. There is a alternate and a better way to do this. That is, adding limit from the docker compose(Let docker handle the memory limit). JVM is now aware of cgroup memory limits (-XX:+UseContainerSupport is set by default), so -XX:MaxRAMPercentage and -XX:MinRAMPercentage will help you staying within the limit set by docker. To know details about this, you can follow the article listed in the reference below.

Note: These are meant to improve the efficiency of the development process. Don’t use them in production.

References

--

--