Swing应用程序启动时,调用JFrame构造函数需要很长时间(因为java.awt.Window()),请问如何解决?

14
我正在尝试使用Java Swing构建一个简单、轻量级和响应式的应用程序。然而,当它启动时,窗口(一个JFrame)出现前会有明显的延迟(>500ms)。
我已经追踪到这是由java.awt.Window类的构造函数引起的,该类是JFrame的祖先类。
奇怪的是,该构造函数仅对第一次调用较慢。如果我创建多个JFrame对象,则构造函数中花费的时间对于第一个对象约为600ms,但通常被测量为后续对象的0ms。
以下是一个简单的示例,在我的系统上,第一个构造函数调用显示了显着的延迟,但第二个构造函数没有。
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初始化之前,可以在Notepad打开时在控制台上打印文本,但上述问题意味着窗口可见之前几乎有一秒钟的延迟。我需要使用不同的语言或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
运行Windows XP SP3,JRE 1.6.0_07。AMD Athlon 64 X2 4200+ 2.19GHz,2GB RAM。虽然有点老,但通常速度还是够快的。 - A Coder
如果我必须猜测的话,我认为更多的内存会最有帮助。 - trashgod
我实际上安装的是JRE 1.6.0_26 - 更新版本为26,而不是7。Java Quick Start(JQS)是在更新版本10中引入的,并且默认情况下在我的计算机上运行。我已经升级到JRE 1.7.0,但问题仍然存在。 - A Coder
我对你的示例进行了更多的实验。JFrame.setDefaultLookAndFeelDecorated(true); 需要额外的35毫秒,frame1.pack();需要115毫秒,frame1.pack();需要24毫秒。 - Alexey
5个回答

10
这个答案记录了我目前所发现的。如果有更多信息,请评论或发布答案。我不满足于简单地禁用Swing对D3D的使用,并且欢迎其他解决方案。
原因:D3D初始化
Swing使用Java2D API进行绘图,根据Java SE 7故障排除指南,Java2D使用一组渲染管道,“可以粗略地定义为不同的基元渲染方式”。更具体地说,Java2D渲染管道似乎将跨平台Java代码连接到本机图形库(OpenGL、X11、D3D、DirectDraw、GDI),这些库可能支持硬件加速。
Java 1.6.0_10(也称为6u10)中,为Windows添加了基于Direct3D的“完全硬件加速图形管道”,以改善Swing和Java2D应用程序的渲染性能(默认情况下启用)。
默认情况下,在Windows系统上使用Java2D时,默认情况下同时启用此Direct3D管道和DirectDraw / GDI管道(我认为它们各自用于不同的事情)。
D3D库只有在需要时才会加载和初始化。当构建窗口(或窗口的后代)时,首次调用本机的D3D初始化函数需要大约500毫秒(对我来说),这导致了初始化速度慢的问题。禁用D3D管道似乎可以消除对该本机函数的调用,从而显着缩短启动时间。(虽然我更喜欢延迟、预计算、共享(跨不同的Java应用程序)或优化D3D初始化,并且我想知道其他语言是否也这么慢。)

尽管如此,在大多数系统上,D3D初始化所花费的时间可能是微不足道的,只有在我的系统中由于某些硬件或驱动程序问题才会出现问题,但我对此持怀疑态度(虽然,如果真的是这样,那将是一个容易解决的问题)。


详细描述追踪到本地的initD3D()

更详细地说(如果您不关心,请跳过以下段落),我使用Netbeans分析器和调试器发现:

当JFrame被初始化(构造函数被调用)时,祖先类java.awt.Window的构造函数也会被调用。Window初始化其GraphicsConfiguration设备,该设备尝试检索默认屏幕设备等。第一次发生这种情况(当第一个Window或Window后代被初始化时),屏幕设备不存在,因此它被构建。在此过程中,sun.java2D.d3d.D3DGraphicsDevice类得到初始化,在其静态初始化块中(请参见<clinit>()),它调用一个本地函数initD3D(),需要较长时间来执行(约500ms)。

我能够找到D3DGraphicsDevice源代码的版本及其静态初始化块(从这个源代码中,我只是假设initD3D()使其<clinit>()花费了如此长的时间——我的分析器似乎不承认本地函数——但这是一个合理的猜测)。


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

可以通过在运行Java时使用-Dsun.java2d.d3d=false选项来禁用D3D管道,参见Java2D“系统属性”指南(以及上述故障排除指南)。我认为这将禁用D3D但不会禁用DirectDraw,可以使用Dsun.java2d.noddraw=true来禁用DirectDraw(然后“所有操作都将使用GDI”),但是这并不能显著改善初始化时间。

例如,我可能会使用以下命令来运行没有D3D的MyJar.jar:

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

如果使用-Dsun.java2d.opengl=True选项,则会使用OpenGL而不是Direct3D。在我的系统上,使用OpenGL相比于使用D3D有轻微的改善(OpenGL大约需要400毫秒,而D3D需要大约500毫秒),但延迟仍然明显。


其他延迟

我注意到,即使它不是第一个窗口,第一个JFrame对象的初始化时间比后续JFrame对象的初始化时间要长得多(记录为31至47毫秒与0毫秒)。

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

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


结束语

最终,只有那么多事情可以做,我必须认识到Swing和JVM在过去几年中取得了多大的进步。


这个详细而准确的解释刚刚为我节省了数天时间去复制你的发现。原来我的第一帧需要这么长时间才能出现!谢谢。 - Steve McLeod

2
创建第一个jframe涉及加载swing库。JVM不会根据import语句加载库。只有当对该库的第一次调用发生时才会加载库。
在这种情况下,frame1的创建是第一个调用Swing库的语句。在创建frame2实例时,Swing库已经加载,因此即使注意到某些时间间隔也很快地创建了frame2对象。因此它显示为0。
这解释了为什么在上面两个Window语句相加时会显示578、47、0。这是因为第一个语句需要时间来加载java.awt库。第二个需要时间来加载Swing库。第三个显示为0,因为其创建所需的库已经加载。
您甚至可以通过以下方式进行测试。尝试将第二个JFrame创建替换为JPanel,它仍然显示为0。

1

我猜测它正在加载本地库。如果是这种情况,你无法做太多事情。


1

Mac OS X 10.5.8,Java 1.6.0_26。考虑到JVM启动时间和重量级图形对等体的创建,这并不意外。

第一JFrame为247。
第二JFrame为0。

附加说明:根据文章Java性能:启动时间Java Quick Starter可能在您的平台上可用。


那么所有这些都发生在对Window构造函数的第一次调用中吗? - A Coder
之前、期间和之后;我已经详细阐述过了。 - trashgod
请注意:例子是在 NetBeans IDE 中运行的,该 IDE 可能已经加载了一些 JVM 使用的共享库。 - trashgod

1

加载所有Swing类需要时间,然后加载本地AWT库也需要时间。也许,类加载需要更多时间,因为如果您只创建JLabel而不是第一个JFrame,它仍然需要更多时间。


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