退出Swing应用程序时偶尔出现InterruptedException。

27

我最近把电脑升级成了一台更加强大的机器,它拥有一个四核超线程处理器(i7),因此具备充足的并发性能。现在我正在开发一个带有Swing GUI的应用程序,有时在退出应用程序(使用System.exit(0))时会出现以下错误:

Exception while removing reference: java.lang.InterruptedException
java.lang.InterruptedException
        at java.lang.Object.wait(Native Method)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:118)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:134)
        at sun.java2d.Disposer.run(Disposer.java:125)
        at java.lang.Thread.run(Thread.java:619)

显然,由于它开始在更具并发性能的硬件上出现,并且涉及线程,并且偶尔发生,所以这显然是某种计时问题。但问题在于堆栈跟踪非常简短。我只有上面的列表。它根本没有包括我的代码,因此很难猜测错误在哪里。

有人遇到过类似的情况吗?有什么想法可以开始解决吗?

编辑:由于使用 System.exit(0) 退出 Swing 应用程序可能会导致 "不干净",但我不想将主框架设置为 EXIT_ON_CLOSE,因为我希望确保应用程序退出时没有任何关键操作正在进行,因此我添加了一个机制,在调用 System.exit(0) 之前执行主框架的 dispose() 方法。所以现在应该很干净了,但偶尔还是会出现异常。这发生在调用 System.exit(0) 后;dispose() 没有任何问题。也就是说,它必须来自关闭挂钩:

mainFrame.dispose(); // No problem! After this returns, all visible GUI is gone.
// In fact, if there were no other threads around, the VM could terminate here.
System.exit(0); // Throws an InterruptedException from sun.java2d.Disposer.run

我甚至尝试通过循环遍历Window.getWindows()数组(其中包含没有所有者的Dialog等)显式地处理所有Window,但并没有什么区别。这个问题似乎与“cleanliness”(即在退出之前显式释放本机屏幕资源)关系不大。它是其他一些原因,但是什么原因呢?

编辑2:将默认关闭操作设置为EXIT_ON_CLOSE没有任何区别。在http://www.google.com/search?q=sun.java2d.Disposer.run(Disposer.java:125)上找到了一些错误报告,因此可能确实是Sun的Java2D实现中的一个错误。我可以想象,像这样的错误可能会长时间未被修复,因为它们在实践中相当无害;来自关闭挂钩的异常几乎不会伤害任何其他人。鉴于这发生在GUI应用程序中,除非将stderr定向到控制台或日志,否则甚至不会注意到异常。

17个回答

16

您的Disposer在调用remove()方法时被阻塞(正在删除下一个平台本地资源)。这意味着当VM通过System.exit()终止时,清理线程(守护线程)不会自然关闭(这是您应该预期的情况)。

您的应用程序中有一个非守护线程,它会阻止VM在所有swing窗口被处理掉后退出。

解决方案: 找到该线程并让其退出。

通常情况下,如果所有swing窗口都被处理掉,例如,此程序将弹出一个窗口,然后在关闭它后退出(所有操作都不需要调用System.exit())。

public static void main(String args[]) throws Exception {
    JFrame jf = new JFrame();
    jf.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    jf.setVisible(true);
}

退出前,你也可以试着运行垃圾回收器,只是为了好玩。


是的,我确实有数十个线程在运行,可能有一些没有被优雅地关闭。但如果其中一些线程正在阻塞Java2D资源,我会感到惊讶;我很清楚这些资源必须通过它们特殊的线程访问,并且相当确定我正在这样做。但是...找出这个烦人的线程的唯一方法是遍历所有线程吗?我甚至无法控制它们中的所有线程,因为许多线程是由第三方库创建的。堆栈跟踪如此短是一种痛苦。 - Joonas Pulakka
并不是你阻塞了Java2D。清理器线程“被阻塞”在一个引用队列上(这是它应该做的事情)。你的非守护线程防止了虚拟机正常关闭(当VM中的所有线程都被标记为守护线程时会发生自然关闭)。在调试器中查看哪些非守护线程正在运行,看看是否有办法停止它们。如果它们是第三方线程(没有stop()方法),那么你将无能为力。 - Justin
2
谢谢,通过调试器查看线程确实为这个问题提供了一些线索。似乎与一个名为“Swing-Shell”的线程有关,该线程由JFileChooser创建。确实,当我不打开JFileChooser时,应用程序在关闭时从未崩溃过。Sun bugs 6744953、6741890和6713352报告了一些与该线程有关的问题。如果我能够找到可重现的演示案例,我将进一步调查并提交错误报告。 - Joonas Pulakka

5
如果您正在使用Swing应用程序,则首先调用System.gc()方法,然后再调用dispose()方法。我认为这样会很好用。我也使用过这个方法。
我想点赞,但是我需要更多的声望。这个解决方案对我有用,尽管我找不到解释原因,我的同事也说它没有意义。
我使用1.7版本和一个Swing应用程序,它读取一个文件,重新排列内容,然后输出到一个文件。它有一个运行和退出按钮,使用偏好API、writer、reader和其他一些东西。如果不使用System.gc()方法,在打开和关闭应用程序时,仅连续两次会返回相同的异常,但在调用dispose()方法之前使用System.gc()方法后,我无法再次引发异常。

2

我想我已经找到了bug的来源!

当你有一个基本的Java应用程序,其中包含文件菜单中的退出子菜单时... 有一个快捷方式可以激活退出操作...这个快捷方式必须创建一个循环链接或类似的东西...

以下是解决方案:

首先,这是可选的,以便获得所有清晰: 从你的主窗口实现"WindowListener"接口。

在构建这个主窗口时执行以下操作:

JFrame frame=this.getFrame();
if(frame!=null)
{
   Window[] windows=frame.getWindows();
   for(Window window : windows)
   window.addWindowListener(this);
}

实现接口中的函数:

public void windowOpened(WindowEvent e) {}

public void windowClosing(WindowEvent e) {
    JFrame frame=this.getFrame();
    if(frame!=null)
    {
        Window[] windows=frame.getOwnedWindows();
        for(Window window : windows)
        {
            window.removeWindowListener(this);
            window.dispose();
        }
    }
    //clear();
    System.gc();
}

public void windowClosed(WindowEvent e) {}
public void windowIconified(WindowEvent e) {}
public void windowDeiconified(WindowEvent e) {}
public void windowActivated(WindowEvent e) {}
public void windowDeactivated(WindowEvent e) {}

之后,您需要注意您的快捷方式!您需要从“退出”菜单中删除该操作,以便使用actionPerformed进行管理:

private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) {
    //this.clear();
    svExitMenuItem.removeActionListener(null);//this call removes the end error from this way to exit.
    javax.swing.ActionMap actionMap = org.jdesktop.application.Application.getInstance(etscampide.ETScampIDEApp.class).getContext().getActionMap(ETScampIDEView.class, this);
    actionMap.get("quit").actionPerformed(null);//null to avoid end error too
}

我不解释为什么,但这对我有效...我认为存在一种循环引用的情况... 希望能帮到你。 再见。


2

使用System.exit()可能不是关闭基于Swing的应用程序最干净的方式。

您不能将主框架设置为EXIT_ON_CLOSE吗:

mainFrame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE )

那么将其设置为不可见吗?
相关问题请查看此处:Java中的System.exit(0)

1
实际上,我已将默认关闭操作设置为DO_NOTHING,以防止在发生关键事件时关闭应用程序。我正在侦听windowClosing事件,并仅在可以这样做时退出应用程序。现在我看JFrame的源代码,EXIT_ON_CLOSE实际上并没有做什么花哨的事情;它只是System.exit(0)。 - Joonas Pulakka
1
抱歉,它并不完全正确。实际上,EXIT_ON_CLOSE 在 System.exit(0) 之前调用 Window#processWindowEvent(WindowEvent)。它反过来调用了一个叫做 "AWTEventMulticaster" 的东西,这听起来确实可能有助于清理关闭。嗯,也许我可以在不实际使用它的情况下模拟 EXIT_ON_CLOSE 的行为...? - Joonas Pulakka
发现这个可能提供一个不错的替代方案:http://blog.srikanths.net/2009/01/on-never-using-systemexit.html - tim_yates
我刚刚尝试了这种方法,但在1.6.0_24版本中仍然遇到了上述异常。这是个聪明的想法,但似乎对我不起作用。 - Luke

2

我想知道这是否与以下错误有关:

Java Bug 6489540

基本上,这意味着如果存在InheritableThreadLocal对象或自定义contextClassLoader时使用Java2D,则它们将被捕获并永久存在,从而导致内存泄漏和可能出现此类奇怪的锁定问题。

如果是这种情况,解决方法是在主线程上触发一个Java2D操作(即在应用程序启动时立即执行),以便DisposerThread处于“干净”的环境中运行。它由静态初始化器启动,因此只需加载java2d资源的虚拟内容并释放它即可获得一个不会保留不必要内容的DisposerThread。


有趣的错误,但在这种情况下没有“奇怪”的锁定,OP 没有看到任何内存泄漏问题。他的问题是 swing 和 VM 之间的简单关闭竞争条件。 - Justin

2

我有同样的问题(Java 6)。 我在调试器中发现一个未知的sun.awt.image.ImageFetcher线程。 我认为这是因为我在使用带有图标的JButtons。 ImageFetcher线程在2秒后消失。 如果ImageFetcher线程没有运行,我的程序就可以正常退出。


1

有时候还是会出现错误,但这似乎运行良好:

private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) {
    svExitMenuItem.removeActionListener(null);
    windowClosing(null);//i add it to clear and call the garbadge collector...
    javax.swing.ActionMap actionMap = org.jdesktop.application.Application.getInstance(etscampide.ETScampIDEApp.class).getContext().getActionMap(ETScampIDEView.class, this);
    actionMap.get("quit").actionPerformed(null);
}

1

听起来你有一个线程在退出时没有终止。特别是,该线程正在等待(wait()),如果您在其运行时尝试停止该线程,则该方法会抛出中断异常。我总是将后台线程设置为守护进程(Daemons),这也可能有所帮助。

在你的JFrame中,我会做以下操作:

myJFrame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
addWindowListener(new WindowAdapter() {
  public void windowClosing(WindowEvent we) {
    // Some pieces of code to instruct any running threads, possibly including
    // this one, to break out of their loops

    // Use this instead of System.exit(0) - as long as other threads are daemons,
    // and you dispose of all windows, the JVM should terminate.
    dispose();
  }
});

通过手动跳出循环,您允许等待方法终止(希望您不要等待太长时间,对吧?如果是这样,您可能需要重新检查如何使用线程),然后到达代码中安全的断点 - 安全是因为您编写了代码来这样做 - 然后线程将终止,让应用程序正常终止。
更新 也许您在某个地方不适当地使用了事件分派线程,并且在尝试退出时它正在等待/仍在工作?调度程序线程应该尽可能少地工作,并且应该尽快将任何复杂的内容传递给另一个线程。我承认我有点盲目地瞎猜,但我倾向于认为这不是一个错误,特别是考虑到您开始在更强大的机器上注意到它 - 对我来说这意味着“竞争条件!”,而不是Java bug。

我已经在执行dispose操作。但是也许我应该检查一下应用程序中的所有线程,并以更加受控的方式关闭其中的一些线程。尽管如此,在堆栈跟踪中看到“sun.java2d.disposer”仍然感觉很奇怪。我非常确定我没有在事件分派线程之外访问任何图形资源。 - Joonas Pulakka

1
我曾经遇到过同样的问题,后来发现是因为有一个Frame在后台隐藏(可见性设置为false)。如果我确保在隐藏Frame上调用.dispose(),然后在主Frame上再次调用.dispose(),那么就没有必要调用System.exit(0)了,应用程序会自动清理并关闭。我的代码是Scala,但这个思路是一样的。
def top = new MainFrame {

   import javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE
   peer.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE)
   override def closeOperation() { endExecution; PreviewFrame.dispose(); this.dispose(); }
}

0

我遇到了类似的错误,但是不知怎么就解决了:

private static void createAndShowGUI() {
  JFrame jf = new MyProgram();
  jf.setVisible(true);
}

public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {

        public void run() {
          createAndShowGUI();
        }
    });
}

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