Compiling Try/Catch/Finally on the JVM

One thing we’re currently working on, for a variety of reasons, is compiling down our in-house programming language into Java bytecode (and just for the record, I can’t make any promises about when we’ll be done or around when or even if this stuff will make it into future product releases).  Part of the fun therein is learning about the internals of the JVM, as well as finding all the crazy edge cases in your own language.  And few things are as, um, “fun” or have as many edges as try/catch/finally statements.  So rather than delving into philosophy or agile methodology this time around, I’ll go into detail on an area of the JVM that most people never have to (or probably want to) delve into.

If you’d asked me two weeks ago, I would have assumed that finally blocks are a feature implemented by the JVM:  it’s such a core language thing, it’s got to be built-in, right?  Much to my surprise, the answer is no:  finally blocks are implemented by inlining the finally code at all possible exits from the try or associated catch blocks, wrapping the whole thing in essentially a “catch(Throwable)” block that rethrows the exception when it finishes, and then adjusting the exception table such that the catch clauses skip over the inlined finally statements.  Huh?  (Small caveat:  prior to the 1.6 compiler, apparently, finally statements used sub-routines instead of full-on code inlining.  But we’re only concerned with 1.6 at this point, so that’s what this applies to).

That probably doesn’t make any sense, so let’s rewind a bit with some JVM basics around exception handling.  Exception handling is built into the JVM, and it’s done so in the form of declaring try/catch blocks within a given method.  What you’re saying is “between point A and point B, any exceptions of type E should be handled by the code at point C.”  You can have as many such declarations as you need, and when an exception is propogated to that method the JVM finds the catch block that matches based on the extent and the type of exception it handles.

So with a simple example:

public void simpleTryCatch() {
  try {
    callSomeMethod();
  } catch (RuntimeException e) {
    handleException(e);
  }
}

You might end up with bytecode like the following (I’m showing it in the format the ASM Eclipse plugin does:  it’s an invaluable tool for learning how the Java compiler works, and I find it’s formatting pretty easy to read.  The “L0” and such are code labels.):

public simpleTryCatch()V
TRYCATCHBLOCK L0 L1 L2 java/lang/RuntimeException
L0
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callSomeMethod()V
L1
  GOTO L3
L2
  ASTORE 1
  ALOAD 0
  ALOAD 1
  INVOKEVIRTUAL test/SimpleTryCatch.handleException(Ljava/lang/RuntimeException;)V
L3
  RETURN

So we’re saying the catch statement covers the full extent of the try block (though not the GOTO statement at the end), and that on a RuntimeException we should transfer control to L2.  If the try statement complete, it jumps over the catch statement and continues on.  When the RuntimeException handler is invoked, the exception is on the top of the stack, so we store it to a local variable slot.  We then load the “this” pointer and the exception argument in order to invoke the exception handler.  The catch block then falls through to the end; if there were additional catch blocks, it would jump over them.

So let’s add in a finally block and another catch statement and see what happens to the bytecode.  Suppose we have the following totally contrived method:

public void tryCatchFinally(boolean arg) {
  try {
    callSomeMethod();
    if (arg) {
      return;
    }
    callSomeMethod();
  } catch (RuntimeException e) {
    handleException(e);
  } catch (Exception e) {
    return;
  } finally {
    callFinallyMethod();
  }
}

In this case, we end up with this far-less-obvious bytecode:

public tryCatchFinally(Z)V
TRYCATCHBLOCK L0 L1 L2 java/lang/RuntimeException
TRYCATCHBLOCK L3 L4 L2 java/lang/RuntimeException
TRYCATCHBLOCK L0 L1 L5 java/lang/Exception
TRYCATCHBLOCK L3 L4 L5 java/lang/Exception
TRYCATCHBLOCK L0 L1 L6
TRYCATCHBLOCK L3 L7 L6
TRYCATCHBLOCK L5 L8 L6
L0
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callSomeMethod()V
L9
  ILOAD 1
  IFEQ L3
L1
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
L10
  RETURN
L3
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callSomeMethod()V
L4
  GOTO L11
L2
  ASTORE 2
L12
  ALOAD 0
  ALOAD 2
  INVOKEVIRTUAL test/SimpleTryCatch.handleException(Ljava/lang/RuntimeException;)V
L7
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
  GOTO L13
L5
  ASTORE 2
L8
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
  RETURN
L6
  ASTORE 3
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
  ALOAD 3
  ATHROW
L11
  ALOAD 0
  INVOKEVIRTUAL test/SimpleTryCatch.callFinallyMethod()V
L13
  RETURN

So what’s going on here?  (Note that the labels are numbered in the order they’re created by the compiler, not by the order they appear in the code). First of all, you’ll notice that both exception handler blocks are now split in two, from L0 to L1 and from L3 to L4.  That’s because the range from L1 to L3 is where the finally block is inlined due to the return statement.  Since exceptions thrown during the finally block shouldn’t be caught by catch blocks associated with the same try statement, that range has to be excluded from the exception table.  The exception entries with no type declared are from the finally block.  It has to handle any exception thrown from the try statement or from the catch blocks, but it also has to exclude any inlined finally statements so that the finally block doesn’t catch exceptions thrown by that same finally block.  It has to have three splits, because in addition to the inlined finally within the try block, the catch(Exception) block also contains a return statement.  You may also be surprised to see that the finally block appears a total of five times in the code.  You’ll see the first inlined finally from L1 to L3, corresponding to the return statement in the try block.  The second finally block is a bit more confusing:  in this case it’s being inlined into the end of the first catch block, which then jumps over the finally code that’s there in the fallthrough case.  (I personally would assume that it would jump down to the fallthrough case, rather than inlining the code again).  The third time it appears is from L8 to L6, around the early return statement within the second catch block.  The fourth time the code in the finally block appears is from L6 to L11, which corresponds to the exception case:  that’s to ensure the finally block is executed in the case of an unhandled exception thrown out of the try block or any of the catch blocks.  Notice that it stores the exception as normal, performs the call in the finally statement, and then loads and rethrows the exception.  The last finally block is for the fall-through case:  it’s where the end of the try block jumps to.

If we were to have nested try/catches or try/finallys, things would get even stranger.  A return from the inner try statement needs to have both the inner and outer finally inlined into it, and the exception table needs to be set up such that exceptions thrown by the inner finally are caught by the outer catch statements and the outer finally, and exceptions thrown by the outer finally aren’t caught by anything.  At this point you can probably start imagining the kind of state your compiler needs to carry around in order to know what to to inline where and how to partition the exception table correctly.

It’s interesting, for me at least, to see how the JVM designers chose to push the finally statement into the compiler rather than building it into the VM; clearly it can be built by the compiler, thus making the VM that much simpler, it just makes life a little rougher for folks like us who are building another language on the JVM.

Knowing how things are implemented by the compiler can actually make it a little easier to figure out what happens in certain odd circumstances.  For example:

try {
  return "foo";
} finally {
  return "bar";
}

will end up returning “bar” since the finally statement is inlined before the return statement, so the return from the finally block executes first and the original return statement never does.

String value = "foo";
try {
  return value;
} finally {
  value = "bar";
}

will return “foo” since the value to return is stored in the stack prior to the finally statement executing, and then restored and returned afterwards.  (My example didn’t show that, but that’s what you’d see if you were to look at the bytecode).  So the finally block changing the “value” variable has no affect on the in-progress return statement.  And lastly, something like:

while(true) {
  try {
    return "foo";
  } finally {
    break;
  }
}
return "bar";

will return “bar”.  This one surprised even me at first, but it makes sense once you realize that a break statement is just a GOTO in bytecode, so when the finally block is inlined as part of the inner return statement, that GOTO statement executes before the RETURN instruction and completely skips over it to the end of the loop (the same logic holds true for a continue statement inside a finally block).

On our end, we’ve decided to disallow return, break, and continue statements within the finally block due to the rather unexpected semantics (and C# disallows them as well, so I feel like we’re in good company with that decision).

If anyone actually found this enlightening, I’ll consider doing some future posts about other interesting issues we’ve run into when generating bytecode, both due to the JVM itself and due to the occassional impedance-mismatch between our language and Java around things like closures and enhancements and generics.


13 Comments on “Compiling Try/Catch/Finally on the JVM”

  1. Kent Beck says:

    Thank you for the detailed account. I’ve always been interested in language implementation issues. This describes it clearly for me. I especially appreciated the explanation of the odd edge cases at the end. That’s the sort of information that would take days to reproduce.

  2. Bernard says:

    No, keep posting. It is very interesting.

    I’ve started using Scala; another language with closures and modern constructs that compiles into the JVM. They took the easy way. Their try, catch, finally clauses behaves just like in Java as far as I can see.

    BTW, your second from last example, the String needs to be declared outside the try block to be in scope for the finally.

  3. Alan Keefer says:

    Bernard, thanks for catching that. I’ve updated the post appropriately. And for the Scala compiler, there’s no really no easy way out: they have to be emitting basically this bytecode (or similar) for similar combinations of try, catch, finally, and control flow statements, meaning their compiler has to know how to do all of this.

    • Bernard says:

      By “taking the easy way” I meant that Scala seems to have chosen the Java bytecode implementation “as is” instead of looking at reducing inconsistencies as you’re doing.
      The “throw” statement is also troublesome inside a finally clause. See Bruce Eckel for instance http://www.codeguru.com/java/tij/tij0102.shtml. Scala keeps that inconsistency here too.

      • Alan Keefer says:

        Yeah, that one’s definitely a bit nasty. Currently we model the Java behavior there, such that the exception thrown from within the finally blocks causes any previous exception to be lost.

        Since we’re compiling our language ourselves, theoretically we could do something different there, like wrap the version of the finally block that’s used to handle exceptions inside a try/catch(Throwable), such that if the finally block code is executing because of an exception and itself throws an exception, we could do something. I’m not entirely sure what that something should be, though.

  4. Dan Hicks says:

    Not extraordinarily surprising, though I’ve never sat down and tried to figure out what a compiler must do. I **have** had to write the classfile verifier, so I know what a b*tch it is to verify try/catch/finally (and the new StackMapTable stuff in Java 6 didn’t help one bit).

    • Alan Keefer says:

      My vague understanding (according to my co-worker who did the first pass at the implementation) is that Java used to use a subroutine call for finally blocks, calling the subroutine at all the necessary places, rather than directly inlining the finally blocks as is done now (apparently the subroutine instruction isn’t allowed in 1.6 bytecode). Apparently they ditched it because it made verification too difficult and too slow, though I can imagine that, as you say, it’s still a serious pain despite that change.

  5. David Linde says:

    I enjoyed the post, I am definately interested in more jvm details or new languages in general.

  6. Taras says:

    give us more 🙂

    btw, on an unrelated topic
    what’s the go with GScript in V5.0 of CC being not case sensitive, but if you use proper casing the manual says you achieve better performance? how does that work?

    • Alan Keefer says:

      Yeaaahhhhhh . . . that one’s kind of an odd story. I believe (though I could be wrong . . . it was more than seven years ago) that GScript ended up case-insensitive because a very early customer requested that for our rules engine (which was all GScript was, at first). Unfortunately, in addition to being annoying due to namespace collisions, it’s a bit of a performance drag. In previous versions of our platform, we implemented case-insensitivity via a CaseInsensitiveHashMap and a CaseInsensitiveCharSequence; basically, doing something like toUpperCase or toLowerCase all the time was too expensive, so we had to wrap Strings in our own object that could do a case-insensitive hash, and have a special Map class that knew how to make use of that. Even that, however, turned out to be a major performance drag.

      So for the version of the platform used in CC5, we killed off as many of the CaseInsensitiveHashMaps as we could, and instead replaced it with a fallback mechanism: we assume that the name typed into the source is the correct one, so we look the property up by that name in a normal HashMap. If that fails, then we fall back to looking through the property names to find the one that matches without regard to case. That lookup process is relatively slow, though, and happens every time that expression is executed.

      The upshot is that we were able to improve performance in the case where everything is in the right case at the expense of slowing down accesses where the case is incorrect. You’ll notice that case mismatches also now generate a parse warning, so you can at least identify and fix those issues. I believe the current plan is to make the language fully case-sensitive in some not-too-distant future release.

      • Taras says:

        Yeah we’ve noticed those 300 odd warnings when the server starts up 🙂

        So how much slower is it going to be? Is it 2-3 times slower?
        What I’m trying to establish, is whether it’s worth going through the legacy code and fix all those case sensitivity issues, or leave it for the distant future when the management decides to upgrade to a version with full case sensitivity.

      • Alan Keefer says:

        It’s almost certainly not worth fixing just for speed improvements; it’s really just a small drop in the bucket compared to everything else going on. Maybe 1 or 2%, depending on how many issues there are on a given page and how frequently that code is hit? Maybe not even that? The main incentive to fix those will be, as you say, that some day you’ll have to fix them if you want to upgrade to some later version that’s fully case-sensitive (probably CC 7/PC 5/BC 4 will have a case-sensitive version of the language, though that’s not set in stone by any means).

  7. Taras says:

    interesting
    i guess it’s all subject for another topic

    would you mind doing a blog on Studio performance and plans to improve it? 🙂


Leave a reply to Alan Keefer Cancel reply