Back to Home

Compiling Try / Catch / Finally for the JVM

Instead of being introduced · the author of the article · Alan Keefer1 · is the chief architect of Guidewire Software2 · an insurance business software company. While still a senior ...

Compiling Try / Catch / Finally for the JVM

Original author: Alan Keefer
  • Transfer

Instead of introducing


The author of the article, Alan Keefer 1 , is the lead architect for Guidewire Software 2 , an insurance business software company. While still a senior developer, he participated in work on the Gosu 3 language . In particular, Alan dealt with issues of compiling the language into Java bytecode.

This article was written in 2009 and is dedicated to the details of the implementation of try / catch / finally in the JVM version 1.6. To read it, you must have basic knowledge of Java syntax, as well as understand the purpose of the byte code, the sheets of which are under the cut. Also at the end of the article are a series of examples similar to the tricky tasks of SCJP.

JVM internals


One of the things that we are currently working on is compiling our “home” language into Java bytecode. (For reference: I can’t say when we will finish. Even approximately. Even if it will fall into future releases.) The fun is to study the insides of the JVM, as well as finding all the fucking sharp corners of your own language. But most of all the “fun” and sharp corners come from operators such as try / catch / finally. Therefore, this time, I will not go into philosophy or agile. Instead, I will delve into the JVM, where most do not need (or do not want to) delve into.

If you asked me about finally blocks two weeks ago, I would suggest that their processing is implemented in the JVM: this is the basic part of the language, it should be built-in, shouldn't it? Imagine my surprise when I found out: no, not so. In fact, finally-blocks are simply substituted in all possible places after try- or associated catch-blocks. These blocks are wrapped in “catch (Throwable),” which will throw an exception once the finally block finishes. All that remains is to twist the exception table so that the substituted finally blocks are skipped. Well how? (A small caveat: prior to JVM 1.6, for the finally operator, apparently, subroutines were used instead of full substitution. But now we are talking about version 1.6, to which all of the above applies.)

To understand whether this approach makes sense, rewind a bit and see how the JVM handles exceptions. Their processing is built into the JVM in the form of declaring try / catch blocks using a special method. All that is required of you is to say "between point A and point B, any exception of type E must be handled by code at point C". You can have as many of these declarations as you need. When an exception is passed to this method, the JVM will find the appropriate catch block, depending on its type.

A simple example of a try / catch block


Consider a simple example:

publicvoidsimpleTryCatch(){
  try {
    callSomeMethod();
  } catch (RuntimeException e) {
    handleException(e);
  }
}

For him, you will end up with the bytecode below. (I use the formatting offered by ASM Eclipse, an invaluable tool for learning about the JVM mechanisms. It seems to me that code in this format is pretty easy to read. “L0” and the like 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 tell the catch statement to cover the entire try block (but not the GOTO statement at the end), and in the case of a RuntimeException, pass control to L2. If the try statement completes, you must jump over the catch statement and continue execution. If the RuntimeException handler is called, the exception is on the top of the stack, and we save it in a local variable. Then we load the pointer to “this” and the exception in that order to call the handleException method. After that, the remaining code is executed to the end. However, if there was an additional catch block, we would have jumped it.

Try / catch / finally block example


Now add a finally block and an additional catch statement and see what happens in the bytecode. Take the following completely contrived example:

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

In this case, we get much less clear byte code:

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 is going on here? (Note that the labels are numbered in the order in which they were created by the compiler, and not in the order they appear in the code.) First of all, you will notice that both exception handling blocks are now divided into two: from L0 to L1 and from L3 to L4. This happened because a finally block was inserted between L1 and L3 due to the return statement.

For the reason that exceptions thrown from the finally block should not be handled by catch blocks associated with the same try statement, the corresponding range has been removed from the exception table. Records in a table without an exception type refer to the finally block. They should handle exceptions of any type thrown from a try statement or from catch blocks, and they should ignore any substituted finally blocks. Thus, finally blocks will not catch exceptions thrown by the same finally blocks. There were three such entries, because in addition to finally, inserted inside the try block, the catch (Exception) block also contains a return statement.

You may also be surprised to see that the finally block occurs in the code 5 (five) times. The first finally substituted, corresponding to the return statement of the try block, occurs between L1 and L3. The second finally block is a bit more confusing: it is inserted at the end of the first catch block, which then jumps through the rest of the finally code. (Personally, I believe that here it was necessary to make a transition to the end instead of the next embedding.) The third time it appears between L8 and L6 before the return statement in the second catch block. The fourth time the finally block appears in the code between L6 and L11, which corresponds to the case of an exception: you need to be sure that the finally block will be executed in the event of an unhandled exception thrown in the try block or any catch block. Please note that the exception is preserved as if nothing had happened, the finally statement is called, after which the exception is loaded and thrown again. In the last finally block, control passes from the end of the try block.

If we had nested try / catch or try / finally blocks, everything would be even weirder. The return statement of an internal try block requires that the finally blocks of both the internal and external try be substituted for it. The exception table must be configured so that the exception thrown by the internal finally is caught by the external catch and finally statements, and the exception thrown by the external finally is not caught by anyone. Now you are probably trying to imagine what set of states your compiler is forced to carry with itself in order to know what to substitute and how to populate the exception table.

It would be interesting to find out, at least to me, how the creators of the JVM decided to stick the finally statement in the compiler instead of embedding it in a virtual machine. Obviously, doing this work with the compiler can greatly simplify the virtual machine, but it makes life a little more difficult for people like us who create another language for the JVM.

Custom examples


Understanding how the compiler is implemented makes it easier to understand some non-standard cases. For example:

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

The result will be “bar”, because the finally statement will be substituted before the return statement, which means that return from the finally block will be called first, and return from the try block will not be called at all.

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

The result will be “foo” because the value for the return statement will be pushed before the finally statement is called, after which it will be restored and returned. (My example does not show this, but this is exactly what you will see if you look at the bytecode.) Thus, changing the value of "value" in the finally block does not have any value for the return statement. And finally, something like:

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

The result will be “bar”. It was a surprise even for me, but everything is logical if you know that the break statement is just GOTO in bytecode. Those. when the finally block is substituted as part of the internal return statement, the GOTO statement is called earlier than the RETURN statement, which causes the loop to exit. (The same goes for the continue statement inside the finally block.)

Conclusion


For our part, we decided to prohibit return, break, and continue statements inside finally blocks due to undefined semantics, as is done in C #. (And I feel that a good company has gathered from those who made this decision.)

If someone found this article instructive, I plan to write a few notes about other interesting things that we encountered when generating bytecode. They will relate both to the JVM directly, and to various inconsistencies between our language and Java, such as closures, external functions, and generics.

Glossary


subroutine = sub-routine
statement = statement
substitution = inlining
exception = exception
external function = enhancement = extension function = mixin

References


[1] devblog.guidewire.com/author/akeefer
[2] www.guidewire.com
[3] gosu-lang.org

Read Next