提升springboot应用速度的方法
问题的出现背景
当对微服务的服务拆分没有把控好业务边界、拆分力度多大的时候,就会出现问题。
主要的耗时原因
为了优化启动速度,首先我们需要观察到底应用慢在哪里,IDEA自带集成了async-profile工具,所以通过火焰图,可以更直观地看到一些应用启动过程中的问题。
通过这张图可以看到大量的耗时在Bean的加载和初始化过程中。
火焰图的看法
Y轴表示调用栈,每一层都是一个函数。调用栈越深火焰就越高。
顶部就是正在执行的函数,下方都是他的父函数。
X轴表示抽样数。
如果一个函数在x轴占据的宽度越宽,就表示他被抽到的次数越多,即执行的时间越长。
优化springboot启动速度的方法
1. 从业务层面的优化
- 减少业务初始化,对一些过大的应用,大部分的耗时都应该在业务太大或者包含大量的初始化逻辑上面。比如建立数据库连接、redis连接、业务逻辑的查询舒适化等等。对于业务方的建议则是尽量减少不必要的依赖。没有必要启动就初始化的业务能异步则异步。
- 延迟初始化,springboot2.2版本之后引入了延迟初始化的属性,配置为true就可以。
表示所有的Bean都将延迟初始化。这样一定程度上可以提高启动速度。但是会导致第一次访问的时候较慢。 - spring 5之后提供了这个功能,它的主要作用是解决在类扫描的时候,避免类过多导致扫描速度过慢的问题。使用的方法也很简单,导入依赖,然后在启动类上打上@Indexed注解。这样在程序编译打包之后,会生成
META-INF/spring.components
文件。当执行ComponentScan
扫描类时,会读取索引文件,提高扫描速度。 - 关闭JMX,springboot2.2以下的版本默认会开启jmx,可以使用jconsole查看。对于我们无需这些监控的话,可以手动关闭它。
- 关闭分层编译,JDK8之后的版本默认打开多层编译,可以使用命令去查看。
其中Tier3就是C1编译器,Tier4就是C2编译器,表示一个方法解释编译2000次以后进行c1编译,c1编译以后执行15000次会进行C2编译。
我们可以通过命令使用C1编译器,这样就不存在C2编译器的优化阶段,能够提高启动速度,同时配合这个命令关闭字节码验证。
但是尽量不要在线上环境使用。
2. 从Java应用程序层面的优化
上面的介绍时一些从业务层面启动参数之类的优化,下面我们接着看一下java应用程序本身有哪些途径可以进行优化。
在此之前我们回忆一下java创建对象的过程,首先需要进行类加载,然后去创建对象,对象创建之后就可以调用对象方法了。
这中间涉及到jit,jit通过运行时将字节码编译为本地机器码来提高java程序的性能,因此下面谈到的几个技术,将会概括以上涉及的几个步骤:
第一个JAR INDEX, JAR包本质上就是一个ZIP文件。当加载类的时候,我们通过类加载器去遍历JAR包,找到对应的class文件进行加载,然后验证准备解析,初始化、实例化对象,JAR INDEX实际上是一个很古老的技术,就是用来解决在加载类的时候,遍历JAR性能的问题。早在JDK1.3版本中就已经引入了。
举个例子来说,假设我们要在abc三个JAR包中查找class文件:
如果能够通过类型com c,立刻推断出具体在哪个JAR包,就可以避免JAR遍历的过程,不过对于现在项目来说,Jar Index很难应用。
首先第一点,通过jar -i生成的索引文件是基于META-INF/MANIFEST.MF中的Class-Path来的,我们目前大多项目不会涉及到这个,所以索引文件的生成需要我们自己去做额外的处理。
第二点只支持URLClassLoader,需要我们自己自定义类加载逻辑,第二个APP CDS, 全称为Application Class Data Sharing,主要是用于启动加速和节省内存。其实早在JDK1.5版本就已经引入,只是在过去的版本迭代过程中,在不断的升级优化。而在jdk13版本中则是默认打开。早期的CDS只支持BootClassLoader,但是在JDK8中引入了AppCDS,支持AppClassLoader和自定义的ClassLoader。
其实我们都知道类在加载过程中伴随着解析、校验这个过程。而CDS就是将这个过程产生的数据结构存储到归档文件中。在下次运行的时候重复使用。这个归档文件被称作Shared Archive。
以jsa
作为文件后缀,在使用的时候,其实就是将jsa文件映射到内存当中,让对象头中的类型指针指向该内存地址。需要注意的是AppCDS只会在包含所有class文件的FatJar生效。对于SpringBoot的嵌套Jar结构无法生效。需要利用maven shade plugin来创建shade jar。
第三个,Heap Archive,JDK9中引入了HeapArchive,并且在JDK12中被正式使用。
我们可以认为Heap Archive是对APPCDS的一个延伸。简单的来说,可以认为HeapArchive是在类初始化的时候,通过内存映射持久化了一些static字段,避免调用类初始化器,提前拿到初始化好的类,提高启动速度。
第4个,AOT编译。我们说过JIT是通过运行时将字节码编译为本地机器码,需要的时候直接执行,减少了解释的时间,从而提高程序运行速度。
上面我们提到的3个提高应用启动速度的方式都可以归为类加载的过程。
当真正创建对象实例,执行方法的时候,由于可能没有被jit编译,在解释模式下执行的速度非常慢。
所以产生了AOT编译的方式,AOT(Ahead-Of-Time)指的是程序运行之前发生的编译行为。它的作用相当于预热,提前编译为机器码,减少解释时间。
比如现在Spring Cloud Native就是这样,在运行时直接静态编译成可执行文件,不依赖JVM,所以速度非常快,但是Java中AOT技术不够成熟。作为实验性的技术,在JDK8之后版本默认关闭,需要我们去手动打开。而且由于长期缺乏维护和调优,这项技术在jdk16的版本中已经被移除了。