Optimize Java Exception and benchmark

In the post Clean Code with Exception I promised to solve the problem of making Exception slow. So this post will go deeper into what Java will do when an Exception is thrown and explain what a stack trace is… and from there will optimize the use of Exception

1. What will Java do when an Exception is thrown?

To clearly understand what Java does when an Exception is thrown, let’s look at the code of the Throwable class, the parent of Exception ;))

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class Throwable implements Serializable {

  //bla bla bla
  public Throwable() {
    fillInStackTrace();
  }

  public Throwable(String message) {
    fillInStackTrace();
    detailMessage = message;
  }

  public Throwable(String message, Throwable cause) {
    fillInStackTrace();
    detailMessage = message;
    this.cause = cause;
  }

  public Throwable(Throwable cause) {
    fillInStackTrace();
    detailMessage = (cause==null ? null : cause.toString());
    this.cause = cause;
  }

  // fu fu fu
  public StackTraceElement[] getStackTrace() {
    return getOurStackTrace().clone();
  }

  private synchronized StackTraceElement[] getOurStackTrace() {
    // Initialize stack trace field with information from
    // backtrace if this is the first call to this method
    if (stackTrace == UNASSIGNED_STACK ||
      (stackTrace == null && backtrace != null) /* Out of protocol state */) {
      stackTrace = StackTraceElement.of(this, depth);
    } else if (stackTrace == null) {
      return UNASSIGNED_STACK;
    }
    return stackTrace;
  }

  public synchronized Throwable fillInStackTrace() {
    if (stackTrace != null ||
      backtrace != null /* Out of protocol state */ ) {
      fillInStackTrace(0);
      stackTrace = UNASSIGNED_STACK;
    }
    return this;
  }

  private native Throwable fillInStackTrace(int dummy);

  // fe fe fe  
}

The above code shows us that all the public constructors of Throwable call the fillInStackTrace() function. So what does this function do and why do we need to call it? To get the answer, let’s go to the next section

2. What is a stack trace? Why do we need to use it?

Suppose we have the following code and try to run it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Junk {
  public static void main(String args[]) {
    try {
      a();
    } catch(HighLevelException e) {
      e.printStackTrace();
    }
  }
  static void a() throws HighLevelException {
    try {
      b();
    } catch(MidLevelException e) {
      throw new HighLevelException(e);
    }
  }
  static void b() throws MidLevelException {
    c();
  }
  static void c() throws MidLevelException {
    try {
      d();
    } catch(LowLevelException e) {
      throw new MidLevelException(e);
    }
  }
  static void d() throws LowLevelException {
    e();
  }
  static void e() throws LowLevelException {
    throw new LowLevelException();
  }
}

class HighLevelException extends Exception {
  HighLevelException(Throwable cause) { super(cause); }
}

class MidLevelException extends Exception {
  MidLevelException(Throwable cause)  { super(cause); }
}

class LowLevelException extends Exception {
}

The result will be the following error:

HighLevelException: MidLevelException: LowLevelException
        at Junk.a(Junk.java:13)
        at Junk.main(Junk.java:4)
Caused by: MidLevelException: LowLevelException
        at Junk.c(Junk.java:23)
        at Junk.b(Junk.java:17)
        at Junk.a(Junk.java:11)
        ... 1 more
Caused by: LowLevelException
        at Junk.e(Junk.java:30)
        at Junk.d(Junk.java:27)
        at Junk.c(Junk.java:21)
        ... 3 more

Analyzing the above error message, we have the following interpretation:

  • An exception HighLevelException was thrown. The cause of HighLevelException is MidLevelException and the cause of MidLevelException is LowLevelException
  • HighLevelException appears in class Junk method a file Junk.java line 13 and method a is called from class Junk method main file Junk.java line 4
  • MidLevelException appears in class Junk method c file Junk.java line 23, method c is called from class Junk method b file Junk.java line 17 and method b is called from class Junk method a file Junk.java line 11
  • LowLevelException is similar to MidLevelException and HighLevelException but I also have a headache. Of course, if there is an error and need to trace, I still have to do it ;))

And from the above analysis, we can redraw the flow where the exception is thrown how it looks like:

Stack trace

My image above can also be considered a representation of stack trace. To be more specific, stack trace is an ordered list of function calls and is arranged in the order of calls from the most recent to the first call. It is very useful in analyzing, finding the cause and from there deciding whether to fix the error or not.

Back to the question in #1. Then we have the answer. The fillInStackTrace() function is to collect stack trace and it is very useful, isn’t it 🤟

3. Collect stack trace fast or slow?

The answer is very slow and to see how slow you can clone this repo. If you do variables, see #5.

4. Disable collect stack trace and optimize

I rewrote NSTException and simplified the 2 functions getStackTrace() and fillInStackTrace() as the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class NSTException extends Exception {

  private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];

  public NSTException(String message) {
    super(message);
  }

  @Override
  public StackTraceElement[] getStackTrace() {
    return EMPTY_STACK_TRACE;
  }

  @Override
  public Throwable fillInStackTrace() {
    return this;
  }
}

Now when NSTException is initialized, it will not collect stack trace anymore. Let’s see the results.

5. Benchmark

I will explain a little bit about how to benchmark. I have 3 methods callNotThrowException, callToThrowException and callToThrowNSTException used to simulate the following situations:

  • callNotThrowException: normal situation, no Exception is thrown
  • callToThrowException: situation where Exception is thrown
  • callToThrowNSTException: situation where NSTException is thrown. NSTException stands for No Stack Trace Exception, a custom exception I rewrote, turning off stack trace collection to increase performance

Due to differences in configuration, OS, Java version… the results may vary. And here are the results from my machine:

Benchmark                    (param)   Mode  Cnt         Score          Error  Units
Jmh.callNotThrowException          1  thrpt   25  80504640.048 ± 15390305.644  ops/s
Jmh.callToThrowException           1  thrpt   25    703653.862 ±    58119.745  ops/s
Jmh.callToThrowNSTException        1  thrpt   25  41387559.456 ±  6619635.038  ops/s

We note that the 2 results of Jmh.callNotThrowException and Jmh.callToThrowException are 80504640.048 and 703653.862 (ignoring the ± error) then 80504640.048 / 703653.862 = 114 is a very large number.

Conclusion: By using NSTException to turn off collect stack trace, we have the following advantages:

  • Speed up the application and use memory more efficiently
  • Make the method signature easier to understand
  • External control code is easier to understand when separating business code and error handling, I will also have a post about this part

But there is also a disadvantage that there is no stack trace, from which tracing errors will be difficult. Please watch the next post to solve it.

updatedupdated2025-01-262025-01-26
Load Comments?