转载

Java全局异常处理,你不知道的骚操作(含hotspot源码分析)

关于 Java 全局异常处理,网上一搜都是说 SpringMVC 的全局异常处理。确实,使用 Spring Boot 开发也好,使用 SSM 也好,都可以使用 SpringMVC 的全局异常处理,也是最好不过,因为出现异常我们也要响应数据给前端。话说,关于 SpringMVC 的全局异常处理,你知道原理了吗?其实可以一句话概括,任何请求都先经过 DispatchServlet

如果应用不是一个 SpringMVC 应用呢?可能很多人都不知道, Java 自己就提供有全局异常处理。这个我还是从我的前领导那里听来的。但这个不适用于接口的全局异常处理。本篇将介绍如何使用 Java 提供的全局异常处理,以及分析一点 hotspot 虚拟机的源码,让大家了解虚拟机是如何将异常交给全局异常处理器处理的。

全局异常处理Demo

main 方法中设置全局异常处理器 DefaultUncaughtExceptionHandler ,从名字中也可以看出,这是用于处理未捕获的异常的。不一定是从 main 方法中设置,在 spring 应用中,可以监听 spring 初始化完成事件,再设置。(如果是web应用,还是使用 SpringMVC 的全局异常处理。)

public static void main(String[] args) throws InterruptedException {
        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("这里是全局异常处理 ====> " + t.getId() + "==> "+e.getLocalizedMessage());
            }
        });
}
复制代码

有朋友可能会觉得奇怪了,为什么是设置在 Thread 里面。虽然是设置在 Thread 里面,但这是一个静态变量。

要知道,我们所写的代码都是在线程里面跑的。一个线程对应一个 Java 虚拟机栈,异常是在栈桢中发生的(即方法)。在调用发生异常时,栈桢出栈,异常一层层往上抛出,并写入调用栈信息。在整个调用栈中,如果都没有方法捕获异常,那么 Java 虚拟机将从当前线程的 Thread 对象中获取一个异常处理器,如果有,则交给异常处理器处理。走到这一步,意味着线程即将退出,这也是我从 hotspot 源码中寻找入口的依据。

当然,如果是设置了针对某个线程的异常处理器,则该线程发现未捕获异常时,会使用该线程设置的异常处理器,否则会使用全局默认的。这里没懂没关系,后面会详细分析。

我们接着把例子看完。在 main 方法中创建多个线程,并在线程的 run 方法中抛出异常。

private static class TaskThread extends Thread {
    @Override
    public void run() {
       throw new NullPointerException("thread-" + Thread.currentThread().getId() + " Exception");
    }
}
/**
 * 在main方法中调用startThread(),
 */
public static void startThread(){
    for (int i = 0; i < 10; i++) {
        new TaskThread().start();
    }
    // 不让主线程退出
    System.in.read();
}
复制代码

程序运行结果:

这里是全局异常处理 ====> 13==> thread-13 Exception
这里是全局异常处理 ====> 16==> thread-16 Exception
这里是全局异常处理 ====> 15==> thread-15 Exception
........
复制代码

前面提到,我们还可以针对某个线程设置单独的异常处理器,且优先级会高于全局默认的。如果为某个线程单独设置异常处理器,那么就这个线程而言,默认的全局异常处理器将不起作用。我们来修改一下前面例子的 startThread 方法,验证一下,其它不变。

/**
 * 在main方法中调用startThread(),
 */
public static void startThread(){
    for (int i = 0; i < 10; i++) {
        Thread thread = new TaskThread();
        if (i == 0) {
            thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println("这是为当前线程设置的异步处理器。===> " + t.getId());
                }
            });
        }
        thread.start();
    }
    // 不让主线程退出
    System.in.read();
}
复制代码

程序输出结果如下:

这是为当前线程设置的异步处理器。===> 13
这里是全局异常处理 ====> 16==> thread-16 Exception
这里是全局异常处理 ====> 15==> thread-15 Exception
.......
复制代码

很显然, id 等于 13 的线程走了单独的异常处理器,而其它线程则走全局默认异常处理器。有朋友可能会好奇,为什么线程 id13 开始,其实我在以前的文章也提到过,只是忘记是哪篇了。 id0 的是 mian 线程,然后接着就是虚拟机的线程,以及 gc 垃圾回收的线程,由于我电脑 cpu9代i7 六核十二线程,所以 gc 线程数比较多。题外话就不扯太多了。

从源码中寻找答案

Java 的全局异常处理是从 jdk1.5 开始加入的新特性,我也不确定是不是 1.5 ,注释上写的。先看下异常处理器 UncaughtExceptionHandler

@FunctionalInterface
public interface UncaughtExceptionHandler {
    /**
      * 未捕获异常处理
      */
    void uncaughtException(Thread t, Throwable e);
}
复制代码

如果在 uncaughtException 方法中,比如写日记记录日常信息,结果因为写日记时发生 IO 异常,或者其它异常,不管是什么异常,此方法抛出的异常都将会被 Java 虚拟机忽略,因为线程已经要结束退出了。

Thread 中声明了两个 UncaughtExceptionHandler 类型的变量,一个是静态变量。其中非静态变量是针对当前线程起作用的,声明为 volatile 原因是可能是其它线程调用设置的;另一个静态变量就是全局默认的。

public class Thread implements Runnable {
    // null unless explicitly set
    private volatile UncaughtExceptionHandler uncaughtExceptionHandler;

    // null unless explicitly set
    private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
}
复制代码

我在看源码的时候,是通过搜索查看哪个地方使用了这两个 UncaughtExceptionHandler ,最后在 dispatchUncaughtException 方法上的注释看到了关键信息。当有未捕获异常抛出时, java 虚拟机会调用当前线程的 Thread 对象的 dispatchUncaughtException 方法。

/**
     * Dispatch an uncaught exception to the handler. This method is
     * intended to be called only by the JVM.
     */
    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }
复制代码

dispatchUncaughtException 方法中,调用了 getUncaughtExceptionHandler 方法获取 UncaughtExceptionHandler 异常处理器对象,再把异常交给拿到的异常处理器去处理。

public UncaughtExceptionHandler getUncaughtExceptionHandler() {
    return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group;
}
复制代码

这里非常奇怪,并没有用到 defaultUncaughtExceptionHandler 这个静态变量。如果当前线程对象没有设置异常处理器,就返回一个 group 。首先看到这,我们就能明白,为什么针对某个线程设置的异常处理器会被优先使用。

group 其实是 ThreadGroup 对象。在 Java 中,每个线程都有一个所属的线程组。在调用 start 方法时,会将当前线程加入一个线程组,而如果在创建 Thread 对象时,没有传入线程组 ThreadGroup ,则会获取当前线程的线程组,可能就是 main 线程所属的线程组了,是不是有点绕,自己看下源码就很好理解了。

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
}
复制代码

ThreadGroup 实现了 UncaughtExceptionHandler 接口,也就说得通,为什么是返回一个 group 了,然后看 ThreadGroupuncaughtException 方法。

public void uncaughtException(Thread t, Throwable e) {
    if (parent != null) {
        parent.uncaughtException(t, e);
    } else {
        Thread.UncaughtExceptionHandler ueh =
            Thread.getDefaultUncaughtExceptionHandler();
        ueh.uncaughtException(t, e);
    }
}
复制代码

线程组还有父线程组,这个太绕,我们不理它。可以看到, ThreadGroupuncaughtException 方法中,会调用 Thread.getDefaultUncaughtExceptionHandler(); 方法获取设置的默认异常处理器,这便是我们设置的全局默认异常处理器。

其实细心看代码,你会发现, ThreadGroupuncaughtException 注释是 1.0 版本就已经存在了。然后我看 hotspot 源码,发现它会兼容旧版本,即 Thread 对象不存在 dispatchUncaughtException 方法时,是转为调用 ThreadGroupuncaughtException 方法的。

接下来我们就要看 hotspot 源码了,看下 hotspot 是怎么调用异常处理器处理异常的。不知道看到这,你是否还记得前面说的一句话,异常处理器会被调用,说明当前 Java 虚拟机栈上没有一个栈桢去捕获异常,也意味着当前线程即将退出。因此源码的入口就是 thread.cpp 类的 exit 方法。下面我将以图片方式贴代码了。

Java全局异常处理,你不知道的骚操作(含hotspot源码分析)
(源码所在文件: vm/runtime/thread.cpp

)

Java全局异常处理,你不知道的骚操作(含hotspot源码分析)

c++ 的知识我就不说了。看图中的红框 0 ,调用 resolve_virtual_call 方法获取调用信息,即 CallInfo 。传递的参数分别是 CallInfo 的指针(分配在 c++ 线程栈上的)、当前线程对象 Thread 、线程 ThreadClass 类结构信息 KlassdispatchUncaughtException 的方法名、方法签名等。

Java全局异常处理,你不知道的骚操作(含hotspot源码分析)
Java全局异常处理,你不知道的骚操作(含hotspot源码分析)

继续看 thread.cppexit 方法,红框 1 是判断 Thread 是否存在 dispatchUncaughtException 方法,即前面说的兼容旧版本的。如果存在,则调用当前线程的 Thread 对象的 dispatchUncaughtException 方法。

Java全局异常处理,你不知道的骚操作(含hotspot源码分析)

如果是 jdk1.0 ,那么不会走红框 1 的代码,而是走红框 2 ,获取当前线程的 ThreadGroup 对象,调用它的 uncaughtException 方法。调用 call_virtual 方法去执行 java 代码。参数 1 便是 Thread 对象,参数 2 便是异常对象。

Java全局异常处理,你不知道的骚操作(含hotspot源码分析)

看完本篇,我相信大家都已经了解了 Java 默认异常处理器是怎么被调用的了。那么,学习这个我们工作中是否能够用得到,那就看大家各自的发挥了。

原文  https://juejin.im/post/5e662d246fb9a07cb427e538
正文到此结束
Loading...