Wednesday, 5 February 2014

Exiting the JVM

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?

  1. Our code registers the shutdown hook
  2. Our code acquires the lock
  3. Our code prints "Exiting"
  4. Our code calls System.exit(0)
  5. System.exit(0) calls our shutdown hook
  6. Our shutdown hook prints "Locking"
  7. Our shutdown hook tries to acquire the lock
  8. 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...

6 comments:

  1. 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).

    ReplyDelete
  2. Hi, nice one! Are there any chances to reach the finally block? I want to use this code and stripped it down to this:


    public 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;
        }

    }

    ReplyDelete
    Replies
    1. I suspect that the SecurityException would be one case where the finally block is reached.

      I'd agree that the JDK could do with a method for invoking Timer more easily.

      Delete
  3. Maybe 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.

    ReplyDelete
  4. how about remove shutdown hook before exit(-1) ? would that work?

    ReplyDelete