You learn something new about the JDK every day.
Apparantly, System.exit(0)
does not always stop the JVM!
System.exit()
This is a great Java puzzler from Peter Lawrey:
public static void main(String... args) { Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { @Override public void run() { System.out.println("Locking"); synchronized (lock) { System.out.println("Locked"); } } })); synchronized (lock) { System.out.println("Exiting"); System.exit(0); } }
What does the code do?
- Our code registers the shutdown hook
- Our code acquires the lock
- Our code prints "Exiting"
- Our code calls System.exit(0)
- System.exit(0) calls our shutdown hook
- Our shutdown hook prints "Locking"
- Our shutdown hook tries to acquire the lock
- Deadlock - Code never exits
Clearly, calling System.exit(0)
and not exiting is a Bad Thing, although hopefully
badly written shutdown hooks are rare. And there are also deprecated runFinalizersOnExit
,
another potential source of problems.
What are the alternatives?
The System.exit(0)
call simply calls Runtime.getRuntime().exit(0)
, so that makes no difference.
The main alternative is Runtime.getRuntime().halt(0)
, described as "Forcibly terminates the currently running Java virtual machine".
This does not call shutdown hooks or exit finalizers, it just exits.
But what if you want to try and exit nicely first, and only halt if that fails?
Well that seems like its a missing JDK method. However, a delay timer can be used to get a reasonable approximation:
/** * Exits the JVM, trying to do it nicely, otherwise doing it nastily. * * @param status the exit status, zero for OK, non-zero for error * @param maxDelay the maximum delay in milliseconds */ public static void exit(final int status, long maxDelayMillis) { try { // setup a timer, so if nice exit fails, the nasty exit happens Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { Runtime.getRuntime().halt(status); } }, maxDelayMillis); // try to exit nicely System.exit(status); } catch (Throwable ex) { // exit nastily if we have a problem Runtime.getRuntime().halt(status); } finally { // should never get here Runtime.getRuntime().halt(status); } }
All things being equal, that really should exit the JVM in the best way it can.
In the puzzler, if you replace System.exit(0)
with a call to this method,
the deadlock will be broken and the JVM will exit.
Summary
System.exit(0)
does not always stop the JVM.
Runtime.getRuntime().halt(0)
should always stop the JVM.
Final thought - if you're writing code to exit the JVM, have you considered whether you even need that code? After all, exiting the JVM doesn't play well in embedded or cloud environments...
Don't forget that invoking exit/halt can throw an instance of SecurityException. Perhaps this is partly why invocations of exit/halt are not considered for the unreachable-statement analysis performed by the compiler, despite the fact that such invocations never complete normally. In any case, the deadlock behavior of the puzzler goes against the warnings in the documentation of Runtime.addShutdownHook(Thread).
ReplyDeleteHi, nice one! Are there any chances to reach the finally block? I want to use this code and stripped it down to this:
ReplyDeletepublic static void exit(int status, long timeout) {
final Runtime runtime = Runtime.getRuntime();
try {
Timers.schedule(() -> runtime.halt(status), timeout);
runtime.exit(status);
} catch (Throwable x) {
runtime.halt(status);
}
}
For scheduling a TimerTask I wrote a convenience method (I'm missing such ones in the Jdk8):
public final class Timers {
private Timers() {
throw new AssertionError(Timers.class.getName() + " cannot be instantiated.");
}
public static Timer schedule(Runnable task, long delay) {
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
task.run();
}
}, delay);
return timer;
}
}
I suspect that the SecurityException would be one case where the finally block is reached.
DeleteI'd agree that the JDK could do with a method for invoking Timer more easily.
thx!
ReplyDeleteMaybe I just do this out of habit/paranoia, but following LCK00-J guidelines would prevent the deadlock because the Runnable instance would have its own lock object... Nevertheless, chalk this up as another example of how giving people different ways to do the same thing is not always a good idea.
ReplyDeletehow about remove shutdown hook before exit(-1) ? would that work?
ReplyDeleteRe your claim: "Final thought - if you're writing code to exit the JVM, have you considered whether you even need that code? After all, exiting the JVM doesn't play well in embedded or cloud environments..."
ReplyDelete... I don't know about embedded environments. In the cloud, I think `Runtime.halt()` is a great idea.
In the cloud, your JVM might disappear entirely at any instant. You code as though any line of code might be the last one executed. In that context, `Runtime.halt()` is sensible: it leaves the broader system in a consistent state.