就这样优化了SpringBoot,启动速度竟然这么快!有些SpringBoot启动速度太慢,你可能也有这样的经历。下面我们就SpringBoot启动速度优化的一些方面进行探讨。启动时间分析IDEA自带了一个集成的async-profile工具,所以我们可以通过火焰图更直观的看到启动过程中的一些问题。比如下面这个例子,通过火焰图和初始化可以看到很多耗时的Bean加载。图片来自IDEA集成的async-profile工具,可以在Preferences中搜索JavaProfilercustomconfiguration,开始使用RunwithxxProfiler。y轴代表调用栈,每一层都是一个函数,调用栈越深,火焰越高,最上面是正在执行的函数,最下面是它的父函数。x轴表示样本数。如果一个函数在x轴上占据的宽度越宽,说明它被采样的次数越多,也就是执行时间越长。启动优化,减少业务初始化耗时。大部分耗时业务应该是太大了或者包含了很多初始化逻辑,比如建立数据库连接,Redis连接,各种连接池等,给业务方的建议是尽量减少不必要的依赖,如果可以的话异步,它将是异步的。延迟初始化spring.main.lazy-initialization属性是在SpringBoot2.2之后引入的。如果配置为true,所有bean将被延迟初始化。一定程度上可以提高启动速度,但是第一次访问可能会比较慢。spring.main.lazy-initialization=trueSpringContextIndexerSpring5及以后版本提供了spring-context-indexer功能,主要作用是解决类扫描时类太多导致扫描速度过慢的问题。使用方法也很简单,导入依赖,然后在启动类上打上@Indexed注解,这样程序编译打包后就会生成META-INT/spring.components文件。当执行ComponentScan扫描类时,会读取索引文件来提高扫描。速度。org.springframeworkspring-context-indexertrue关闭JMXSpringBoot2.2.X以上版本会开启JMX默认情况下,可以使用jconsole查看,如果我们不需要这些监控,可以手动关闭。spring.jmx.enabled=false关闭Java8以后版本的分层编译,默认开启多层编译。使用命令java-XX:+PrintFlagsFinal-version|grepCompileThreshold来查看。Tier3是C1,Tier4是C2,也就是说一个方法解释编译2000次进行C1编译,C1编译执行15000次后进行C2编译。我们可以通过命令使用C1编译器,这样就没有了C2优化阶段,可以提高启动速度,同时配合-Xverify:none/-noverify关闭字节码校验,但是尽量不要使用它在在线环境中。-XX:TieredStopAtLevel=1-noverify另一种思路上面从业务层面和启动参数上介绍了一些优化。接下来,让我们看看优化基于Java的应用程序本身的方法。在此之前,让我们回顾一下Java中创建对象的过程。首先,我们需要加载类,然后创建对象。对象创建好后,我们就可以调用对象的方法了,这也会涉及到JIT。JIT在运行时将字节码编译成本地机器码,以提高Java程序的性能。因此,下面涉及的技术将上面涉及的几个步骤进行总结。JARIndexJar包其实本质上就是一个ZIP文件。在加载类时,我们通过类加载器遍历Jar包,找到对应的类文件进行加载,然后对对象进行校验、准备、解析、初始化、实例化。JarIndex其实是一个很古老的技术,用来解决加载类时遍历Jar的性能问题。早在JDK1.3就引入了。假设我们要在A\B\C这三个Jar包中找一个类。如果我们可以通过类型com.C立即推断出它在哪个jar包中,就可以避免遍历jar包的过程。A.jarcom/AB.jarcom/BC.jarcom/C可以通过JarIndex技术生成对应的索引文件INDEX.LIST。com/A-->A.jarcom/B-->B.jarcom/C-->C.jar但是对于现在的项目来说,JarIndex很难应用:jar-i生成的索引文件是基于META的-来自INF/MANIFEST.MF中的类路径。我们现在的项目大部分不会涉及到这个,所以索引文件的生成需要我们自己做额外的处理。只支持URLClassloader,需要我们自己自定义类加载逻辑APPCDSAppCDS的全称是ApplicationClassDataSharing,主要用于启动加速和内存节省。其实早在JDK1.5版本就已经引入,只是在后续的版本迭代过程中不断优化升级。在JDK13版本是默认Open,早期CDS只支持BootClassLoader,JDK8引入了AppCDS,支持AppClassLoader和自定义ClassLoader。我们都知道类加载的过程伴随着解析和验证的过程。CDS将此过程生成的数据结构存储在存档文件中,并在下一次运行时重复使用。此存档文件称为共享存档。作为文件后缀。使用时,将jsa文件映射到内存中,对象头中的类型指针指向内存地址。让我们一起看看如何使用它。首先,我们需要生成一个我们希望在应用程序之间共享的类列表,即lst文件。对于OracleJDK,需要添加-XX:+UnlockCommercialFeature命令开启商业化能力。OpenJDK不需要这个参数。在JDK13版本中,第一步和第二步合二为一,低版本仍然需要这样做。java-XX:DumpLoadedClassList=test.lst然后得到lst类列表后,dump成一个适合内存映射的jsa文件进行归档。java-Xshare:dump-XX:SharedClassListFile=test.lst-XX:SharedArchiveFile=test.jsa最后加上运行参数指定启动时的归档文件。-Xshare:on-XX:SharedArchiveFile=test.jsa需要注意的是,AppCDS只会对包含所有class文件的FatJar生效,对SpringBoot的嵌套Jar结构不起作用。您需要使用mavenshade插件来创建shadejar。helloworldorg.apache.maven.pluginsmaven-shade-plugintruefalse*:*META-INF/*.SFMETA-复制代码INF/*.DSAMETA-INF/*.RSApackage阴影</goal>META-INF/spring.handlersMETA-INF/spring.factoriesMETA-INF/spring.schemas${mainClass}然后按照上面的步骤就可以使用了,但是如果项目太大,文件数大于65535,启动的时候会报错:Causedby:java.lang.IllegalStateException:Zip64archivesarenotsupported源代码如下:publicintgetNumberOfRecords(){longnumberOfRecords=Bytes.littleEndianValue(this.block,this.offset+10,2);if(numberOfRecords==0xFFFF){thrownewIllegalStateException("Zip64archivesarenotsupported");}在2.2及以上版本修复了这个问题,所以在使用的时候尽量使用高版本,避免出现此类问题。HeapArchive在JDK9中引入,在JDK12中正式使用。我们可以认为HeapArchive是APPCDS的一个扩展,APPCDS是将类加载过程中验证解析产生的数据进行持久化,而HeapArchive是类初始化相关的堆内存的数据(执行静态代码块cinit用于初始化)。简单来说,可以认为HeapArchive在类初始化时通过内存映射持久化一些静态字段,避免调用类初始化器,提前获取初始化类,提高启动速度。AOT编译我们说过,JIT在运行时将字节码编译成本地机器码,需要的时候直接执行,减少解释的时间,从而提高程序的运行速度。我们上面提到的三种提高应用程序启动速度的方法,都可以归为类加载过程。当真正创建对象实例并执行方法时,解释模式下的执行速度很慢,因为可能没有经过JIT编译。于是AOT编译的方式诞生了。AOT(Ahead-Of-Time)是指在程序运行之前发生的编译行为。它的作用相当于预热,提前编译成机器码,减少解释时间。比如现在的SpringCloudNative是这样的。它在运行时直接静态编译成可执行文件,不依赖JVM,所以速度非常快。但是Java中的AOT技术还不够成熟。作为一项实验性技术,在JDK8之后的版本中默认关闭,需要手动开启。java-XX:+UnlockExperimentalVMOptions-XX:AOTLibrary=并且由于该技术长期缺乏维护和调优,在JDK16版本中已经移除,这里不再赘述。离线时间优化GracefulofflineSpringBoot在2.3版本中新增了一个特性。Gracefulshutdown支持Jetty、ReactorNetty、Tomcat和Undertow。Usage:server:shutdown:graceful#最长等待时间spring:lifecycle:timeout-per-shutdown-phase:30s如果版本低于2.3,官方也提供了低版本实现方案。新版本中的实现基本上是相同的逻辑。首先暂停外部请求,关闭线程池处理剩余任务。@SpringBootApplication@RestControllerpublicclassGh4657Application{publicstaticvoidmain(String[]args){SpringApplication.run(Gh4657Application.class,args);}@RequestMapping("/pause")publicStringpause()throwsInterruptedException{Thread.sleep(10000);返回“暂停完成”;}@BeanpublicGracefulShutdowngracefulShutdown(){返回新的GracefulShutdown();}@BeanpublicEmbeddedServletContainerCustomizertomcatCustomizer(){returnnewEmbeddedServletContainerCustomizer(){@Overridepublicvoidcustomize(ConfigurableEmbeddedServletContainercontainer){if(containerinstanceofTomcatEmbeddedServletContainerFactory){((TomcatEmbeddedServletContainerFactory)container).addConnectorCustomizers(gracefulShutdown());}}};}公关ivatestaticclassGracefulShutdownimplementsTomcatConnectorCustomizer,ApplicationListener{privatestaticfinalLoggerlog=LoggerFactory.getLogger(GracefulShutdown.class);}privatevolatileConnector连接器;@Overridepublicvoidcustomize(Connectorconnector){this.connector=connector;}@OverridepublicvoidonApplicationEvent(ContextClosedEventevent){this.connector.pause();执行器executor=this.connector.getProtocolHandler().getExecutor();if(executorinstanceofThreadPoolExecutor){尝试{ThreadPoolExecutorthreadPoolExecutor=(ThreadPoolExecutor)executor;threadPoolExecutor.shutdown();if(!threadPoolExecutor.awaitTermination(30,TimeUnit.SECONDS)){log.warn("Tomcat线程池没有关闭做在“+”30秒内优雅地wn。进行强制关闭");}}catch(InterruptedExceptionex){Thread.currentThread().interrupt();}}}}}Eureka服务下线时间另外,对于客户端感知服务器下线时间,我在中提到上一篇Eureka使用三级缓存保存服务实例信息,当服务注册时,会与服务端保持心跳,心跳时间为30秒,服务注册后,客户端的实例信息为保存在Registry服务注册中心,注册中心的信息会立即同步到readWriteCacheMap,如果客户端感知服务,需要从readOnlyCacheMap读取,这个只读缓存需要30秒从readWriteCacheMap同步。客户端和Ribbon负载均衡器都维护了一个本地缓存,每隔30秒定时同步一次,根据上面我们来计算客户端感知到一个服务下线的需要。多久。客户端每30秒向服务器发送一次心跳。注册中心保存了所有服务注册的实例信息。它会与readWriteCacheMap保持实时同步,readWriteCacheMap和readOnlyCacheMap每30秒同步一次。客户端每30秒同步一次readOnlyCacheMap的注册实例信息,考虑到ribbon如果做负载均衡的话,它还有一层缓存,每30秒同步一次。如果一个服务正常下线,极端情况下,时间应该是30+30+30+30差不多120秒。如果服务异常下线,需要依赖每60秒执行一次的清理线程,将超过90秒没有心跳的服务剔除。这里极端情况下,可能需要3次60秒才能检测到,也就是180秒。最长可能累计感知时间为:180+120=300秒,5分钟。当然,解决方案是更改这些时间。修改ribbon同步缓存时间为3秒:ribbon.ServerListRefreshInterval=3000修改客户端同步缓存时间为3秒:eureka.client.registry-fetch-interval-seconds=3修改心跳间隔为3秒:eureka.instance。lease-renewal-interval-in-seconds=3超时淘汰时间改为9秒:eureka.instance.lease-expiration-duration-in-seconds=9清洗线程计时改为5秒执行一次:尤里卡服务器。eviction-interval-timer-in-ms=5000同步到只读缓存的时间改为每3秒一次:eureka.server.response-cache-update-interval-ms=3000如果我们按照这个时间参数设置,我们有可能重新计算感知服务下线的最大时间:正常下线是3+3+3+3=12秒,异常下线加上15秒是27秒。结束就OK了,关于SpringBoot服务的启动和下线时间的优化的聊到此就结束了,不过我觉得服务拆分的已经够好了,代码写的更好了。这些问题可能不是问题。