51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Boot 启动加速

1、简介 {#1简介}

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

2、调整 Spring 应用 {#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、延迟初始化 {#21延迟初始化}

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、排除不必要的自动配置 {#22排除不必要的自动配置}

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、其他小调整 {#23其他小调整}

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调整-jvm}

3.1、Verify 参数 {#31verify-参数}

此参数用于设置字节码验证模式。字节码验证可确定类的格式是否正确,是否符合 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 参数 {#32tieredcompilation-参数}

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 {#4spring-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

赞(3)
未经允许不得转载:工具盒子 » Spring Boot 启动加速