OpenJdk初始启动时间非常缓慢。

6
我在服务器上运行openjdk 11.0.3。每次服务器重启后(每晚都会),用户在首次启动我的应用程序时必须等待35秒才能开始使用。(在main方法中第一次System.out.println输出之前。)(但随后的启动非常快) 我尝试了以下选项来调试此问题:
-Xlog:class+load:file=classload.txt

以下是最重要的发现:
... 
[2.284s][info][class,load] jdk.internal.loader.URLClassPath$FileLoader source: jrt:/java.base
[5.032s][info][class,load] sun.security.rsa.RSASignature$SHA1withRSA source: jrt:/java.base
…
[5.051s][info][class,load] java.util.LinkedList$Node source: jrt:/java.base 
[8.121s][info][class,load] pos.LFChangeable source: file:/C:/Users/rho/AppData/Roaming/edapp/pos.jar
…
[8.135s][info][class,load] java.io.FileNotFoundException source: jrt:/java.base
[10.584s][info][class,load] sun.reflect.misc.ReflectUtil source: jrt:/java.base
…
[11.744s][info][class,load] java.security.NoSuchAlgorithmException source: jrt:/java.base
[34.853s][info][class,load] jdk.internal.logger.DefaultLoggerFinder source: jrt:/java.base

为什么在加载java.security.NoSuchAlgorithmException和jdk.internal.logger.DefaultLoggerFinder时会停顿23秒?而其他减速的时间呢?
编辑: 根据评论,我来澄清一些事情。 这是一个Windows远程桌面服务器。 实际上,不止一个服务器,但问题在所有服务器上都存在。 该应用程序是一个独立的应用程序。 因此,每天早上都会出现问题,因为尝试启动应用程序的用户会在“没有反应”时多次尝试启动它。 我现在已经尝试了多次重新启动其中一个服务器,以下是我的发现:
在重新启动后使用java11启动我的应用程序时,在第一个System.out.println之前需要平均40秒。然后,仅需要1-2秒钟,我的第一个JFrame就会显示出来。 在重启后使用java8(sun)启动我的应用程序需要平均16秒,直到第一个System.out.println。但是,我会在我的第一个JFrame显示之前等待25秒钟。 在先使用java8启动后再使用java11启动我的应用程序需要平均4-6秒钟。

1
如果后续启动很快,我就不会太担心Java应用程序在做什么。很可能你正在等待磁盘I/O。也许磁盘已经休眠并需要旋转起来,这可能需要10-15秒钟?然后对于随后的运行,文件被缓存。也许你可以通过在重新启动后主动启动应用程序来解决这个问题(https://www.cyberciti.biz/faq/linux-execute-cron-job-after-system-reboot)? - ewramner
你正在使用哪个服务器?猜测一下:你是否在某个地方使用了SecureRandom?在服务器上,您可能缺少熵,这可能解释了为什么第一次启动如此缓慢。(对于Tomcat,请参见https://wiki.apache.org/tomcat/HowTo/FasterStartUp#Entropy_Source) - Joachim Rohde
1
在应用程序启动期间获取一些堆栈跟踪(jstack、jcmd等)可能会提供一些见解。虚拟机可能在<100ms内启动,因此35s的启动时间表明其他事情正在发生。Joachim的建议是熵源可能是一个好猜测,而堆栈跟踪可能会揭示更多信息。 - Alan Bateman
2个回答

5
你的应用程序可能缺少“类数据共享(CDS)归档文件”。这样的归档文件允许更快地加载标准类,并且默认情况下已由某些旧版本的安装程序生成,但是OpenJDK 11没有安装程序。 JEP 341解决了这个问题:
目前,JDK镜像包括在构建时生成的默认类列表,位于lib目录中。即使只使用JDK提供的默认类列表,想要利用CDS的用户也必须作为额外步骤运行java -Xshare:dump。尽管该选项已有说明文档,但许多用户并不知道它。
因此,尽管这个JEP是关于JDK 12自动执行必要步骤的,但它也提到了JDK 11的修复方法:只需在命令行上运行一次java -Xshare:dump来生成归档文件。
请注意,您可以通过将应用程序类包含在CDS中进一步提高启动时间。请参阅JDK 11文档中的 Class Data Sharing部分。

1
我已经尝试了java -Xshare:dump,但不幸的是,它似乎并没有显著加速。jdk.internal.logger.DefaultLoggerFinder仍需要33.285秒。 - runholen
在使用-Xshare:dump运行后,您可以使用-Xshare:on来强制使用共享功能,以确保它没有因某种原因而回退到不使用它的状态。您还可以查看链接的文章并使用-XX:SharedClassListFile来包含更多的类。除此之外,仍然有可能涉及到除类加载之外的其他活动。也许,NoSuchAlgorithmException确实被抛出,有人捕获它并执行了漫长的操作,然后再尝试记录它(这会导致加载DefaultLoggerFinder)。 - Holger

1
我已经进行了全面测试,并准备发布我的结果,以及我制作的2个不同的“解决方案”。 首先,让我简单介绍一下我的应用程序。它是一个Swing企业应用程序,始于13年前,并一直扩展至今。 因此,该应用程序很大,完成了许多不同的任务(尽管大多数用户只使用其中的一部分),并且在其类路径中包括大约120个JAR文件,包括所有第三方JAR文件。 如前所述,在服务器重新启动后,我的第一个登录JFrame显示需要35秒。
解决方案1: 这是我的第一个解决方案,不是解决慢启动问题,而是解决用户不会启动多个应用程序实例的问题。 我注意到,虽然我的应用程序在第一次启动时非常慢,但其他应用程序并非如此。 因此,一个解决方法是制作一个小型的独立应用程序来显示闪屏,我在程序中像这样启动它:
splashProcess = Runtime.getRuntime().exec("javaw -jar splash.jar");

后来我只是用

结束了它。
splashProcess.destroy();

请注意,如果我使用新的JFrame()创建闪屏屏幕,则需要通常的35秒才能显示。
解决方案2: 在测试过程中,我发现我可以通过删除所有的jar文件并将它们复制回来来模拟重新启动。 除了减少测试时间外,我还发现只使用4-5个jar文件来启动应用程序非常快(尽管稍后会导致ClassNotFoundExceptions), 这也意味着我可以尝试找出哪个jar文件导致了挂起,方法是先复制所有jar文件,然后逐个省略一个或更多。 然而,我发现不是一个jar文件有问题。每次我删除一些jar文件之前,启动应用程序所需的时间都会稳步减少一点。 因此,似乎问题是当我在应用程序中第一次调用new JFrame()时,Java似乎要构建某种索引或类路径中所有类的某种东西,尽管此时它们未被使用。 我不知道为什么会这样,但是这个过程需要在类路径上有120个jar文件时花费相当长的时间。 这使我想到了解决方案2。现在,当我的应用程序启动时,我会检查是否存在参数“startSilent”。 如果存在,则我的应用程序所做的唯一事情就是显示一个大小为0,0的新JDialog,然后调用System.exit(0); 然后,我编写了一个脚本,该脚本使用“startSilent”参数运行我的应用程序,并在用户登录时启动。 现在,如果用户登录到服务器并等待至少35秒才启动我们的应用程序,启动就会非常快,因为应用程序已经启动并退出了一次,所以“类路径索引”或无论它是什么都已经建立。 如果用户在较短的时间内启动应用程序,则启动时间将减少静默脚本已经运行的时间。 (而且启动始终至少比以前快得多,因为脚本在桌面准备好之前就开始了)。
这是我研究的结果。希望其他人会发现它们有用。如果有人能解释为什么我所谓的“类路径索引”被创建成这样,那将不胜感激。请保留 HTML 标签。

感谢您的跟进。在JDK-8>11之后,我也遇到了类似的症状,但不是很严重,所以忍受了几年。启动JFrame后,它会保持空白2-3秒钟才开始绘制。我们有一个后台线程使用JNI加载32个.DLL(数学求解器),并查询和卸载它们。在本地方法调用中看到了三个约800毫秒的延迟。完全可重复,但如果删除了DLL,则延迟会移动到另一个DLL上。感觉Java正在“停止世界”进行簿记或其他操作。更改GC(Parallel vs G1GC)或堆设置对其没有影响。使用JDK8,所有内容都在100ms内加载完成。 - Luke Usherwood
简单的解决方法是将此线程推迟到用户不太可能注意到的时间点,但了解根本原因仍然很重要。 - Luke Usherwood

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接