1. Introduction

In this tutorial, we’ll cover different configurations and setups that can help decrease Spring Boot startup time. First, we’ll go over Spring specific configurations. Second, we’ll cover Java virtual machine options. Finally, we’ll cover how we can leverage GraalVM and native image compilation to reduce startup time further.

2. Spring Tweaks

Before we start, let’s set up a test application. We’ll use Spring Boot version 2.5.4 with Spring Web, Spring Actuator, and Spring Security as dependencies. In pom.xml, we’ll add spring-boot-maven-plugin with configuration to pack our application in a jar file:

<plugin> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-maven-plugin</artifactId> 
    <version>${spring-boot.version}</version> 
    <configuration> 
        <finalName>springStartupApp</finalName> 
        <mainClass>com.baeldung.springStart.SpringStartApplication</mainClass> 
    </configuration> 
    <executions> 
        <execution> 
            <goals> 
                <goal>repackage</goal> 
            </goals> 
        </execution> 
    </executions> 
</plugin>

We run our jar file with the standard java -jar command and monitor the start time of our application:

c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 3.403 seconds (JVM running for 3.961)

As we can see, our application starts at approximately 3.4 seconds. We’ll use this time as a reference for future tweaks.

2.1. Lazy Initialization

Spring Framework has support for lazy initialization. Lazy initialization means that Spring won’t create all beans on startup. Also, Spring will inject no dependencies until that bean is needed. Since Spring Boot version 2.2. it’s possible to enable lazy initialization using the application.properties:

spring.main.lazy-initialization=true

After building a new jar file and starting it as in the previous example, the new startup time is slightly better:

 c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 2.95 seconds (JVM running for 3.497)

Depending on the size of our codebase, lazy initialization can result in a significant amount of startup time reduction. The reduction depends on the dependency graph of our application.

Also, lazy initialization has benefits during development while using DevTools hot restart functionality. An increased number of restarts with lazy initialization will enable JVM to optimize the code better.

However, lazy initialization has a few drawbacks. The most significant disadvantage is that the application will serve the first request slower. Because Spring needs time to initialize the required beans, another disadvantage is that we can miss some errors on startup. This can result in ClassNotFoundException during runtime.

2.2. Excluding Unnecessary Autoconfiguration

Spring Boot always favored convention over configuration. Spring may initialize beans that our application doesn’t require. We can check all autoconfigured beans using startup logs. Setting the logging level to DEBUG on org.springframework.boot.autoconfigure in the application.properties:

logging.level.org.springframework.boot.autoconfigure=DEBUG

In the logs, we’ll see new lines dedicated to autoconfiguration, starting with:

============================
CONDITIONS EVALUATION REPORT
============================

Using this report, we can exclude parts of the application’s configuration. To exclude part of the configuration, we use @EnableAutoConfiguration annotation:

@EnableAutoConfiguration(exclude = {JacksonAutoConfiguration.class, JvmMetricsAutoConfiguration.class, 
  LogbackMetricsAutoConfiguration.class, MetricsAutoConfiguration.class})

If we excluded the Jackson JSON library and some of the metrics configuration that we don’t use, we could save some time on startup:

c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 3.183 seconds (JVM running for 3.732)

2.3. Other Minor Tweaks

Spring Boot comes with an embedded servlet container. By default, we get Tomcat. While Tomcat is good enough in most cases, other servlet containers can be more performant. In tests, Undertow from JBoss performs better than Tomcat or Jetty. It requires less memory and has a better average response time. To switch to Undertow, we need to change pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

The following minor improvement can be in the classpath scan. Spring classpath scanning is fast action. We can improve startup time by creating a static index when we have a large codebase. We need to add a dependency to the spring-context-indexer to generate the index. Spring doesn’t require any additional configuration. During compile-time, Spring will create an additional file in META-INF\spring.components. Spring will use it automatically during startup:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-indexer</artifactId>
    <version>${spring.version}</version>
    <optional>true</optional>
</dependency>

Since we have only one Spring component, this tweak didn’t produce significant results in our tests.

It’s worth noting the spring-context-indexer has been deprecated since Spring 6.1. More information about it can be found here.

Next, there are several valid places for application.properties (or .yml) files. Most usual are at classpath root or in the same folder as the jar file. We can avoid searching multiple locations by setting an explicit path with spring.config.location parameter and save a couple of milliseconds on searching:

java -jar .\target\springStartupApp.jar --spring.config.location=classpath:/application.properties

Finally, Spring Boot offers some MBeans to monitor our application using JMX. Turn off JMX entirely and avoid the cost of creating those beans:

spring.jmx.enabled=false

3. JVM Tweaks

3.1. Verify Flag

This flag sets bytecode verifier mode. Bytecode verification provides whether classes are properly formated and within JVM specification constraints. We set this flag on JVM during startup.

There are a couple of options for this flag:

  • -Xverify is the default value and enables verification on all non-bootloader classes. 
  • -Xverify:all enables verification of all classes. This setup will have a significant negative performance impact on startups.
  • -Xverify:none (or -Xnoverify). This option disables the verifier completely and will reduce startup time significantly.

We can pass this flag on startup:

java -jar -noverify .\target\springStartupApp.jar 

We’ll get a warning from JVM that this option is deprecated. Also, startup time will decrease:

 c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 3.193 seconds (JVM running for 3.686)

This flag brings in a significant tradeoff. Our application can break during runtime with an error that we could catch earlier. This is one of the reasons this option is marked as deprecated in Java 13. Hence it will be removed in future releases.

3.2. TieredCompilation Flag

Java 7 introduced tiered compilation. The HotSpot compiler will use different levels of compilation for the code.

As we know, Java code first gets interpreted to bytecode. Next, bytecode gets compiled into machine code. This translation happens on the method level. C1 compiler compiles a method after a certain amount of calls. After even more runs C2 compiler compiles it increasing performance even more.

Using the -XX:-TieredCompilation flag, we can disable intermediate compilation tiers. This means that our methods will get interpreted or compiled with the C2 compiler for maximum optimization. This won’t result in a decrease in startup speed. What we need is to disable the C2 compilation. We can do this with -XX:TieredStopAtLevel=1 option. In conjunction with -noverify flag, this can reduce startup time. Unfortunately, this will slow down the JIT compiler at later stages.

TieredCompilation flag alone brings in solid improvement:

 c.b.springStart.SpringStartApplication   : Started SpringStartApplication in 2.754 seconds (JVM running for 3.172)

For an extra kick, running both flags from this section in conjunction reduces startup time even more:

 java -jar -XX:TieredStopAtLevel=1 -noverify .\target\springStartupApp.jar
c.b.springStart.SpringStartApplication : Started SpringStartApplication in 2.537 seconds (JVM running for 2.912)

4. Spring Native

Native image is Java code compiled using an ahead-of-time compiler and packed into an executable file. It doesn’t require Java to run. The resulting program is faster and less memory-dependent since there is no JVM overhead. The GraalVM project introduced native images and required build tools.

Spring Native is an experimental module that supports the native compilation of Spring applications using the GraalVM native-image compiler. The ahead-of-time compiler executes several tasks during build time that reduce startup time (static analysis, removal of unused code, creating fixed classpath, etc.). There are still some limitations to native images:

  • It doesn’t support all Java features
  • Reflection requires a special configuration
  • Lazy class loading is unavailable
  • Windows compatibility is an issue.

To compile an application to a native image, we need to add the spring-aot and spring-aot-maven-plugin dependency to pom.xml. Maven will create the native image on the package command in the target folder.

5. Conclusion

In this article, we explored different ways to improve the startup time of Spring Boot applications. First, we covered various Spring related features that can help reduce startup time. Next, we showed JVM-specific options. Last, we introduced Spring Native and native image creation. As always, the code used in this article can be found over on GitHub.