我們線上的業(yè)務(wù) jar 包基本上普遍比較龐大,動(dòng)不動(dòng)一個(gè) jar 包上百 M,啟動(dòng)時(shí)間在分鐘級(jí),拖慢了我們?cè)诠收蠒r(shí)快速擴(kuò)容的響應(yīng)。于是做了一些分析,看看 Java 程序啟動(dòng)慢到底慢在哪里,如何去優(yōu)化,目前的效果是大部分大型應(yīng)用啟動(dòng)時(shí)間可以縮短 30%~50%
主要有下面這些內(nèi)容
- 修改 async-profiler 源碼,只抓取啟動(dòng)階段 main 線程的 wall 時(shí)間火焰圖( )
- 重新實(shí)現(xiàn) JarIndex( )
- 結(jié)合 JarIndex 重新自定義類加載器,啟動(dòng)提速 30%+( )
- SpringBean 加載耗時(shí) timeline 可視化分析( )
- SpringBean 的可視化依賴分析( )
- 基于依賴拓?fù)涞?SpringBean 的異步加載( )
無(wú)觀測(cè)不優(yōu)化
秉承著無(wú)觀測(cè)不優(yōu)化的想法,首先我們要知道啟動(dòng)慢到底慢在了哪里。我之前分享過(guò)很多次關(guān)于火焰圖的使用,結(jié)果很多人遇到問(wèn)題就開(kāi)始考慮火焰圖,但是一個(gè)啟動(dòng)慢其實(shí)是一個(gè)時(shí)序問(wèn)題,不是一個(gè) hot CPU 熱點(diǎn)問(wèn)題。很多時(shí)候慢,不一定是 cpu 占用過(guò)高,很有可能是等鎖、等 IO 或者傻傻的 sleep。
在 Linux 中有一個(gè)殺手級(jí)的工具 bootchart 來(lái)分析 linux 內(nèi)核啟動(dòng)的問(wèn)題,它把啟動(dòng)過(guò)程中所有的 IO、CPU 占用情況都做了詳細(xì)的劃分,我們可以很清楚地看到各個(gè)時(shí)間段,時(shí)間耗在了哪里,基于這個(gè) chart,你就可以看看哪些過(guò)程可以延后處理、異步處理等。
在 Java 中,暫時(shí)沒(méi)有類似的工具,但是又想知道時(shí)間到底耗在了哪里要怎么做呢,至少大概知道耗在了什么地方。在生成熱點(diǎn)調(diào)用火焰圖的時(shí)候,我們通過(guò) arthas 的幾個(gè)簡(jiǎn)單的命令就可以生成,它底層用的是 async-profiler 這個(gè)開(kāi)源項(xiàng)目,它的作者 apangin 做過(guò)一系列關(guān)于 jvm profiling 相關(guān)的分享,感興趣的同學(xué)可以去看看。
async-profiler 底層原理簡(jiǎn)介
async-profiler 是一個(gè)非常強(qiáng)大的工具,使用 jvmti 技術(shù)來(lái)實(shí)現(xiàn)。它的 NB 之處在于它利用了 libjvm.so 中 JVM 內(nèi)部的 API AsyncGetCallTrace 來(lái)獲取 Java 函數(shù)堆棧,精簡(jiǎn)后的偽代碼如下:
static bool vm_init(JavaVM *vm) { std::cout << "vm_init" << std::endl; // 從 libjvm.so 中獲取 AsyncGetCallTrace 的函數(shù)指針句柄 void *libjvm = dlopen("libjvm.so", RTLD_LAZY); _asyncGetCallTrace = (AsyncGetCallTrace) dlsym(libjvm, "AsyncGetCallTrace");}// 事件回調(diào)void recordSample(void *ucontext, uint64_t counter, jint event_type, Event *event) { std::cout << "Profiler::recordSample: " << std::endl; ASGCT_CallFrame frames[maxFramesToCapture]; ASGCT_CallTrace trace; trace.frames = frames; trace.env = getJNIEnv(g_jvm); // 調(diào)用 AsyncGetCallTrace 獲取堆棧 _asyncGetCallTrace(&trace, maxFramesToCapture, ucontext);}
你可能要說(shuō)獲取個(gè)堆棧還需要搞這么復(fù)雜,jstack 等工具不是實(shí)現(xiàn)得很好了嗎?其實(shí)不然。
jstack 等工具獲取函數(shù)堆棧需要 jvm 進(jìn)入到 safepoint,對(duì)于采樣非常頻繁的場(chǎng)景,會(huì)嚴(yán)重的影響 jvm 的性能,具體的原理不是本次內(nèi)容的重點(diǎn)這里先不展開(kāi)。
async-profiler 除了可以生成熱點(diǎn)調(diào)用的火焰圖,它還提供了 Wall-clock profiling 的功能,這個(gè)功能其實(shí)就是固定時(shí)間采樣所有的線程(不管線程當(dāng)前是 Running、Sleeping 還是 Blocked),它在文檔中也提到了,這種方式的 profiling 適合用來(lái)分析應(yīng)用的啟動(dòng)過(guò)程,我們姑且用這個(gè)不太精確的方式來(lái)粗略測(cè)量啟動(dòng)階段耗時(shí)在了哪些函數(shù)里。
但是這個(gè)工具會(huì)抓取所有的線程的堆棧,按這樣的方式抓取的 wall-clock 火焰圖沒(méi)法看,不信你看。
就算你找到了 main 線程,在函數(shù)耗時(shí)算占比的時(shí)候也不太方便,我們關(guān)心的其實(shí)只是 main 線程(也就是加載 jar 包,執(zhí)行 spring 初始化的線程),于是我做了一些簡(jiǎn)單的修改,讓 async-profiler 只取抓取 main 線程的堆棧。
重新編譯運(yùn)行
java -agentpath:/path/to/libasyncProfiler.so=start,event=wall,interval=1ms,threads,file=profile.html-jar xxx.jar
這樣生成的火焰圖就清爽多了,這樣就知道時(shí)間耗在了什么函數(shù)上。
接下來(lái)就是分析這個(gè) wall-clock 的火焰圖,點(diǎn)開(kāi)幾個(gè)調(diào)用棧仔細(xì)分析,發(fā)現(xiàn)很多時(shí)間花費(fèi)在類和資源文件查找和加載(挺失望的,java 連這部分都做不好)
繼續(xù)分析代碼看看類加載在做什么。
Java 垃圾般實(shí)現(xiàn)的類查找加載
Java 地類加載不出意外最終都走到了 java.net.URLClassLoader#findClass 這里。
這里的 ucp 指的是 URLClassPath,也就是 classpath 路徑的集合。對(duì)于 SpringBoot 的應(yīng)用來(lái)說(shuō),classpath 已經(jīng)在 META-INF 里寫(xiě)清楚了。
Spring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/
此次測(cè)試的程序 BOOT-INF/lib/ 有 300 多個(gè)依賴的 jar 包,當(dāng)加載某個(gè)類時(shí),除了 BOOT-INF/classes/ 之外 Java 居然要遍歷那 300 個(gè) jar 包去查看這些 jar 包中是否包含某個(gè)類。
我在 loader.getResource 上注入了一下打印,看看這些函數(shù)調(diào)用了多少次。
可以看到太喪心病狂了,加載一個(gè)類,居然要調(diào)用 loader.getResource 去 jar 包中嘗試幾百次。我就按二分之一 150 來(lái)算,如果加載一萬(wàn)個(gè)類,要調(diào)用這個(gè)函數(shù) 150W 次。
請(qǐng)忽略源碼中的 LookupCache 特性,這個(gè)特性看起來(lái)是為了加速 jar 包查找的,但是這個(gè)特性看源碼是一個(gè) oracle 商業(yè)版的才有的特性,在目前的 jdk 中是無(wú)法啟用的。(推測(cè),如果理解不對(duì)請(qǐng)告知我)
于是有了一些粗淺的想法,為何不告訴 java 這個(gè)類在那個(gè) jar 里?做索引這么天然的想法為什么不實(shí)現(xiàn)。
以下面為例,項(xiàng)目依賴三個(gè) jar 包,foo.jar、bar.jar、baz.jar,其中分別包含了特定包名的類,理想情況下我們可以生成一個(gè)索引文件,如下所示。
foo.jarcom/foo1com/foo2bar.jarcom/barcom/bar/barbarbaz.jarcom/baz
這就是我們接下來(lái)要介紹的 JarIndex 技術(shù)。
JarIndex 技術(shù)
其實(shí) Jar 在文件格式上是支持索引技術(shù)的,稱為 JarIndex,通過(guò) jar -i 就可以在 META-INF/ 目錄下生成 INDEX.LIST 文件。別高興的太早,這個(gè) JarIndex 目前無(wú)法真正起到作用,有下面幾個(gè)原因:
- INDEX.LIST 文件生成不正確,尤其是目前最流行的 fatjar 中包含 jar 列表的情況
- classloader 不支持(那不是白忙活嗎)
首先來(lái)看 INDEX.LIST 文件生成不正確的問(wèn)題,隨便拿一個(gè) jar 文件,使用 jar -i 生成一下試試。
JarIndex-Version: 1.0encloud-api_origin.jarBOOT-INFBOOT-INF/classesBOOT-INF/classes/comBOOT-INF/classes/com/encloud….META-INFMETA-INF/mavenMETA-INF/maven/com.encloudMETA-INF/maven/com.encloud/encloud-apiBOOT-INF/liborgorg/springframeworkorg/springframework/bootorg/springframework/boot/loaderorg/springframework/boot/loader/jarorg/springframework/boot/loader/dataorg/springframework/boot/loader/archiveorg/springframework/boot/loader/util
可以看到在 BOOT-INF/lib 目錄中的類索引并沒(méi)有在這里生成,這里面可是有 300 多個(gè) jar 包。
同時(shí)生成不對(duì)的地方還有,org 目錄下只有文件夾并沒(méi)有 class 文件,org 這一行不應(yīng)該在 INDEX.LIST 文件中。
第二個(gè)缺陷才是最致命的,目前的 classloader 不支持 JarIndex 這個(gè)特性。
所以我們要做兩個(gè)事情,生成正確的 JarIndex,同時(shí)修改 SpringBoot 的 classloader 讓其支持 JarIndex。
生成正確的 JarIndex
這個(gè)簡(jiǎn)單,就是遍歷 jar 包里的類,將其所在的包名抽取出來(lái)。SpringBoot 應(yīng)用有三個(gè)地方存放了 class:
- BOOT-INF/classes
- BOOT-INF/lib
- jar 包根目錄下 org/springframework/boot/loader
生成的時(shí)候需要考慮到上面的情況,剩下的就簡(jiǎn)單了。遍歷這些目錄,將所有的包含 class 文件的包名過(guò)濾過(guò)來(lái)就行。
大概生成的結(jié)果是:
JarIndex-Version: 1.0encloud-api.jar/BOOT-INF/classescom/encloudcom/encloud/app/controllercom/encloud/app/controller/v2/org/springframework/boot/loaderorg/springframework/boot/loader/archiveorg/springframework/boot/loader/dataorg/springframework/boot/loader/jarorg/springframework/boot/loader/util/BOOT-INF/lib/spring-core-4.3.9.RELEASE.jarorg/springframework/asmorg/springframework/cgliborg/springframework/cglib/beansorg/springframework/cglib/core/BOOT-INF/lib/guava-19.0.jarcom/google/common/annotationscom/google/common/basecom/google/common/base/internalcom/google/common/cache… other jar …
除了加載類需要查找,其實(shí)還有不少資源文件需要查找,比如 spi 等掃描過(guò)程中需要,順帶把資源文件的索引也生成一下寫(xiě)入到 RES_INDEX.LIST 中,原理類似,這里展開(kāi)。
自定義 classloder
生成了 INDEX.LIST 文件,接下來(lái)就是要實(shí)現(xiàn)了一個(gè) classloader 能支持一步到位通過(guò)索引文件去對(duì)應(yīng)的 jar 包中去加載 class,核心的代碼如下:
public class JarIndexLaunchedURLClassLoader extends URLClassLoader { public JarIndexLaunchedURLClassLoader(boolean exploded, Archive rootArchive, URL[] urls, ClassLoader parent) { super(urls, parent); initJarIndex(urls); // 根據(jù) INDEX.LIST 創(chuàng)建包名到 jar 文件的映射關(guān)系 } @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { Class loadedClass = findLoadedClass(name); if (loadedClass != null) return loadedClass; // 如果是 loader 相關(guān)的類,則直接加載,不用找了,就在 jar 包的根目錄下 if (name.startsWith(“org.springframework.boot.loader.”) || name.startsWith(“com.seewo.psd.bootx.loader.”)) { Class result = loadClassInLaunchedClassLoader(name); if (resolve) { resolveClass(result); } return result; } // skip java.*, org.w3c.dom.* com.sun.* ,這些包交給 java 默認(rèn)的 classloader 去處理 if (!name.startsWith(“java”) && !name.contains(“org.w3c.dom.”) && !name.contains(“xml”) && !name.startsWith(“com.sun”)) { int lastDot = name.lastIndexOf(‘.’); if (lastDot >= 0) { String packageName = name.substring(0, lastDot); String packageEntryName = packageName.replace(‘.’, ‘/’); String path = name.replace(‘.’, ‘/’).concat(“.class”); // 通過(guò) packageName 找到對(duì)應(yīng)的 jar 包 List loaders = package2LoaderMap.get(packageEntryName); if (loaders != null) { for (JarFileResourceLoader loader : loaders) { ClassSpec classSpec = loader.getClassSpec(path); // 從 jar 包中讀取文件 if (classSpec == null) { continue; } // 文件存在,則加載這個(gè) class Class definedClass = defineClass(name, classSpec.getBytes(), 0, classSpec.getBytes().length, classSpec.getCodeSource()); definePackageIfNecessary(name); return definedClass; } } } } // 執(zhí)行到這里,說(shuō)明需要父類加載器來(lái)加載類(兜底) definePackageIfNecessary(name); return super.loadClass(name, resolve); }}
到這里我們基本上就實(shí)現(xiàn)了一個(gè)支持 JarIndex 的類加載器,這里的改動(dòng)經(jīng)實(shí)測(cè)效果已經(jīng)效果非常明顯。
除此之外,我還發(fā)現(xiàn)查找一個(gè)已加載的類是一個(gè)非常高頻執(zhí)行的操作,于是可以在 JarIndexLaunchedURLClassLoader 之前再加一層緩存(思想來(lái)自 sofa-boot)
public class CachedLaunchedURLClassLoader extends JarIndexLaunchedURLClassLoader { private final Map classCache = new ConcurrentHashMap(3000); @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { return loadClassWithCache(name, resolve); } private Class loadClassWithCache(String name, boolean resolve) throws ClassNotFoundException { LoadClassResult result = classCache.get(name); if (result != null) { if (result.getEx() != null) { throw result.getEx(); } return result.getClazz(); } try { Class clazz = super.findLoadedClass(name); if (clazz == null) { clazz = super.loadClass(name, resolve); } if (clazz == null) { classCache.put(name, LoadClassResult.NOT_FOUND); } return clazz; } catch (ClassNotFoundException exception) { classCache.put(name, new LoadClassResult(exception)); throw exception; }}
注意:這里為了簡(jiǎn)單示例直接用 ConcurrentHashMap 來(lái)緩存 class,更好的做法是用 guava-cache 等可以帶過(guò)期淘汰的 map,避免類被永久緩存。
如何不動(dòng) SpringBoot 的代碼實(shí)現(xiàn) classloader 的替換
接下的一個(gè)問(wèn)題是如何不修改 SpringBoot 的情況下,把 SpringBoot 的 Classloader 替換為我們寫(xiě)的呢?
大家都知道,SpringBoot 的 jar 包啟動(dòng)類其實(shí)并不是我們項(xiàng)目中寫(xiě)的 main 函數(shù),其實(shí)是
org.springframework.boot.loader.JarLauncher,這個(gè)類才是真正的 jar 包的入口。
package org.springframework.boot.loader;public class JarLauncher extends ExecutableArchiveLauncher {public static void main(String[] args) throws Exception {new JarLauncher().launch(args);}}
那我們只要替換這個(gè)入口類就可以接管后面的流程了。如果只是替換那很簡(jiǎn)單,修改生成好的 jar 包就可以了,但是這樣后面維護(hù)的成本比較高,如果在打包的時(shí)候就替換就好了。SpringBoot 的打包是用 spring-boot-maven-plugin 插件
org.springframework.boot spring-boot-maven-plugin
最終生成的 META-INF/MANIFEST.MF 文件如下
$ cat META-INF/MANIFEST.MFManifest-Version: 1.0Implementation-Title: encloud-apiImplementation-Version: 2.0.0-SNAPSHOTArchiver-Version: Plexus ArchiverBuilt-By: arthurImplementation-Vendor-Id: com.encloudSpring-Boot-Version: 1.5.4.RELEASEImplementation-Vendor: Pivotal Software, Inc.Main-Class: org.springframework.boot.loader.JarLauncherStart-Class: com.encloud.APIBootSpring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Created-By: Apache Maven 3.8.5Build-Jdk: 1.8.0_332Implementation-URL: http://projects.spring.io/spring-boot/parent/enclo ud-api/
為了實(shí)現(xiàn)我們的需求,就要看 spring-boot-maven-plugin 這個(gè)插件到底是如何寫(xiě)入 Main-Class 這個(gè)類的,經(jīng)過(guò)漫長(zhǎng)的 maven 插件源碼的調(diào)試,發(fā)現(xiàn)這個(gè)插件居然提供了擴(kuò)展點(diǎn),可以支持修改 Main-Class,它提供了一個(gè) layoutFactory 可以自定義
org.springframework.boot spring-boot-maven-plugin repackage com.seewo.psd.bootx bootx-loader-tools 0.1.1
實(shí)現(xiàn)這個(gè)
package com.seewo.psd.bootx.loader.tools;import org.springframework.boot.loader.tools.*;import java.io.File;import java.io.IOException;import java.util.Locale;public class MyLayoutFactory implements LayoutFactory { private static final String NESTED_LOADER_JAR = “META-INF/loader/spring-boot-loader.jar”; private static final String NESTED_LOADER_JAR_BOOTX = “META-INF/loader/bootx-loader.jar”; public static class Jar implements RepackagingLayout, CustomLoaderLayout { @Override public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException { // 拷貝 springboot loader 相關(guān)的文件到 jar 根目錄 writer.writeLoaderClasses(NESTED_LOADER_JAR); // 拷貝 bootx loader 相關(guān)的文件到 jar 根目錄 writer.writeLoaderClasses(NESTED_LOADER_JAR_BOOTX); } @Override public String getLauncherClassName() { // 替換為我們自己的 JarLauncher return “com.seewo.psd.bootx.loader.JarLauncher”; } }}
接下來(lái)實(shí)現(xiàn)我們自己的 JarLauncher
package com.seewo.psd.bootx.loader;import java.net.URL;public class JarLauncher extends org.springframework.boot.loader.JarLauncher { @Override protected ClassLoader createClassLoader(URL[] urls) throws Exception { return new CachedLaunchedURLClassLoader(urls, getClass().getClassLoader()); } public static void main(String[] args) throws Exception { new JarLauncher().launch(args); }}
重新編譯就可以實(shí)現(xiàn)替換
$ cat META-INF/MANIFEST.MFManifest-Version: 1.0…Main-Class: com.seewo.psd.bootx.loader.JarLauncher…
到這里,我們就基本完成所有的工作,不用改一行業(yè)務(wù)代碼,只用改幾行 maven 打包腳本,就可以實(shí)現(xiàn)支持 JarIndex 的類加載實(shí)現(xiàn)。
優(yōu)化效果
我們來(lái)看下實(shí)際的效果,項(xiàng)目 1 稍微小型一點(diǎn),啟動(dòng)耗時(shí)從 70s 降低到 46s
第二個(gè) jar 包更大一點(diǎn),效果更明顯,啟動(dòng)耗時(shí)從 220s 減少到 123s
未完待續(xù)
其實(shí)優(yōu)化到這里,還遠(yuǎn)遠(yuǎn)沒(méi)有達(dá)到我想要的目標(biāo),為什么啟動(dòng)需要這么長(zhǎng)時(shí)間,解決了類查找的問(wèn)題,那我們來(lái)深挖一下 Spring 的初始化。
Spring bean 的初始化是串行進(jìn)行的,于是我先來(lái)做一個(gè)可視化 timeline,看看到底是哪些 Bean 耗時(shí)很長(zhǎng)。
Spring Bean 初始化時(shí)序可視化
因?yàn)椴粫?huì)寫(xiě)前端,這里偷一下懶,利用 APM 的工具,把數(shù)據(jù)上報(bào)到 jaeger,這樣我們就可以得到一個(gè)包含調(diào)用關(guān)系的timeline 的界面了。jaeger 的網(wǎng)址在這里:www.jaegertracing.io/
首先我們繼承 DefaultListableBeanFactory 來(lái)對(duì) createBean 的過(guò)程做記錄。
public class BeanLoadTimeCostBeanFactory extends DefaultListableBeanFactory { private static ThreadLocal parentStackThreadLocal = new ThreadLocal(); @Override protected Object createBean(String beanName, RootBeanDefinition rbd, Object[] args) throws BeanCreationException { // 記錄 bean 初始化開(kāi)始 Object object = super.createBean(beanName, rbd, args); // 記錄 bean 初始化結(jié)束 return object; }
接下來(lái)我們實(shí)現(xiàn) ApplicationContextInitializer,在 initialize 方法中替換 beanFactory 為我們自己寫(xiě)的。
public class BeanLoadTimeCostApplicationContextInitializer implements ApplicationContextInitializer, Ordered { public BeanLoadCostApplicationContextInitializer() { System.out.println(“in BeanLoadCostApplicationContextInitializer()”); } @Override public void initialize(ConfigurableApplicationContext applicationContext) { if (applicationContext instanceof GenericApplicationContext) { System.out.println(“BeanLoadCostApplicationContextInitializer run”); BeanLoadTimeCostBeanFactory beanFactory = new BeanLoadTimeCostBeanFactory(); Field field = GenericApplicationContext.class.getDeclaredField(“beanFactory”); field.setAccessible(true); field.set(applicationContext, beanFactory); } }}
接下來(lái)將記錄的狀態(tài)上報(bào)到 jaeger 中,實(shí)現(xiàn)可視化堆棧顯示。
public void reportBeanCreateResult(BeanCreateResult beanCreateResult) { Span span = GlobalTracer.get().buildSpan(beanCreateResult.getBeanClassName()).withStartTimestamp(beanCreateResult.getBeanStartTime() * 1000).start(); try (Scope ignore = GlobalTracer.get().scopeManager().activate(span)) { for (BeanCreateResult item : beanCreateResult.getChildren()) { Span childSpan = GlobalTracer.get().buildSpan(item.getBeanClassName()).withStartTimestamp(item.getBeanStartTime() * 1000).start(); try (Scope ignore2 = GlobalTracer.get().scopeManager().activate(childSpan)) { printBeanStat(item); } finally { childSpan.finish(item.getBeanEndTime() * 1000); } } } finally { span.finish(beanCreateResult.getBeanEndTime() * 1000); }}
通過(guò)這種方式,我們可以很輕松的看到 spring 啟動(dòng)階段 bean 加載的 timeline,生成的圖如下所示。
這對(duì)我們進(jìn)一步優(yōu)化 bean 的加載提供了思路,可以看到 bean 的依賴關(guān)系和加載耗時(shí)具體耗在了哪個(gè) bean。通過(guò)這種方式可以在 SpringBean 串行加載的前提下,把 bean 的加載盡可能的優(yōu)化。
SpringBean 的依賴分析
更好一點(diǎn)的方案是基于 SpringBean 的依賴關(guān)系做并行加載。這個(gè)特性 2011 年前就有人提給了 Spring,具體看這個(gè) issue:github.com/spring-proj…
就在去年,還有人去這個(gè) issue 下去恭祝這個(gè) issue 10 周年快樂(lè)。
做并行加載確實(shí)有一些難度,真實(shí)項(xiàng)目的 Spring Bean 依賴關(guān)系非常復(fù)雜,我把 Spring Bean 的依賴關(guān)系導(dǎo)入到 neo4j 圖數(shù)據(jù)庫(kù),然后進(jìn)行查詢
MATCH (n)RETURN n;
得到的圖如下所示。一方面 Bean 的數(shù)量特別多,還有復(fù)雜的依賴關(guān)系,以及循環(huán)依賴。
基于此依賴關(guān)系,我們是有機(jī)會(huì)去做 SpringBean 的并行加載的,這部分還沒(méi)實(shí)現(xiàn),希望后面有機(jī)會(huì)可以完整的實(shí)現(xiàn)這塊的邏輯,個(gè)人感覺(jué)可以做到 10s 內(nèi)啟動(dòng)完一個(gè)超大的項(xiàng)目。
Java 啟動(dòng)優(yōu)化的其它技術(shù)
Java 啟動(dòng)的其它技術(shù)還有 Heap Archive、CDS,以及 GraalVM 的 AOT 編譯,不過(guò)這幾個(gè)技術(shù)目前都有各自的缺陷,還無(wú)法完全解決目前我們遇到的問(wèn)題。
后記
這篇文章中用到的技術(shù)只是目前比較粗淺的嘗試,如果大家有更好的優(yōu)化,可以跟我交流,非常感謝。
作者:挖坑的張師傅鏈接:https://juejin.cn/post/7117815437559070734