Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Truth of Java Exceptions: What's Really Going on Under the Hood?

DZone's Guide to

The Truth of Java Exceptions: What's Really Going on Under the Hood?

To get the most out of your code, it's important to know exactly how exceptions work and when they're thrown, even down to the bytecode level.

· Java Zone
Free Resource

Microservices! They are everywhere, or at least, the term is. When should you use a microservice architecture? What factors should be considered when making that decision? Do the benefits outweigh the costs? Why is everyone so excited about them, anyway?  Brought to you in partnership with IBM.

For more like this, visit the Takipi blog.

Unlike finding out how a sausage gets made, a deeper understanding of Java exceptions is a piece of knowledge you wouldn’t regret learning more about.

In this post, we’ll go one step deeper into the JVM and see what happens under the hood when an exception is thrown, and how the JVM stores the information on how to handle it. If you’re interested in the inner workings of exceptions beyond the behavior they display on the surface, this is a good place to get started.

Kicking Off With Some Basic Exception Flow and Behavior

Exceptions are one of the basic Java constructs, yet so many developers still get them wrong.

In Joshua Bloch’s book, Effective Java, he described these eight guidelines of exception handling that we like to quote:

  1. Use exceptions only for exceptional scenarios.

  2. Use checked exceptions for recoverable conditions and runtime exceptions for programming errors.

  3. Avoid unnecessary use of checked exceptions.

  4. Favor the use of standard exceptions.

  5. Throw exceptions appropriate to the abstraction.

  6. Document all exceptions thrown by each method.

  7. Include failure-capture information in detail messages.

  8. Don’t ignore exceptions.

Exceptional scenarios in that sense could mean situations that need additional context further up to the stack to handle properly, or breaking out of a method to avoid causing damage to the control flow. We’ve gathered these guidelines and additional exceptional (pun intended) insights into a cheat sheet that you can check it out right here.

With that said, too many projects use exceptions as part of their control flow, among other sins. A common abuse is treating them like sophisticated GOTO statements. Ideally, “normal” exceptions should not exist.

This is a frequent research topic for us. To dig in further and see how exceptions behave in the wild, we gathered data from over 600,000 Java projects, and 1,000 production applications generating over 1 billion events. You can read more about it in the following eBook.

The JVM Internals Schematic

The basic under the hood exception flow can be explained in a fairly simple way. To make sure we’re on the same page here, let’s start with some key JVM constructs in a nutshell (incl. the effects of Java 8).

Stack: Per Thread

Holds the frames that lead to the current point in time in the execution of the application. The frame is popped when a method terminates, either by returning normally or if an uncaught exception is thrown.

Heap Memory: Shared by Threads

Manages the runtime memory required by the application. We’ve also published a number of posts around memory management and garbage collection: GC overhead, GC misconceptions, garbage collectors comparisons, and performance tuning.

Non-Heap Memor: Shared by Threads

All the memory that’s allocated outside the heap. This includes the exception table which we’ll elaborate on in a bit.

With Java 8, PermGen, as shown in the following diagram, was replaced with a new mechanism called “Metaspace,” which serves the same purpose but is implemented in a different way. Most prominently, since Java 8, there’s no need to specify the PermGen size, as Metaspace dynamically resizes at runtime.

Image title


Source: JVM Internals by James Blooms

The Non-Heap Exception Table

An Exception Table is stored in PermGen / Metaspace on Non-Heap storage per method. It is created when a method defines try-catch or finally blocks.

The table has four fields:

  1. From – Start point.

  2. To – End point.

  3. Target – Handler code.

  4. Type – The exception class.

When an exception is thrown, the JVM would use the exception table to locate its handler. If it does not exist, the stack frame would pop and the exception will be rethrown to the calling method according to its stack trace.

Finally, handlers would execute no matter what. No matter which exception is thrown, and even if there was no exception.

A Sample Exception Flow

There’s no better way than seeing it in practice, so we’ve created a simple method to demonstrate how it works:

  public static void main(String[] args) throws Exception {
      try {
          throw new Exception();
      } catch (Exception e) {
          System.out.print("Caught!");
      } finally {
          System.out.print("Finally!");
      }
  }


Not much going on, an exception is thrown, caught, and then a finally block runs. So how do we get to see the bytecode and exception table?

Let’s disassemble the corresponding class file and look at the output using:

javap -v sample.class

Here’s the bytecode we get (with one cool “side effect” we’ll mention in a bit):

     0: new           #17                 // class java/lang/Exception
     3: dup
     4: invokespecial #19                 // Method java/lang/Exception."<init>":()V
     7: athrow
     8: astore_1
     9: getstatic     #20                 // Field java/lang/System.out:Ljava/io/PrintStream;
    12: ldc           #26                 // String Caught!
    14: invokevirtual #28                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
    17: getstatic     #20                 // Field java/lang/System.out:Ljava/io/PrintStream;
    20: ldc           #34                 // String Finally!
    22: invokevirtual #28                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
    25: goto          39
    28: astore_2
    29: getstatic     #20                 // Field java/lang/System.out:Ljava/io/PrintStream;
    32: ldc           #34                 // String Finally!
    34: invokevirtual #28                 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
    37: aload_2
    38: athrow
    39: return


And here’s the exception table:

     from    to  target type
         0     8     8   Class java/lang/Exception
         0    17    28   any


Alright, bytecode might sound scary, but even if you’re not familiar with it at all, it’s possible to understand the general flow. Let’s decipher what’s going on in the table.

First line: That’s our try-catch! If an Exception is thrown between lines 0 to 8, go to the handler at line 8.

Second line: That’s our finally! If anything happens between line 0 to 17, go to the handler at line 28.

That’s it. Now you know what an exception table is, where is it stored, and how it relates the bytecode generated from your Java applications.

You might have noticed something weird along the way. “Finally” appears twice in the bytecode. It’s the javac equivalent for dealing with Murphy’s law, if something can go wrong – it will go wrong.

The first finally block is glued to the catch block on lines 8 to 25. The second finally block exists to make sure it executes in the case of a rethrow in the catch block or anything else that might break the normal flow. Notice the athrow bytecode instruction on line 38.

The flow goes like this: an Exception is created and then thrown on bytecode line 7. Exception table says, if this happens, go to line 8. Then, we just print out “Caught!” and “Finally!”, and goto line 39 where the method returns.

On bytecode lines 28 to 38, as we explained, we have the finally rethrow protection.

By the way, if you’re interested in learning more about exception performance, The Exceptional Performance of Lil’ Exception by Aleksey Shipilev (TL;DR: scroll to the bottom of that post). Using exceptions for non-exceptional purposes is bad ya’ll.

Uncaught Exception Horror Stories

What happens when there’s nothing out there to catch our exception? Stack frames pop until we reach the last method in the trace. If there’s no handler there as well, the thread dies. If it’s the last non-daemon thread in the process – the JVM dies. That’s why it’s always recommended to set a last resort uncaught exception handler, to capture whatever is left from the context of that error without external tools like what we’re building at OverOps.

We’ve seen many cases where our users would swear there are no uncaught exceptions in their applications, only to find to out that there are errors causing some serious damage that they didn’t even know existed.

What’s the Best Way to Manage Exceptions in Production?

That’s the bread and butter of our day to day at OverOps as we get a chance to work on solving that problem. Whenever an exception or a logged error/warning happens in production, OverOps capture its source code and variable state across the entire call stack.

This means that for every exception, you’re able to see the exact variable state that caused it and easily reproduce or solve it. Without it, the usual course of action, if you were even aware and able to know that there was an error is the vicious production debugging cycle. Sift through logs looking for clues, find out you’re missing more context (in like 99% of the cases), add logging statements, build-test-stage-deploy, hope the error happens again (which is kind of a paradox), and again and again, until you nail down the root cause of the issue.

This cycle sucks – but now there’s a new way to debug Java in production.

Check it out.

Final Thoughts

We hope you’ve enjoyed learning more about how exceptions behave under the hood and how they relate to internal JVM schematics with the exception table. Please let us know if you enjoyed reading this post, and if you have any questions or other topics you’d like us to write about.

For more like this, visit the Takipi blog.

Discover how the Watson team is further developing SDKs in Java, Node.js, Python, iOS, and Android to access these services and make programming easy. Brought to you in partnership with IBM.

Topics:
java ,exception handling ,java internals ,tutorial ,bytecode

Published at DZone with permission of Alex Zhitnitsky, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}