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.
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.