Java异常

Exception和Error

  1. 异常基础
    Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
    Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
    Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
    Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。前面我介绍的不可查的Error,是 Throwable 不是 Exception。
    不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
    avatar
    随着 Java 语言的发展,引入了一些更加便利的特性,比如 try-withresources和 multiple catch,具体可以参考下面的代码段。在编译时期,会自动生成相应的处理逻辑,比如,自动按照约定俗成 close 那些扩展了 AutoCloseable 或者 Closeable的对象。

    1
    2
    3
    4
    5
    6
    try (BufferedReader br = new BufferedReader(…);
    BufferedWriter writer = new BufferedWriter(…)) {// Try-with-resources
    // do something
    catch ( IOException | XEception e) {// Multiple catch
    // Handle it
    }
  2. 异常处理的两个基本原则

  • 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常
  • 不要生吞(swallow)异常
    有的时候,我们会根据需要自定义异常,这个时候除了保证提供足够的信息,还有两点需要考虑:
  • 是否需要定义成 Checked Exception,因为这种类型设计的初衷更是为了从异常情况恢复,作为异常设计者,我们往往有充足信息进行分类。
  • 在保证诊断信息足够的同时,也要考虑避免包含敏感信息,因为那样可能导致潜在的安全问题。如果我们看 Java 的标准类库,你可能注意到类似 java.net.ConnectException,出错信息是类似“ Connection refused (Connection refused)”,而不包含具体的机器名、IP、端口等,一个重要考量就是信息安全。类似的情况在日志中也有,比如,用户数据一般是不可以输出到日志里面的。

强引用、软引用、弱引用、幻象引用

  • 不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。
  1. 强引用(“Strong” Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。
  2. 软引用(SoftReference),是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
  3. 弱引用(WeakReference)并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。
  4. 幻象引用,有时候也翻译成虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem清理机制,也有人利用幻象引用监控对象的创建和销毁。
  • 对象可达性
    简单描述了对象生命周期和不同可达性状态,以及不同状态可能的改变关系,可能未必 100% 严谨,来阐述下可达性的变化。
    avatar
  1. 强可达(Strongly Reachable),就是当一个对象可以有一个或多个线程可以不通过各种引用访问到的情况。比如,我们新创建一个对象,那么创建它的线程对它就是强可达。
  2. 软可达(Softly Reachable),就是当我们只能通过软引用才能访问到对象的状态。
  3. 弱可达(Weakly Reachable),类似前面提到的,就是无法通过强引用或者软引用访问,只能通过弱引用访问时的状态。这是十分临近finalize 状态的时机,当弱引用被清除的时候,就符合 finalize 的条件了。
  4. 幻象可达(Phantom Reachable),上面流程图已经很直观了,就是没有强、软、弱引用关联,并且 finalize 过了,只有幻象引用指向这个对象的时候。
  5. 当然,还有一个最后的状态,就是不可达(unreachable),意味着对象可以被清除了。
    判断对象可达性,是 JVM 垃圾收集器决定如何处理对象的一部分考虑。所有引用类型,都是抽象类 java.lang.ref.Reference 的子类,你可能注意到它提供了 get()方法:
    avatar
    除了幻象引用(因为 get 永远返回 null),如果对象还没有被销毁,都可以通过 get 方法获取原有对象。这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态!所以,对于软引用、弱引用之类,垃圾收集器可能会存在二次确认的问题,以保证处于弱引用状态的对象,没有改变为强引用。
  • 引用队列(ReferenceQueue)使用
    在创建各种引用并关联到响应对象时,可以选择是否需要关联引用队列,JVM 会在特定时机将引用 enqueue 到队列里,可以从队列里获取引用(remove 方法在这里实际是有获取的意思)进行相关后续逻辑。尤其是幻象引用,get 方法只返回 null,如果再不指定引用队列,基本就没有意义了。利用引用队列,我们可以在对象处于相应状态时(对于幻象引用,就是前面说的被 finalize 了,处于幻象可达状态),执行后期处理逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Object counter = new Object();
    ReferenceQueue refQueue = new ReferenceQueue<>();
    PhantomReference<Object> p = new PhantomReference<>(counter, refQueue);
    counter = null;
    System.gc();
    try {
    // Remove 是一个阻塞方法,可以指定 timeout,或者选择一直阻塞
    Reference<Object> ref = refQueue.remove(1000L);
    if (ref != null) {
    // do something
    }
    } catch (InterruptedException e) {
    // Handle it
    }
  • 显式地影响软引用垃圾收集
    软引用通常会在最后一次引用后,还能保持一段时间,默认值是根据堆剩余空间计算的(以 M bytes 为单位)。从 Java 1.3.1 开始,提供了 -
    XX:SoftRefLRUPolicyMSPerMB 参数,我们可以以毫秒(milliseconds)为单位设置。比如,下面这个示例就是设置为 3 秒(3000 毫秒)。

    1
    -XX:SoftRefLRUPolicyMSPerMB=3000
  • 诊断 JVM 引用情况
    HotSpot JVM 自身便提供了明确的选项(PrintReferenceGC)去获取相关信息,我指定了下面选项去使用 JDK 8 运行一个样例应用:

    1
    2
    3
    4
    -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintReferenceGC
    这是 JDK 8 使用 ParrallelGC 收集的垃圾收集日志,各种引用数量非常清晰。
    1 0.403: [GC (Allocation Failure) 0.871: [SoftReference, 0 refs, 0.0000393 secs]0.871:
    JDK 9 对 JVM 和垃圾收集日志进行了广泛的重构,类似 PrintGCTimeStamps 和PrintReferenceGC 已经不再存在
  • Reachability Fence
    除了前面介绍的几种基本引用类型,我们也可以通过底层 API 来达到强引用的效果,这就是所谓的设置reachability fence。需要一个方法,在没有强引用情况下,通知 JVM 对象是在被使用的。

    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
    class Resource {
    private static ExternalResource[] externalResourceArray = ...
    int myIndex; Resource(...) {
    myIndex = ...
    externalResourceArray[myIndex] = ...;
    ...
    }
    protected void finalize() {
    externalResourceArray[myIndex] = null;
    ...
    }
    public void action() {
    try {
    // 需要被保护的代码
    int i = myIndex;
    Resource.update(externalResourceArray[i]);
    } finally {
    // 调用 reachbilityFence,明确保障对象 strongly reachable
    Reference.reachabilityFence(this);
    }
    }
    private static void update(ExternalResource ext) {
    ext.status = ...;
    }
    }
    方法 action 的执行,依赖于对象的部分属性,所以被特定保护了起来。否则,如果我们在代码中像下面这样调用,那么就可能会出现困扰,因为没有强引用指向我们创建出来的Resource 对象,JVM 对它进行 finalize 操作是完全合法的。
    参考地址:https://docs.oracle.com/javase/9/docs/api/java/lang/ref/Reference.html#reachabilityFence-java.lang.Object-