Java在System.exit()之后仍然运行的原因是什么?

29
我有一个Java程序,通过另一个Java程序的ProcessBuilder启动。子程序调用System.exit(0),但是对于一些用户(在Windows上),与子程序关联的java.exe进程没有终止。子程序没有关闭挂钩(shutdown hooks),也没有可能阻止System.exit()终止VM的SecurityManager。我无法在Linux或Windows Vista上复现该问题。到目前为止,唯一报告这个问题的是两个Windows XP用户和一个Vista用户,他们使用两个不同的JREs(1.6.0_15和1.6.0_18),但每次都能够复现该问题。
有人可以提出JVM在System.exit()后未能终止,并且仅在某些机器上的原因吗?
编辑1:我让用户安装JDK,以便我们可以从有问题的VM中获取线程转储。用户告诉我的是,只要他单击我的菜单中的“退出”项,VM进程就会从VisualVM中消失---但是,根据Windows任务管理器的说法,该进程尚未终止,无论用户等待多长时间(几分钟、几小时),它都不会终止。
编辑2:我现在已经确认,父进程中的Process.waitFor()对于至少一个有问题的用户永远不会返回。因此,总结一下:子VM似乎已经死了(甚至VisualVM都看不到它),但父进程仍然将该进程视为活动状态,并且Windows也是如此。
9个回答

19

如果您的代码(或您使用的库)具有未能清理完成的关闭钩子或终结器,可能会发生这种情况。

一种更强力的(因此只应在极端情况下使用!)强制关闭的方法是运行:

Runtime.getRuntime().halt(0);

8

父进程有一个线程专门用于消耗每个子进程的STDOUT和STDERR(将输出传递给日志文件)。就我所看到的,它们正常工作,因为我们在日志中看到了所有预期的输出

当我处理stdout/stderr时,我的程序在任务管理器中消失的问题与此类似。在我的情况下,如果在调用system.exit()之前关闭正在监听的流,则javaw.exe会挂起。奇怪的是,它没有写入流中...

在我的情况下,解决方案是在退出之前仅刷新流而不是关闭它。当然,在退出之前,您可以始终先刷新,然后重新定向回标准输出和标准错误。


5

检查是否存在死锁。

例如:

Runtime.getRuntime().addShutdownHook(new Thread(this::close));
System.exit(1);

close()方法中

public void close() {
// some code
System.exit(1);
}
System.exit() 实际上调用了关闭挂钩和终结器。如果您的关闭挂钩在内部再次调用 exit(),出于某种原因(例如,当 close() 引发异常导致需要退出程序时),那么您也将在那里调用 exit()。就像这样...
public void close() {
  try {
  // some code that raises exception which requires to exit the program
  } catch(Exception exceptionWhichWhenOccurredShouldExitProgram) {
    log.error(exceptionWhichWhenOccurredShouldExitProgram);
    System.exit(1);
  }
}

虽然抛出异常是一个好的实践,但有些人可能选择记录日志并退出。

注意,如果发生死锁,则Ctrl+C也无法正常工作。因为它也调用了关闭钩子函数。

总之,如果是这种情况,可以通过以下解决方法解决问题:

private static AtomicBoolean exitCalled=new AtomicBoolean();

    private static void exit(int status) {
        if(!exitCalled.get()) {
            exitCalled.set(true);
            System.exit(status);
        }
    }

    Runtime.getRuntime().addShutdownHook(new Thread(MyClass::close));
      exit(1);
    }

    private static void close() {
        exit(1);
    }

附注:我认为上述的exit()版本实际上应该只在System.exit()方法中编写(或许适用于JDK的某些PR?)因为,从我所看到的至少没有任何意义去处理System.exit()中的死锁问题。


4
以下是一些情景...
根据http://java.sun.com/j2se/1.4.2/docs/api/java/lang/Thread.html中线程的定义:
当Java虚拟机启动时,通常会有一个非守护线程(通常调用某个指定类的main方法)。Java虚拟机继续执行线程,直到发生以下情况之一:
1)已经调用了Runtime类的exit方法,并且安全管理器允许退出操作。 2)所有非守护线程都已死亡,或者通过从run方法返回或抛出超出run方法范围的异常来死亡。
另一个可能性是如果已经调用了runFinalizersOnExit方法。根据http://java.sun.com/j2se/1.4.2/docs/api/java/lang/System.html中的文档,此方法已被弃用。这种方法本质上是不安全的。它可能导致在其他线程同时操作这些对象时,在活动对象上调用finalizers,从而导致不稳定的行为或死锁。 启用或禁用退出时的finalization;这样做指定要在Java运行时退出之前运行尚未自动调用的具有finalizers的所有对象的finalizers。默认情况下,退出时的终止被禁用。 如果存在安全管理器,则首先使用0作为其参数调用其checkExit方法以确保允许退出。这可能导致SecurityException。

我们没有任何对 runFinalizersOnExit 的调用,也没有 SecurityManager,所以我认为这些不是原因。 - uckelman

1

可能是一个糟糕编写的终结程序?当我看到主题时,我的第一反应是关机钩。猜测:如果一个线程捕获了InterruptedException并且仍然继续运行,会阻止退出过程吗?

在我看来,如果问题可以重现,您应该能够附加到JVM并获取线程列表/堆栈跟踪,显示了挂起的内容。

您确定子进程仍在运行,并且这不仅是未收割的僵尸进程吗?


如果它是一个终结器,那么它不是我们代码中的终结器——我们没有任何终结器。我确定子进程仍在运行,因为我们看到两个Java进程,用户系统上没有其他使用Java的程序。我有点希望避免要求他安装JDK以便我们可以使用jstack,但我认为现在这可能是最好的方法。 - uckelman
我所指的子进程是,它是否还有任何线程?它是否仍然响应输入?它是否仍然有打开的句柄?我对Unix比Windows更熟悉,但我知道在Unix操作系统中,除非父进程收割它们,否则进程不会清除,我认为在Windows上也是如此。仅仅因为它出现在top或TaskManager中并不意味着它仍然处于活动状态。当然,这并不能解释为什么它只发生在某些用户身上... - Chris Dolan
1
默认情况下,终结器不会在退出时运行;请参见http://java.sun.com/javase/6/docs/api/java/lang/System.html#runFinalizersOnExit(boolean)。 - Stephen C

1

父进程是否会消耗子进程的错误和输出流? 如果在某些操作系统下,子进程在stdout/stderr上打印出一些错误/警告信息而父进程没有消耗这些流,则子进程将被阻塞且无法达到System.exit();


父进程有一个线程专门用于消耗每个子进程的STDOUT和STDERR(将该输出传递到日志文件中)。就我所见,它们正常工作,因为我们在日志中看到了我们期望看到的所有输出。 - uckelman

1

你好,我遇到了同样的问题,但是我的原因是使用了远程调试(VmArgs: -Xdebug -Xrunjdwp:transport=dt_socket,address=%port%,server=y,suspend=y),当我禁用它后,java.exe进程按预期退出了。


0

我认为所有显而易见的原因都已经被暂时覆盖了,例如finalizers、shutdown hooks,在父进程中没有正确地排除标准输出/标准错误。现在你需要更多的证据来找出问题所在。

建议:

  1. 设置一个Windows XP或Vista机器(或虚拟机),安装相关的JRE和您的应用程序,并尝试重新复制该问题。一旦您可以重现该问题,要么附加调试器,要么发送相应的信号以获取线程转储到标准错误。

  2. 如果无法按照上述方式复制问题,请让您的用户获取线程转储,并将日志文件转发给您。


我在Vista上无法重现它,用户也无法从VisualVM中获取线程转储,因为虚拟机似乎已经不再运行。 - uckelman

0

这里没有提到的另一种情况是如果关闭挂钩卡在某个地方,因此在编写关闭挂钩代码时要小心(如果第三方库注册了一个也挂起的关闭挂钩)。

迪恩


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