Java 异常处理

前言

1、异常

  • 导致程序的正常流程被中断的事件,叫做异常

  • 比如要打开某个文件,这个文件是有可能不存在的。

  • Java 中通过 new FileInputStream(f) 试图打开某文件,就有可能抛出文件不存在异常 FileNotFoundException

  • 如果不处理该异常,就会有编译错误。

2、异常处理

  • 异常处理常见手段 trycatchfinallythrows

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    try {
    // 可能抛出异常的代码
    ...
    } catch(Exception e) {
    // 捕捉到异常,处理异常的代码
    ...
    } finally {
    // 无论是否出现异常,都会执行的代码
    ...
    }
  • 1
    2
    3
    4
    5
    6
    7
    try (/* 初始化代码 */ ...) {
    ...
    } catch(Exception e) {
    ...
    } finally {
    ...
    }
  • 如果 try {} 里有一个 return 语句

    • try 里的 return 和 finally 里的 return 都会执行,但是当前方法只会采纳 finally 中 return 的值。
  • final, finally, finalize 的区别

    • final 修饰类,方法,基本类型变量,引用的时候分别有不同的意思。
      • 修饰类 表示该类不能被继承。
      • 修饰方法 表示该方法不能被重写。
      • 修饰基本类型变量 表示该变量只能被赋值一次。
      • 修饰引用 表示该引用只有一次指向对象的机会。
    • finally 是用于异常处理的场面,无论是否有异常抛出,都会执行。
    • finalize 是 Object 的方法,所有类都继承了该方法。当一个对象满足垃圾回收的条件,并且被回收的时候,其 finalize() 方法就会被调用。

2.1 捕捉异常

  • 捕捉异常

    • 将可能抛出异常的代码放在 try 里。
    • 如果没有抛出异常,就会顺序往下执行,并且不执行 catch 块中的代码。如果有抛出异常,try 里的代码会立即终止,程序流程会运行到对应的 catch 块中。
    • 无论是否出现异常,finally 中的代码都会被执行。
  • 使用异常的父类也可以 catch 住异常。

  • 多异常捕捉

    • 分别进行捕捉

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      File f = new File("d:/LOL.exe");

      // 多异常分别进行 catch
      try {
      FileInputStream fis = new FileInputStream(f);

      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
      Date d = sdf.parse("2016-06-03");
      } catch (FileNotFoundException e) { // 捕捉 FileInputStream 的异常
      System.out.println("文件不存在");
      e.printStackTrace();
      } catch (ParseException e) { // 捕捉 SimpleDateFormat 的异常
      System.out.println("日期格式解析错误");
      e.printStackTrace();
      }
    • 统一进行捕捉

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      File f = new File("d:/LOL.exe");

      // 多异常统一进行 catch
      try {
      FileInputStream fis = new FileInputStream(f);

      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
      Date d = sdf.parse("2016-06-03");
      } catch (FileNotFoundException | ParseException e) { // 捕捉 FileInputStream 和 SimpleDateFormat 的异常
      if (e instanceof FileNotFoundException) {
      System.out.println("文件不存在");
      }
      if (e instanceof ParseException) {
      System.out.println("日期格式解析错误");
      }

      e.printStackTrace();
      }

2.2 抛出异常

  • 抛出异常

    1
    2
    3
    4
    5
    6
    private static void method2(file) throws FileNotFoundException {      // throws 声明方法可能抛出异常

    if (ObjectUtils.isEmpty(file)) {
    throw new FileNotFoundException(9006, "file not found!"); // throw 抛出异常
    }
    }
  • throws 与 throw 的区别

    • throws 出现在方法声明上,而 throw 通常都出现在方法体内。
    • throws 表示出现异常的一种可能性,并不一定会发生这些异常。
    • throw 则是抛出了异常,执行 throw 则一定抛出了某个异常对象。

3、异常分类

  • 异常分类为可查异常(CheckedException),运行时异常(RuntimeException)和错误(Error)3 种。

  • 其中,运行时异常和错误又叫非可查异常。

异常类型 是否可查 说明
可查异常 即必须进行处理的异常,要么 try catch 住,要么往外抛,谁调用,谁处理,如果不处理,编译器,就不让你通过
运行时异常 否,非可查异常 不是必须进行 try catch 的异常,在编写代码的时候,依然可以使用 try catch throws 进行处理,
与可查异常不同之处在于,即便不进行 try catch,也不会有编译错误
错误 否,非可查异常 指的是系统级别的异常,通常是内存用光了,在默认设置下,一般 Java 程序启动的时候,最大可以使用 16m 的内存,
与运行时异常一样,错误也是不要求强制捕捉的
  • 运行时异常与一般异常有何异同

    • 运行时异常 又叫做非可查异常,在编译过程中,不要求必须进行显示捕捉。
    • 一般异常又叫做可查异常,在编译过程中,必须进行处理,要么捕捉,要么通过 throws 抛出去。
  • Error 和 Exception 的区别

    • Error 和 Exception 都实现了 Throwable 接口。
    • Error 指的是 JVM 层面的错误,比如内存不足 OutOfMemoryError。
    • Exception 指的是代码逻辑的异常,比如下标越界 OutOfIndexException。

3.1 异常类

  • Throwable 类ExceptionError 都继承了该类,所以在捕捉的时候,也可以使用 Throwable 进行捕捉。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package java.lang;

    /**
    * The {@code Throwable} class is the superclass of all errors and
    * exceptions in the Java language. Only objects that are instances of this
    * class (or one of its subclasses) are thrown by the Java Virtual Machine or
    * can be thrown by the Java {@code throw} statement. Similarly, only
    * this class or one of its subclasses can be the argument type in a
    * {@code catch} clause.
    *
    * @author unascribed, Josh Bloch
    * @jls 11.2 Compile-Time Checking of Exceptions
    * @since JDK1.0
    */

    public class Throwable implements Serializable {

    }
  • Exception 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package java.lang;

    /**
    * The class {@code Exception} and its subclasses are a form of
    * {@code Throwable} that indicates conditions that a reasonable
    * application might want to catch.
    *
    * @author Frank Yellin
    * @see java.lang.Error
    * @jls 11.2 Compile-Time Checking of Exceptions
    * @since JDK1.0
    */

    public class Exception extends Throwable {

    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package java.lang;

    /**
    * {@code RuntimeException} is the superclass of those
    * exceptions that can be thrown during the normal operation of the
    * Java Virtual Machine.
    *
    * @author Frank Yellin
    * @jls 11.2 Compile-Time Checking of Exceptions
    * @since JDK1.0
    */

    public class RuntimeException extends Exception {

    }
    1
    2
    3
    4
    5
    package com.sun.xml.internal.ws.api.model;

    public interface CheckedException {

    }
  • Error 类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package java.lang;

    /**
    * An {@code Error} is a subclass of {@code Throwable}
    * that indicates serious problems that a reasonable application
    * should not try to catch. Most such errors are abnormal conditions.
    * The {@code ThreadDeath} error, though a "normal" condition,
    * is also a subclass of {@code Error} because most applications
    * should not try to catch it.
    *
    * @author Frank Yellin
    * @see java.lang.ThreadDeath
    * @jls 11.2 Compile-Time Checking of Exceptions
    * @since JDK1.0
    */

    public class Error extends Throwable {

    }

3.2 常见异常

异常 介绍
Exception 可查异常
IOException 由于文件未找到、未打开或者 I/O 操作不能进行而引起异常
ClassNotFoundException 未找到指定名字的类或接口引起异常
CloneNotSupportedException 程序中的一个对象引用 Object 类的 clone 方法,但 此对象并没有连接 Cloneable 接口,从而引起异常
InterruptedException 当一个线程处于等待状态时,另一个线程中断此线程,从 而引起异常,有关线程的内容,将在下一章讲述
NoSuchMethodException 所调用的方法未找到,引起异常
FileNotFoundException 未找到指定文件引起异常
EOFException 未完成输入操作即遇文件结束引起异常
RuntimeException 运行时异常
ArithmeticException 算术异常,由于除数为 0 引起的异常
ArrayStoreException 由于数组存储空间不够引起的异常
ClassCastException 类型转换异常,当把一个对象归为某个类,但实际上此对象并不是由这个类 创建的,也不是其子类创建的,则会引起异常
ConcurrentModificationException 同步修改异常,遍历一个集合的时候,删除集合的元素,就会抛出该异常
NegativeArraySizeException 数组长度是负数,则产生异常
NullPointerException 空指针异常,程序试图访问一个空的数组中的元素或访问空的对象中的 方法或变量时产生异常
NumberFormatException 字符的 UTF 代码数据格式有错引起异常
IndexOutOfBoundsException 数组下标越界异常
IllegalMonitorStateException 监控器状态出错引起的异常
IllegalThreadException 线程调用某个方法而所处状态不适当,引起异常
SecurityException 由于访问了不应访问的指针,使安全性出问题而引起异常
StringIndexOutOfBoundsException 访问字符串序号越界,引起异常
Error 错误
OutofMemoryException 用 new 语句创建对象时,如系统无法为其分配内存空 间则产生异常
IndexOutOfBoundsExcention 由于数组下标越界或字符串访问越界引起异常
Illega1AccessExcePtion 试图访问一个非 public 方法
ArrayIdexOutOfBoundsException 访问数组元素下标越界,引起异常

4、自定义异常

  • 创建 自定义异常。创建一个自定义异常类,并继承 Exception,提供两个构造方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // AdviceNotFoundException.java

    package com.qianchia.support.exception;

    import java.io.Serializable;

    // 自定义异常类
    public class AdviceNotFoundException extends NotFoundException implements Serializable {

    public AdviceNotFoundException() { // 无参的构造方法
    }

    public AdviceNotFoundException(String message) { // 带参的构造方法,并调用父类的对应的构造方法
    super(message);
    }

    public AdviceNotFoundException(int code, String message) { // 带参的构造方法,并调用父类的对应的构造方法
    super(code, message);
    }
    }
  • 抛出 自定义异常。创建一个自定义异常类实例,通过 throw 抛出该异常,当前方法通过 throws 抛出该异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // AdviceService.java

    package com.qianchia.service;

    import com.qianchia.support.exception.AdviceNotFoundException;

    public interface AdviceService {

    void deleteAdvice(int id) throws AdviceNotFoundException; // 声明 可能抛出异常的方法
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // AdviceServiceImpl.java

    package com.qianchia.service.impl;

    import com.qianchia.dao.AdviceDao;
    import com.qianchia.po.Advice;

    import com.qianchia.service.AdviceService;
    import com.qianchia.support.exception.AdviceNotFoundException;

    @Service
    public class AdviceServiceImpl implements AdviceService {

    @Override
    public void deleteAdvice(int id) throws AdviceNotFoundException { // 实现 可能抛出异常的方法

    Advice advice = adviceDao.getById(id);
    if (ObjectUtils.isEmpty(advice)) {
    throw new AdviceNotFoundException(9006, "Advice not found!"); // 抛出异常
    }
    adviceDao.delete(id);
    }
    }
  • 捕捉 自定义异常。使用 catch 捕捉异常。

    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
    // AdviceController.java

    package com.qianchia.controller.manage;

    import com.qianchia.po.Advice;

    import com.qianchia.service.AdviceService;
    import com.qianchia.support.exception.AdviceNotFoundException;

    @RestController
    @RequestMapping("/api/v1")
    public class AdviceController {

    @Autowired
    private AdviceService adviceService;

    @DeleteMapping("/advice/{id:\\d+}")
    public Response deleteAdvice(@PathVariable("id") int id) {

    try { // 处理 可能抛出异常的方法
    adviceService.deleteAdvice(id);
    return new Response<>();
    } catch (AdviceNotFoundException e) { // 捕捉异常
    return new Response(e.getCode(), e.getMessage());
    }
    }
    }

5、OOM 内存溢出

  • OOM 全称 “Out Of Memory”,内存溢出,即 “内存用完了”,来源于 java.lang 包下的一个类 OutOfMemoryError

  • OOM 属于 Error,一般当 JVM 虚拟机内存不够用的时候,就会抛出该错误。

5.1 堆内存溢出

  • java.lang.OutOfMemoryError: java heap space 堆内存溢出,这个 OOM 是最常见的。

  • JVM 堆是 Java 存放对象的位置,当堆空间已经被占满到无法再创建新的对象时,就会抛出这个 OOM 错误。

  • 下面这个例子是通过不断给加大对象在堆内存中占用的空间,最终造成了堆内存溢出。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 事先把堆内存配小,方便测试结果
    // -verbose:gc -Xms10M -Xmx10M -XX:MaxDirectMemorySize=5M -Xss160k -XX:+PrintGCDetails

    public class HeapSpaceTest {

    public static void main(String[] args) {

    String str = "test";

    // 起一个无限循环往 str 里加内容,直到撑爆堆
    while (true){
    str += str + new Random(1111111111) + new Random(1111111111);
    }
    }
    }
  • 可能会造成堆内存溢出的原因

    • 创建了一个超级大的对象。
    • 快速的创建海量的对象,直到最后来不及 GC。
    • 内存泄漏,一些对象创建之后没有释放,比如一些文件对象。
    • 过度使用 finalizer 终结器。
  • 堆内存溢出的解决方法

    • 使用 -Xmx 参数调高堆内存空间。
    • 对某些场景做限流,降低存在短期内创建海量对象的可能。
    • 排查是否出现内存泄漏。
  • finalizer 终结方法,是每个类都会有的特殊方法,当其被 JVM 调用时,就能帮助 JVM 清理资源。

    • 但是它有个问题就是:不保证及时性。
    • 如果你给类提供 finalizer 执行的时候,其开始到被 JVM 回收的这段时间是不可预见的.
    • 且 JVM 会延迟执行 finalizer,从而导致类一直无法被回收。
    • 如果量一大就会有堆内存溢出的风险。
    • 使用 finalizer 会使创建并销毁对象的时间变成原来的 400~500 倍。

5.2 元空间内存溢出

  • java.lang.OutOfMemoryError: Metaspace 元空间内存溢出。

  • hotspot 虚拟机在 java8 后,将原本的方法区(永久代)彻底移除,取而代之的是元空间(Metaspace)。

    • 常量池与静态变量转移到了堆中储存,元空间中只存了类的信息(class 定义,名称,方法),字节码文件等信息。
    • 元空间已经不占 JVM 虚拟机空间,而是使用本地内存。
    • 但是仍然有一个配置设置了元空间的大小,-XX:MaxMetaspaceSize
  • 造成内存溢出的原因

    • 当元空间中,加载的 class 数目太多或者体量太大时,占满元空间的时候。
    • 就会出现 OutOfMemoryError: Metaspace 元空间内存溢出的错误。
  • 解决方法

    • -XX:MaxMetaspaceSize 配置更大的元空间,一般用于启动时就报错的情况。
    • 重启 JVM 虚拟机,可能是一些应用没有重启导致了加载多份 class 的原因。
    • 设置 -XX:+CMSClassUnloadingEnabled-XX:+UseConcMarkSweepGC 这两个参数允许 JVM 卸载 class。因为 JVM 默认是不会卸载 class 的,但是可能会有程序动态创建太多 class 的情况出现。

5.3 永久代内存溢出

  • java.lang.OutOfMemoryError: Permgen space 永久代内存溢出。

  • 与上述元空间内存溢出类似,可使用 -XX:MaxPermSize 改动永久代大小。

5.4 方法区溢出

  • java.lang.OutOfMemoryError: GC overhead limit exceeded 方法区溢出。

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

    public static void main(String[] args) throws Exception {

    System.out.println("方法区内存溢出案例");

    try {
    int item = 0;
    List<String> strList = new LinkedList<String>();
    while (true) {
    strList.add(String.valueOf(item++).intern());
    }
    } catch (Exception e) {
    e.printStackTrace();
    throw e;
    }
    }
    }
  • 原因

    • 当内存基本被耗尽,JVM 虚拟机反复进行 GC 垃圾回收却几乎无法回收垃圾的时候,就会抛出这个错误。
    • 通俗的说就是,GC 垃圾收集器反复反复进行垃圾回收却没啥用的时候,它就放弃治疗了。
  • 解决方法

    • 其实与堆内存溢出情况类似,就算不出这个异常,再创建几个对象也会出现堆内存溢出的情况。
    • 因此解决方法可以参考堆内存溢出的解决方法。

5.5 本地线程过多

  • java.lang.OutOfMemoryError: unable to create native Thread 本地线程过多。

  • Java 底层是调用 native 本地方法创建的线程,但是当线程创建的数量到达机器允许的上限时就会触发该问题。

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

    public static void main(String[] args) {

    for (int i = 0; ; i++) {
    System.out.println("Thread ["+i+"]");

    new Thread(() -> {
    try {
    // 让线程一直睡
    Thread.sleep(Integer.MAX_VALUE);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }).start(); // 注意:开个虚拟机验证,千万别在自己电脑跑!不然冒烟了别找我
    }
    }
    }
  • 注意:开个虚拟机验证,千万别在自己电脑跑!!!不然冒烟了别找我!

5.6 非 JVM 内存溢出

  • java.lang.OutOfMemoryError: Direct buffer memory

    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
    // 配置 -verbose:gc -Xms10M -Xmx10M -XX:MaxDirectMemorySize=10M -Xss160k -XX:+PrintGCDetails

    import java.nio.ByteBuffer;

    public class DirectMemoryOutOfMemory {

    private final static int ONE_GB = 1024*1024*1024;
    private static int count = 1;

    public static void main(String[] args) {

    try {
    while (true) {
    ByteBuffer buffer = ByteBuffer.allocateDirect(ONE_GB);
    count++;
    }
    } catch (Exception e) {
    System.out.println("Exception: instance create " + count);
    e.printStackTrace();
    } catch (Error e) {
    System.out.println("Error: instance create " + count);
    e.printStackTrace();
    }
    }
    }
  • Java 的 NIO 包中的 Direct Bytebuffer 可以直接访问堆外内存,搭配内存映射文件(MemoryMapFile),能够实现高速 IO 操作。

    • 但是,其实这个能够访问提堆外内存大小是有限制的。一般默认是 64M。
    • 所以使用 NIO 时一定要注意不要超过限制大小,否则会包该错误。
  • 解决方法

    • 可以通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值。

5.7 栈内存溢出

  • java.lang.StackOverflowError 栈内存溢出

  • 每个线程有自己的线程私有区,stack 就是线程私有的。

  • 当栈堆满的时候会出现栈溢出。

  • 例如循环调用方法。

    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
    // 配置 -verbose:gc -Xms10M -Xmx10M -XX:MaxDirectMemorySize=5M -Xss160k -XX:+PrintGCDetails

    public class StackOverFlow {

    private static int counter;

    public void count() {
    counter++;
    count(); // 循环调用
    }

    // main 方法发生占内存泄露
    public static void main(String[] args) {

    System.out.println("StackOverFlow");
    StackOverFlow sof = new StackOverFlow();

    try{
    sof.count();
    } catch (Exception e) {
    System.out.println("栈的深度:" + (++counter));
    e.printStackTrace();
    }
    }
    }
文章目录
  1. 1. 前言
  2. 2. 1、异常
  3. 3. 2、异常处理
    1. 3.1. 2.1 捕捉异常
    2. 3.2. 2.2 抛出异常
  4. 4. 3、异常分类
    1. 4.1. 3.1 异常类
    2. 4.2. 3.2 常见异常
  5. 5. 4、自定义异常
  6. 6. 5、OOM 内存溢出
    1. 6.1. 5.1 堆内存溢出
    2. 6.2. 5.2 元空间内存溢出
    3. 6.3. 5.3 永久代内存溢出
    4. 6.4. 5.4 方法区溢出
    5. 6.5. 5.5 本地线程过多
    6. 6.6. 5.6 非 JVM 内存溢出
    7. 6.7. 5.7 栈内存溢出
隐藏目录