Spring Boot 启动加速

1、简介

本文将带你了解如何通过调整 Spring 应用的配置、JVM 参数和使用 GraalVM 原生镜像来缩短 Spring Boot 的启动时间。

2、调整 Spring 应用

首先,创建一个 Spring Boot(2.5.4)应用,添加 Spring Web、Spring Actuator 和 Spring Security 依赖。

还要添加 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>com.baeldung.springStart.SpringStartApplication</mainClass> 
    </configuration> 
    <executions> 
        <execution> 
            <goals> 
                <goal>repackage</goal> 
            </goals> 
        </execution> 
    </executions> 
</plugin>

使用标准的 java -jar 命令运行 jar 文件,并查看应用的启动时间。

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

如上,应用启动时间约为 3.4 秒。我们把这个时间作为下文调整的参考。

2.1、延迟初始化

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

spring.main.lazy-initialization=true

新建一个 jar 文件并按上例配置、启动后,新的启动时间略有改善:

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

根据代码规模的大小,延迟初始化可以显著缩短启动时间。启动时间的减少取决于应用的依赖关系。

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

不过,延迟初始化也有一些缺点。最明显的缺点是应用处理第一个请求的速度会变慢,因为 Spring 需要时间来初始化所需的 Bean。另一个缺点是可能会在启动时错过一些错误。这可能会在运行时导致 ClassNotFoundException

2.2、排除不必要的自动配置

Spring Boot 的理念是约定大于配置。Spring 可能会初始化应用并不需要的 Bean。我们可以通过启动日志检查所有自动配置的 Bean。

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.b.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 性能更好。它需要的内存更少,平均响应时间也更长。

修改 pom.xml,切换到 Undertow:

<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 Boot 中使用 Undertow 作为嵌入式服务器”。

在 classpath 扫描方面可以有以下小改进。Spring 的 classpath 扫描速度很快。当代码库较大时,可以通过创建静态索引来缩短启动时间。

添加一个依赖 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)配置文件有几个有效的存放位置,最常见的是在 classpath 根目录下或与 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 规范约束。

该参数有几个选项:

  • -Xverify 是默认值,可对所有非 “boot” 类进行验证。
  • -Xverify:all 可对所有类进行验证。这种设置会对启动性能产生很大的负面影响。
  • -Xverify:none(或 -Xnoverify)该选项可完全禁用校验器,大大缩短启动时间。

在启动 JVM 时设置此参数。

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

JVM 会警告这个选项已被弃用,此外,启动时间也会缩短:

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

这个选项会有一个重大的问题,可能导致应用在运行时因错误而崩溃,而这些错误本应该提前捕获到的。这也是该选项在 Java 13 中被标记为废弃的原因之一。

3.2、TieredCompilation 参数

Java 7 引入了分层编译。HotSpot 编译器将对代码进行不同层次的编译。

Java 代码首先被解释为字节码。然后,字节码被编译成机器码。这种编译发生在方法级别。C1 编译器会在调用一定次数后对方法进行编译。在运行更多次后,C2 编译器会对其进行编译,从而进一步提高性能。

使用 -XX:-TieredCompilation 参数,可以禁用中间编译层。这意味着我们的方法将使用 C2 编译器进行解释或编译,以实现最大优化。这不会降低启动速度。

要禁用 C2 编译。可以使用 -XX:TieredStopAtLevel=1 选项。结合 -noverify 参数,可以缩短启动时间。遗憾的是,这会降低 JIT 编译器后期的运行速度。

使用 -XX:TieredStopAtLevel=1 选项就带来了显著的改进:

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

如果同时使用本节中的 2 个参数,还能进一步缩短启动时间:

 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)是使用 AOT(Ahead-Of-Time)编译器编译的 Java 代码,并打包成可执行文件。它不需要 Java 就能运行。由于没有 JVM 的开销,因此程序运行速度更快,对内存的依赖性也更小。GraalVM 项目引入了原生镜像和所需的构建工具。

Spring Native 是一个实验性模块,它支持使用 GraalVM 原生镜像编译器对 Spring 应用程序进行原生编译。AOT 编译器会在构建过程中执行多项任务,从而缩短启动时间(静态分析、删除未使用的代码、创建固定的类路径等)。

原生镜像仍有一些限制:

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

要将应用编译为原生镜像,需要在 pom.xml 中添加 spring-aotspring-aot-maven-plugin 依赖。Maven 将在 target 文件夹中,通过 package 命令创建原生镜像。


参考:https://www.baeldung.com/spring-boot-startup-speed