Press "Enter" to skip to content

Has my JVM Lost Exception Stacktraces?!

Your JVM application “broke” and stopped logging exception stack traces? Everything is fine. Follow me!

Exception Throwing Overhead

Exceptions are not ordinary POJOs. I mean, they mostly are, but with one extra tiny detail:

public synchronized Throwable fillInStackTrace() {
    // ...
}

This makes the exception creation time dependent on the depth of a stack trace, and in the world of contemporary frameworks, those can get pretty impressive!

source: https://ptrthomas.wordpress.com/2006/06/06/java-call-stack-from-http-upto-jdbc-as-a-picture/

Internally, it’s calling a native method which doesn’t speed things up.

That’s why many performance-critical libraries prefer skipping collecting stack traces, which makes those exceptions easily cacheable.

Netty’s Norman Maurer did benchmark that some time ago, and, as you can see, the difference is significant:

Throwable
source: http://normanmaurer.me/blog/2013/11/09/The-hidden-performance-costs-of-instantiating-Throwables/

As you can see, the concept is quite trivial. If you want to leverage it, it’s enough to create a static exception instance, reset the stacktrace, and… keep throwing it:

class StaticStacklessExceptionExample {

    private static final NullPointerException NULL_POINTER_EXCEPTION = new NullPointerException();

    static {
        NULL_POINTER_EXCEPTION.setStackTrace(new StackTraceElement[0]);
    }

    public static void main(String[] args) {
        throw NULL_POINTER_EXCEPTION;
    }   
}

JVM Stacktrace Optimization

However, this is where the fun starts. The JVM knows that collecting stacktraces is expensive, and it can optimize them on the spot¬†when they’re needed. By “optimizing”, I mean “dropping”.

Let’s try to reproduce it! In order to do this, we’ll induce a NullPointerException by calling a method on a null String repeatedly and log the results when the stack trace is empty:

class StacktraceDropExample {

    public static void main(String[] args) {
        NullPointerException previous = null;

        String foo = null;
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            try {
                foo.toUpperCase();
            } catch (NullPointerException e) {
                if (e.getStackTrace().length == 0) {
                    System.out.printf("Stacktrace dropped at iteration %d%n", i);
                    if (previous != null) {
                        System.out.printf("Last stacktrace: %s%n",
                          Arrays.toString(previous.getStackTrace()));
                    }
                    System.out.printf("New stacktrace: %s%n",
                      Arrays.toString(e.getStackTrace()));
                    return;
                }
                previous = e;
            }
        }
    }
}

Here’s my result

Stacktrace dropped at iteration 41984
Last stacktrace: [com.pivovarit.exception.StacktraceDropExample.main(StacktraceDropExample.java:14)]
New stacktrace: []

As you can see, we forced the JVM to drop stacktraces of that exception after 41984 iterations! Amazing!

What’s more, let’s try to spice things up. Instead of printing stacktraces, let’s try to collect all witnessed exceptions and count distinct instances according to reference equality:

class ExceptionCacheExample {

    public static void main(String[] args) {
        var exceptions = Collections.newSetFromMap(new IdentityHashMap<>());

        String foo = null;
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            try {
                foo.toUpperCase();
            } catch (NullPointerException e) {
                exceptions.add(e);
            }
        }

        System.out.println(exceptions.size());
    }
}

Note that we can’t use classic HashSet, since we need to rely on reference equality:

Collections.newSetFromMap(new IdentityHashMap<>());

Let’s run it Integer.MAX_VALUE times:

99327

Despite 2147483647 iterations, there were only 99327 distinct instances! This means that JVM started caching exceptions and reusing them!

This behaviour is configurable and can be turned off by using the -XX:-OmitStackTraceInFastThrow switch. When we start the above with this argument, the first example doesn’t print anything, and the second one… consumes all the heap space:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.pivovarit.exception.ExceptionCacheExample.main(ExceptionCacheExample.java:14)

Summary

Creating stackless reusable static instances of Exceptions is one of the classic performance tricks, which JVM can apply automatically under certain circumstances. This can be turned off, but I wouldn’t recommend it since it makes JVMs more resilient when exceptions start falling from the sky.

Also, when you start dropping stack traces on your own, make sure that it brings more value than stack traces themselves, which are quite useful.

Sources

The complete example can be found on GitHub.




If you enjoyed the content, consider supporting the site: