Exceptions are familiar to everyone. The online problems we deal with are caused by various exceptions. We usually consider various exception scenarios when designing the system, and we also deal with various exception branching logic in our daily coding.

But we don’t seem to know much about anomalies. We hear a lot about exception handling, but is it all right?

What is the Exception

To understand exceptions, you have to start with Exception’s inheritance. There are many articles about Java Exception on the web, so I’ll make a long story short. Exception and Error are subclasses of Throwable.

Exception is subdivided into Checked/Unchecked, where Checked Exception is subject to compile-time checking and must be handled explicitly at the code level. Unchecked Exception, however, requires no explicit handling, which is what is commonly called Runtime Exception, and can be handled by code in most cases.

How does the JVM handle Exception

Through the above brief introduction, I believe you have at least a certain understanding of the basic concept of Exception. Let’s look at how the JVM handles Exception.

First, we need to define a method that throws an exception and how the corresponding try/catch block handles the exception method, as shown below:

public class OneExceptionService {

    public void createSomeException(a) {
        throw new IllegalArgumentException("Have some exception"); }}public class MiddleService {

    private OneExceptionService oneExceptionService;
    
    public void catchException(a) {
        try {
            oneExceptionService.createSomeException();
        } catch (Exception e) {
            throw newRuntimeException(e); }}}Copy the code

The above code is a bit cumbersome for the rest of our presentation because we will continue to use this set of code. Next we just need to focus on the bytecode corresponding to the catchException method as follows:

public void catchException(); Code: 0: aload_0 1: getfield #2 // Field oneExceptionService:Lme/hergootian/exception/OneExceptionService; 4: invokevirtual #3 // Method me/hergootian/exception/OneExceptionService.createSomeException:()V 7: goto 20 10: astore_1 11: new #7 // class java/lang/RuntimeException 14: dup 15: aload_1 16: invokespecial #8 // Method java/lang/RuntimeException."<init>":(Ljava/lang/Throwable;) V 19: athrow 20: return Exception table: from to target type 0 7 10 Class java/lang/ExceptionCopy the code

In the bytecode generated by compilation, we see an Exception table below. The exception table contains four attributes: from and to represent the scope of exception monitoring, target represents the start of exception processing, and Type represents the type of exception caught by exception processing. This exception table is stored in the PermGen/Metaspace area on the non-heap space.

So what does the JVM do with the exception table when an exception occurs in our program?

  1. If the current method exception table is not empty, one of the entries in the exception table is judged to determine whether the exception occurred in the code line monitored by the item from to to, and type matches, then the JVM calls the processing logic pointed at target for processing.
  2. If the above procedure does not find a handler that matches the criteria, the search continues for other entries in the exception table;
  3. When traversing the exception table of the whole method still does not find the handler that meets the condition, look up (pop stack);
  4. Find the exception table of the calling method of the current method, repeat the above logic 1-3;
  5. If all stack frames are ejected and no suitable handler is found, they are thrown to the current thread of execution and the thread terminates.
  6. If the current thread is the last non-daemon thread and the exception is not handled, the JVM will terminate.

Does Exception really affect performance

Catch exceptions affect performance

I’m sure some of you have heard that before. If catch exceptions affect performance, wouldn’t it be better to handle them all at the outermost layer of the program? When an exception cannot be handled, methods are iterated stack by stack. Is that slower?

Add an unCatchException method to the code as follows:

public class MiddleService {

    private OneExceptionService oneExceptionService;

    public MiddleService(OneExceptionService oneExceptionService) {
        this.oneExceptionService = oneExceptionService;
    }

    public void unCatchException(a) {
        oneExceptionService.createSomeException();
    }

    public void catchException(a) {
        try {
            oneExceptionService.createSomeException();
        } catch (Exception e) {
            throw newRuntimeException(e); }}}Copy the code

Then we add a new service, which calls our MiddleService. On the one hand, it is convenient for us to do performance testing, and on the other hand, it conforms to the outermost unified Exception handling scenario. At the same time, in order to check whether instantiating Exception really has performance cost, we add another layer of try/catch, code example is as follows:

@Slf4j
public class FacadeService {

    private MiddleService middleService;

    public FacadeService(MiddleService middleService) {
        this.middleService = middleService;
    }

    public void callUnCatchException(a) {
        try {
            middleService.unCatchException();
        } catch (Exception e) {
            log.error("Catch Exception:", e); }}public void callCatchException(a) {
        try {
            middleService.catchException();
        } catch (Exception e) {
            log.error("Catch Exception:", e); }}public void callMoreCatchException(a) {
        try {
            addOneLayerCatch();
        } catch (Exception e) {
            log.error("Catch Exception:", e); }}private void addOneLayerCatch(a) {
        try {
            middleService.catchException();
        } catch (Exception e) {
            throw newRuntimeException(e); }}}Copy the code

Then we tested the two methods of FacadeService through JMH, and the performance test results are as follows:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark. CallCatchException THRPT 80 25.897 + / - 0.232 ops/ms ExceptionBenchmark. CallUnCatchException THRPT 80 28.128 + / - 2.608 ops/ms ExceptionBenchmark callMoreCatchException THRPT 80 21.477 ± 1.234 OPS /msCopy the code

From the test reports we can see that the method call stack is less wrapped in try/catch and the performance is actually better. The real time consuming area for Exception handling is the construction of the Exception instance, because it takes a snapshot of the stack, which is a relatively heavy operation.

You know the part where exception handling takes a lot of time. Another way to think about it is, if a program is already abnormal, after all, the program is not running correctly, then why pursue performance? Or how does this affect the real world?

We know that if the processing of a section of logic is slow, the number of processing times per unit time will be affected. So we use JMH to do a test again, and first look at the test report:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark. CallCatchException sample 10184827 0.314 + / - 0.001 ms/op ExceptionBenchmark. CallCatchException: callCatchException p0.00 sample 0.043 ms/op ExceptionBenchmark. CallCatchException: callCatchException p0.50 sample 0.084 ms/op ExceptionBenchmark. CallCatchException: callCatchException p0.90 sample 0.443 ms/op ExceptionBenchmark. CallCatchException: callCatchException p0.95 sample 2.118 ms/op ExceptionBenchmark. CallCatchException: callCatchException p0.99 sample 3.936 ms/op ExceptionBenchmark. CallCatchException: callCatchException p0.999 sample 10.355 ms/op ExceptionBenchmark. CallCatchException: callCatchException p0.9999 sample 16.007 ms/op ExceptionBenchmark. CallCatchException: callCatchException p1.00 sample 369.623 ms/op ExceptionBenchmark. CallUnCatchException sample 10231220 0.313 + / - 0.001 ms/op ExceptionBenchmark. CallUnCatchException: callUnCatchException p0.00 sample 0.046 ms/op ExceptionBenchmark. CallUnCatchException: callUnCatchException p0.50 sample 0.083 ms/op ExceptionBenchmark. CallUnCatchException: callUnCatchException p0.90 sample 0.421 ms/op ExceptionBenchmark. CallUnCatchException: callUnCatchException p0.95 sample 2.097 ms/op ExceptionBenchmark. CallUnCatchException: callUnCatchException p0.99 sample 3.969 ms/op ExceptionBenchmark. CallUnCatchException: callUnCatchException p0.999 sample 10.682 ms/op ExceptionBenchmark. CallUnCatchException: callUnCatchException p0.9999 sample 16.663 ms/op ExceptionBenchmark. CallUnCatchException: callUnCatchException p1.00 sample 500.695 ms/opCopy the code

It doesn’t really make sense to talk about performance only for abnormal scenarios, but in the real environment, because the number of processing times per unit time is reduced, it means that your system throughput is not increasing, that is, some abnormal scenarios, affecting the experience in normal scenarios.

Going back to the above issue, because exception handling affects performance, it ultimately translates into reduced overall system throughput at the system level. So, do we just let it go and not deal with it? I believe that every software engineer who has checked the problem in the log can not fully agree with this view, how to deal with it? Before we answer that question, let’s finish the performance issue. Where Exception instantiation takes time is where StackTrace is built.

public synchronized Throwable fillInStackTrace(a) {
    if(stackTrace ! =null|| backtrace ! =null) {
        fillInStackTrace(0);
        stackTrace = UNASSIGNED_STACK;
    }
    return this;
}
Copy the code

So is there a way not to build StackTrace? If you are using JDK1.7 or later, we can do this by rewriting the constructor, adding the following method to the Exception class in JDK1.7:

/**
 * Constructs a new exception with the specified detail message,
 * cause, suppression enabled or disabled, and writable stack
 * trace enabled or disabled.
 *
 * @param  message the detail message.
 * @param cause the cause.  (A {@code null} value is permitted,
 * and indicates that the cause is nonexistent or unknown.)
 * @param enableSuppression whether or not suppression is enabled
 *                          or disabled
 * @param writableStackTrace whether or not the stack trace should
 *                           be writable
 * @since1.7 * /
protected Exception(String message, Throwable cause,
                    boolean enableSuppression,
                    boolean writableStackTrace) {
    super(message, cause, enableSuppression, writableStackTrace);
}
Copy the code

The above constructor can specify whether or not to build StackTrace with a parameter, so let’s define our own exception class to complete the above test case. The exception class looks like this:

public class MyException extends RuntimeException {

    public MyException(Throwable cause, String message) {
        super(message, cause, false.false); }}Copy the code

Then we add a new method to MiddleService, as shown in the code below:

public class MiddleService {

    public void catchMyException(a) {
        try {
            oneExceptionService.createSomeException();
        } catch (Exception e) {
            throw newMyException(e); }}}Copy the code

Add a new method to FacadeService, as follows:

@Slf4j
public class FacadeService {

    public void callCatchMyException(a) {
        try {
            middleService.catchMyException();
        } catch (Exception e) {
            log.error("Catch Exception:", e); }}}Copy the code

Another round of testing was conducted through JMH, and the test report was as follows:

Benchmark Mode Cnt Score Error Units ExceptionBenchmark. CallCatchException THRPT 80 25.897 + / - 0.232 ops/ms ExceptionBenchmark. CallUnCatchException THRPT 80 28.128 + / - 2.608 ops/ms ExceptionBenchmark callMoreCatchException THRPT 21.477 + / - 1.234 80 ops/ms ExceptionBenchmark. CallCatchMyException THRPT 80 29.445 + / - 0.370 ops/msCopy the code

We can see significant performance improvements in the reports. But why is this rarely used in real development? Because StackTrace can help us locate problems, missing StackTrace can cause you to lose a lot of critical information when troubleshooting problems, so it is not recommended.

At the same time, similar optimizations are made by the JVM itself to improve performance when exceptions of the same type occur frequently in a short period of time (the default is actually high), resulting in incomplete information on the exception stack. You need to specify the parameters * * – XX: – OmitStackTraceInFastThrow * *, can cancel the optimization and exception stack for complete information.

Correct posture for handling Exception

Before we talk about the correct way to handle Exception, let’s go back to the example code above, FacadeService, Take a look at the difference between callCatchException and callUnCatchException and the exception stack information for callMoreCatchException.

- callUnCatchException -
java.lang.IllegalArgumentException: Have some exception
	at me.hergootian.exception.OneExceptionService.createSomeException(OneExceptionService.java:9)
	at me.hergootian.exception.MiddleService.unCatchException(MiddleService.java:15)
	at me.hergootian.exception.FacadeService.and(FacadeService.java:63)
	at me.hergootian.exception.FacadeService.callUnCatchException(FacadeService.java:16)
	......


- callCatchException -
java.lang.RuntimeException: java.lang.IllegalArgumentException: Have some exception
	at me.hergootian.exception.MiddleService.catchException(MiddleService.java:30)
	at me.hergootian.exception.FacadeService.then(FacadeService.java:59)
	at me.hergootian.exception.FacadeService.callCatchException(FacadeService.java:24)
	at me.hergootian.exception.ExceptionTest.testCallCatchException(ExceptionTest.java:15)
    ......
Caused by: java.lang.IllegalArgumentException: Have some exception
	at me.hergootian.exception.OneExceptionService.createSomeException(OneExceptionService.java:9)
	at me.hergootian.exception.MiddleService.catchException(MiddleService.java:28)
	... 25 common frames omitted
	
	
- callMoreCatchException -
java.lang.Exception: java.lang.RuntimeException: java.lang.IllegalArgumentException: Have some exception
    at me.hergootian.exception.FacadeService.more(FacadeService.java:50)
    at me.hergootian.exception.FacadeService.callMultiCatchException(FacadeService.java:40)
    at me.hergootian.exception.ExceptionTest.testCallMultiCatchException(ExceptionTest.java:27)
    ......
Caused by: java.lang.RuntimeException: java.lang.IllegalArgumentException: Have some exception
    at me.hergootian.exception.MiddleService.catchException(MiddleService.java:30)
    at me.hergootian.exception.FacadeService.more(FacadeService.java:48)
    ... 24 common frames omitted
Caused by: java.lang.IllegalArgumentException: Have some exception
    at me.hergootian.exception.OneExceptionService.createSomeException(OneExceptionService.java:9)
    at me.hergootian.exception.MiddleService.catchException(MiddleService.java:28)
    ... 25 common frames omitted
Copy the code

With the stack information in the log, not only for performance, but also for stack clarity, we don’t want to catch too much and rewrap exceptions.

Remember not to catch and repackage exceptions at every level in the system

Although we discussed the catch Exception and the fact that instantiating Exception is a time-consuming operation, we know from the above Exception stack information, as well as our daily system maintenance and troubleshooting that the unclear Exception stack interferes with our problem location.

Catching exceptions at each level may seem like it makes the system more robust, but performance is secondary, and a cluttering and complex exception stack will keep you coming back to thumbs up. In fact, each layer is trying to replenish the abnormal, the problem lies in your own system stratification is not clear, coding is not considered comprehensively caused by.

Throw early, Catch late

There is a principle of “Throw early” and “Catch late” for exception handling, which means that we should judge/Catch as early as possible where exceptions may occur during system operation and expose more specific errors that may cause system problems. Catch the exception where the system is appropriate, or able to handle it, and handle it accordingly.

Do not branch from exceptions

On some systems there will be branches of the exception stream, and for performance reasons alone, this approach is no better than if/else.

Sometimes, however, we will validate the input and then inform the method caller as a parameter class exception. Finally, make defensive checks on the outermost layer of the system. If the Exception is clear and does not require an additional Exception stack, try overwriting the Exception constructor and unbuilding StackTrace to improve performance and load the system.