The secret life of Java exceptions and JVM internals: Level up your Java knowledge
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 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 8 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 Memory – 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 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.
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 4 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:
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):
And here’s the exception table:
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’s an exception table, 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.
btw, if you’re interested to learn 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.
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.
Achieving Observability: How to Address the Unknown Unknowns in Your Application
Subscribe for Post Updates