在 Swing 应用程序启动期间,首次调用 JFrame 构造函数需要很长时间(因为 java.awt.Window())

2022-09-04 03:04:06

我正在尝试使用Java Swing构建一个简单,轻量级且响应迅速的应用程序。但是,当它启动时,在窗口(JFrame)出现之前有一个明显的延迟(>500ms)。

我已经把它追溯到java.awt.Window类的构造函数,它是JFrame的祖先。

奇怪的是,构造函数仅在第一次调用时速度较慢。如果我创建多个 JFrame 对象,则第一个对象在构造函数中花费的时间约为 600 毫秒,但对于后续对象,通常测量为 0 毫秒。

下面是一个简单的示例,在我的系统上,它显示了第一个构造函数调用的显著延迟,而不是第二个构造函数调用:

public static void main(String args[]) {
    java.awt.EventQueue.invokeLater(new Runnable() {
        public void run() {
            long start;

            start = System.currentTimeMillis();
            JFrame frame1 = new JFrame();
            System.out.println((System.currentTimeMillis() - start) + " for first JFrame.");

            start = System.currentTimeMillis();
            JFrame frame2 = new JFrame();
            System.out.println((System.currentTimeMillis() - start) + " for second JFrame.");
        }
    });
}

典型输出:

641 for first JFrame.
0 for second JFrame.

如果我在JFrame对象之前添加这个窗口对象初始化:

java.awt.Window window = new java.awt.Window(null);

然后,输出将更改为如下所示的内容:

578 for first Window.
47 for first JFrame.
0 for second JFrame.

当我尝试使用 Window 的超类 java.awt.Container 时,Window 构造函数仍然是需要很长时间才能执行的构造函数(因此问题不会超出 Window 类)。

由于 JFrame 构造函数调用 Window 构造函数,因此上述内容似乎表明对 Window 构造函数的第一次调用开销很大。

在对构造函数的第一次调用中发生了什么,这需要这么长时间,我能做些什么吗?是否有一些简单的解决方案,或者该问题是 Swing/AWT 的根本问题?或者它可能是特定于我的系统/设置的问题?

我希望我的应用程序打开的速度与MS记事本一样快(或几乎一样快),并且,虽然我可以在记事本打开时将文本打印到控制台(如果我在第一个JFrame初始化之前放置代码),但上述问题意味着在窗口可见之前几乎有整整一秒钟的延迟。我是否需要使用其他语言或 GUI 框架才能获得我想要的性能?


编辑:如果我添加 Thread.sleep(10000) 作为 run() 的第一行,结果不会改变(它们只是在 10 秒后出现)。这表明问题不是由某些异步启动代码引起的,而是由构造函数调用直接触发的。

编辑 2:实现 NetBeans Profiler 可以在 JRE 类内部进行剖析,并发现大部分时间都花在初始化 sun.java2d.d3d.D3DGraphicsDevice 对象(Window 对象需要屏幕边界和插图)上,该对象是 Java 6u10 中引入的“Microsoft Windows 平台的 Direct3D 加速渲染管道,默认启用”的一部分。.可以通过将“-Dsun.java2d.d3d=false”属性传递给JVM来禁用它,这确实将启动时间缩短了大约3/4,但我还不确定我是否需要它(D3D),或者是否有其他方法可以更快地加载它。如果我将该参数放在命令行上,则输出如下:

0 for first Window
47 for first JFrame.
0 for second JFrame.

我会回来清理这篇文章,我以后再深入挖掘。


答案 1

这个答案记录了我到目前为止的发现。如果有人有更多信息,请发表评论或发布答案。我对简单地禁用Swing对D3D的使用并不完全满意,并且对其他解决方案持开放态度。

原因:D3D 初始化

Swing 使用 Java2D API 进行绘制,根据此 Java SE 7 故障排除指南,Java2D 使用一组渲染管道,“可以粗略地定义为渲染基元的不同方式”。更具体地说,Java2D渲染管道似乎将跨平台Java代码连接到可能支持硬件加速的本机图形库(OpenGL,X11,D3D,DirectDraw,GDI)。

Java 1.6.0_10(又名 6u10)中,基于 Direct3D 的“完全硬件加速图形管道”被添加到 Java2D for Windows 中,以提高 Swing 和 Java2D 应用程序中的渲染性能(默认情况下启用)。

默认情况下,当在 Windows 系统上使用 Java2D 时,默认情况下会同时启用此 Direct3D 管道和 DirectDraw/GDI 管道(我假设它们各自用于不同的事情)。

至少,D3D库仅在需要时加载和初始化,并且在首次构造Window(或Window子体)时调用的本机D3D初始化函数需要大约500ms(对我来说)并导致报告的初始化速度缓慢,并且禁用D3D管道似乎消除了对此本机函数的调用, 显著缩短启动时间。(尽管我更喜欢延迟,预先计算,共享(跨不同的java应用程序)或优化D3D初始化,我想知道其他语言是否这么慢。

当然,在大多数系统上,在D3D init中花费的时间可以忽略不计,并且由于某些硬件或驱动程序问题,这只是我的系统上的问题,但我对此持怀疑态度(尽管如果这是真的,那将是一个简单的解决方案)。


详细描述到本机 initD3D() 的跟踪

更详细地说(如果您不关心,请跳过以下段落),我使用 Netbeans 探查器和调试器来查找:

当初始化 JFrame(调用构造函数)时,将调用祖先类 java.awt.Window 的构造函数。窗口初始化其图形配置设备,该设备尝试检索默认屏幕设备,依此类推。第一次发生这种情况时(初始化第一个窗口或窗口后代时),屏幕设备不存在,因此会构建它。在此过程中,sun.java2D.d3d.D3DGraphicsDevice 类被初始化,并且在其静态初始化块(参见 <clinit>()) 中,它调用了一个本机函数 initD3D(),这需要很长时间才能执行(约 500 毫秒)。

我能够找到D3DGraphicsDevice及其静态init块的源代码版本(我真的只是从这个源代码中假设initD3D()是使其<clinit>()花费如此之久的原因 - 我的分析器似乎不承认本机函数 - 但这是一个合理的猜测)。


一种解决方法 - 禁用 Java2D 的 D3D

D3D 管道可以通过运行带有该选项的 java 来禁用,如 Java2D“系统属性”指南(以及前面提到的故障排除指南)所示。我认为这禁用了D3D,但不能禁用DirectDraw,DirectDraw可以禁用(然后“所有操作都将使用GDI执行”),但这并没有显着改善初始化时间。-Dsun.java2d.d3d=falseDsun.java2d.noddraw=true

例如,我可能会使用如下所示的命令来运行 MyJar.jar而不运行 D3D:

java -jar -Dsun.java2d.d3d=false MyJar.jar

使用问题中发布的代码(初始化一个窗口,然后初始化2个JFrame对象),我得到的结果如下:

0 for first Window
47 for first JFrame.
0 for second JFrame.

而不是这样的结果:

547 for first Window
31 for first JFrame.
0 for second JFrame.

(请注意,时间以毫秒为单位,是使用Windows上的System.currentTimeMillis()测量的,我认为它的分辨率约为15到16毫秒。


OpenGL vs Direct3D

如果使用该选项,则使用 OpenGL 而不是 Direct3D。在我的系统上,有一个轻微的改进(OpenGL为约400ms,而D3D为~500ms),但延迟仍然很明显。-Dsun.java2d.opengl=True


其他延误

我注意到,第一个 JFrame 对象的初始化,即使它不是第一个 Window,也比后续 JFrame 对象的初始化花费更多的时间(记录为 31 到 47 毫秒与 0 毫秒)。

分析表明,这与创建第一个玻璃窗格(JPanel)有关,最终似乎是由 javax.swing.UIManager 类和对象初始化代码中的外观和感觉以及系统属性初始化/加载引起的。这不太重要,但它确实解释了观察到的异常。

在我的实际程序中,有点复杂(必须初始化更多的Swing组件),延迟似乎在Swing代码中分布得更分散,但我注意到大量的本机类加载,“UI安装”(加载默认UI属性等)等等。不幸的是,我认为这些方面没有太多可做的(如果有,请说出来)。


结束语

最后,能做的只有这么多,我必须认识到Swing和JVM在过去几年中已经走了多远。


答案 2

创建第一个 jframe 涉及加载 swing 库。JVM 不会加载库来查看 import 语句。仅当对该库进行第一次调用时,才会加载这些库。

在本例中,frame1 的创建是第一个调用 Swing 库的语句。在创建 frame2 实例时,swing 库已经加载,因此 frame2 的对象创建速度太快,甚至没有注意到一些时间的流逝。因此,它显示 0。

这就解释了为什么当您将 Window 语句加在两个语句之上时,它会显示 578、47、0。这是因为第一个语句需要时间来加载 java.awt 库。第二个需要时间来加载摆动库。三分之一显示0,因为创建其所需的库已加载。

您甚至可以通过这种方式对其进行测试。尝试将第二个 JFrame 创建替换为 JPanel,它仍然显示 0。


推荐