加快Spring Boot启动时间

2023/05/11

1. 简介

在本教程中,我们将介绍有助于减少Spring Boot启动时间的不同配置和设置。首先,我们将回顾一下Spring特定的配置。其次,我们将介绍Java虚拟机选项。最后,我们将介绍如何利用GraalVM和本机镜像编译来进一步缩短启动时间。

2. Spring调整

在开始之前,让我们设置一个测试应用程序。我们将使用Spring Boot版本2.5.4,并将Spring Web、Spring Actuator和Spring Security作为依赖项。在pom.xml中,我们将添加带有配置的spring-boot-maven-plugin以将我们的应用程序打包到一个jar文件中:

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

我们使用标准的java -jar命令运行我们的jar文件并监控我们应用程序的启动时间:

c.t.t.springstart.SpringStartApplication: Started SpringStartApplication in 3.403 seconds (JVM running for 3.961)

如我们所见,我们的应用程序在大约3.4秒后启动。我们将使用这段时间作为未来调整的参考。

2.1 惰性初始化

Spring框架支持惰性初始化。惰性初始化意味着Spring不会在启动时创建所有bean。此外,在需要该bean之前,Spring不会注入任何依赖项。从Spring Boot版本2.2开始。可以使用application.properties启用延迟初始化:

spring.main.lazy-initialization=true

在构建一个新的jar文件并像前面的例子一样启动它之后,新的启动时间稍微好一些:

c.t.t.springstart.SpringStartApplication: Started SpringStartApplication in 2.95 seconds (JVM running for 3.497)

根据我们代码库的大小,惰性初始化可以显著减少启动时间。减少取决于我们应用程序的依赖图。

此外,延迟初始化在使用DevTools热重启功能的开发过程中也有好处。增加延迟初始化的重启次数将使JVM更好地优化代码。

但是,惰性初始化有一些缺点。最显著的缺点是应用程序处理第一个请求的速度较慢。因为Spring需要时间来初始化所需的bean,另一个缺点是我们可能会在启动时错过一些错误。这可能会导致运行时出现ClassNotFoundException。

2.2 排除不必要的自动配置

Spring Boot总是倾向于约定优于配置。Spring可能会初始化我们的应用程序不需要的beans。我们可以使用启动日志检查所有自动配置的beans。在application.properties中的org.springframework.boot.autoconfigure上将日志记录级别设置为DEBUG:

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

在日志中,我们将看到专用于自动配置的新行,以:

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

使用此报告,我们可以排除部分应用程序配置。为了排除部分配置,我们使用@EnableAutoConfiguration注解:

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

如果我们排除Jackson JSON库和一些我们不使用的指标配置,我们可以在启动时节省一些时间:

c.t.t.springstart.SpringStartApplication: Started SpringStartApplication in 3.183 seconds (JVM running for 3.732)

2.3 其他小调整

Spring Boot带有一个嵌入式Servlet容器。默认情况下,我们获取Tomcat。虽然Tomcat在大多数情况下已经足够好了,但其他Servlet容器的性能可能更高在测试中,来自JBoss的Undertow表现优于Tomcat或Jetty。它需要更少的内存并具有更好的平均响应时间。要切换到Undertow,我们需要更改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>

以下小改进可以在类路径扫描中进行。Spring类路径扫描是快速操作。当我们有一个大型代码库时,我们可以通过创建静态索引来缩短启动时间。我们需要在spring-context-indexer中添加一个依赖来生成索引。Spring不需要任何额外的配置。在编译期间,Spring将在META-INF\spring.components中创建一个附加文件。Spring会在启动时自动使用它:

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

由于我们只有一个Spring组件,因此这种调整在我们的测试中没有产生显著的结果。

接下来,application.properties(或.yml)文件有几个有效位置。最常见的是在类路径根目录或与jar文件相同的文件夹中。我们可以通过使用spring.config.location参数设置显式路径来避免搜索多个位置,并节省几毫秒的搜索时间:

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

最后,Spring Boot提供了一些MBean来使用JMX监控我们的应用程序。完全关闭JMX并避免创建这些bean的成本

spring.jmx.enabled=false

3. JVM调整

3.1 Verify标志

该标志设置字节码验证器模式。字节码验证提供类的格式是否正确以及是否在JVM规范约束内。我们在启动期间在JVM设置此标志。

这个标志有几个选项:

  • -Xverify是默认值并启用对所有非引导加载程序类的验证。
  • -Xverify:all启用所有类的验证。此设置将对初创公司产生重大的负面性能影响。
  • -Xverify:none(或-Xnoverify)。此选项会完全禁用验证程序,并将显著减少启动时间。

我们可以在启动时传递这个标志:

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

我们将收到来自JVM的警告,指出此选项已弃用。此外,启动时间将减少:

c.t.t.springstart.SpringStartApplication: Started SpringStartApplication in 3.193 seconds (JVM running for 3.686)

这个标志带来了重要的权衡。我们的应用程序可能会在运行时中断,并出现我们可以更早捕获的错误。这是此选项在Java 13中被标记为已弃用的原因之一,因此它将在未来的版本中删除。

3.2 分层编译标志

Java 7引入了分层编译。HotSpot编译器将对代码使用不同级别的编译。

众所周知,Java代码首先被解释为字节码。接下来,字节码被编译成机器码。这种转换发生在方法级别。C1编译器在一定数量的调用后编译一个方法。在运行更多次之后,C2编译器编译它进一步提高性能。

使用-XX:-TieredCompilation标志,我们可以禁用中间编译层。这意味着我们的方法将使用C2编译器进行解释或编译,以实现最大优化。这不会导致启动速度下降。我们需要的是禁用C2编译。我们可以使用-XX:TieredStopAtLevel=1选项来做到这一点。结合-noverify标志,这可以减少启动时间。不幸的是,这会在后期减慢JIT编译器的速度。

TieredCompilation标志单独带来了坚实的改进:

c.t.t.springstart.SpringStartApplication: Started SpringStartApplication in 2.754 seconds (JVM running for 3.172)

更有趣的是,结合运行本节中的两个标志可以进一步减少启动时间:

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

4. Spring Native

本机映像是使用提前编译器编译并打包到可执行文件中的Java代码。它不需要运行Java。由于没有JVM开销,生成的程序速度更快且对内存的依赖更少。GraalVM项目引入了本机镜像和所需的构建工具。

Spring Native是一个实验模块,支持使用GraalVM本机镜像编译器对Spring应用程序进行本机编译。提前编译器在构建期间执行多项任务以减少启动时间(静态分析、删除未使用的代码、创建固定的类路径等)。原生镜像仍然存在一些限制:

  • 它不支持所有Java功能
  • 反射需要特殊配置
  • 延迟类加载不可用
  • Windows兼容性是一个问题

要将应用程序编译为原生镜像,我们需要在pom.xml中添加spring-aot和spring-aot-maven-plugin依赖。Maven将在target文件夹中的package命令上创建本机镜像。

5. 总结

在本文中,我们探讨了改进Spring Boot应用程序启动时间的不同方法。首先,我们介绍了各种有助于减少启动时间的Spring相关功能。接下来,我们展示了特定于JVM的选项。最后,我们介绍了Spring Native和原生镜像的创建。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章