一、简介
本文将带你了解如何通过调整 Spring 应用的配置、JVM 参数和使用 GraalVM 原生镜像来缩短 Spring Boot 的启动时间。
二、调整 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 秒。我们把这个时间作为下文调整的参考。
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、排除不必要的自动配置
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)
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>
在 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
三、调整 JVM
1、Verify 参数
此参数用于设置字节码验证模式。字节码验证可确定类的格式是否正确,是否符合 JVM 规范约束。
该参数有几个选项:
-Xverify
是默认值,可对所有非 "boot" 类进行验证。-Xverify:all
可对所有类进行验证。这种设置会对启动性能产生很大的负面影响。-Xverify:none
(或-Xnoverify
)该选项可完全禁用校验器,大大缩短启动时间。
在启动 JVM 时设置此参数。
从 JDK 13 开始及其后续版本中,不建议继续使用 -Xverify:none
和-noverify
参数。
java -jar -noverify .\target\springStartupApp.jar
JVM 会警告这个选项已被弃用,此外,启动时间也会缩短:
c.b.springStart.SpringStartApplication : Started SpringStartApplication in 3.193 seconds (JVM running for 3.686)
这个选项会有一个重大的问题,可能导致应用在运行时因错误而崩溃,而这些错误本应该提前捕获到的。这也是该选项在 Java 13 中被标记为废弃的原因之一。
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)
四、Spring Native
原生镜像(Native image)是使用 AOT(Ahead-Of-Time)编译器编译的 Java 代码,并打包成可执行文件。它不需要 Java 就能运行。由于没有 JVM 的开销,因此程序运行速度更快,对内存的依赖性也更小。GraalVM 项目引入了原生镜像和所需的构建工具。
Spring Native是一个实验性模块,它支持使用 GraalVM 原生镜像编译器对 Spring 应用程序进行原生编译。AOT 编译器会在构建过程中执行多项任务,从而缩短启动时间(静态分析、删除未使用的代码、创建固定的类路径等)。
原生镜像仍有一些限制:
- 它不支持所有 Java 功能
- 反射功能需要特殊配置
- 延迟类加载不能使用
- Windows 兼容性问题
要将应用编译为原生镜像,需要在 pom.xml
中添加 spring-aot
和 spring-aot-maven-plugin
依赖。Maven 将在 target
文件夹中,通过 package
命令创建原生镜像。