Java异常处理的陷阱

2024-01-07 18:32:28

另一篇:Java异常简单介绍

1. 正确关闭资源的方式

资源不能被垃圾回收

  • 实际开发中,程序需要打开一些物理资源,如数据库连接、网络连接、磁盘文件等,打开这些物理资源后必须显示关闭,否则将引起内存泄漏。
  • 可能有读者会问,JVM不是提供了垃圾回收机制吗?JVM的垃圾回收机制难道不会回收这些资源吗,答案是不会。JVM只会回收堆和元数据区里的内存,至于打开的物理资源,gc无能为力,不关闭会造成内存泄漏。

除了堆,元数据区也可以被垃圾回收

  • 元数据区的垃圾回收被称为元空间垃圾回收(Metaspace GC),当类加载器不再需要时(即没有活动的类引用了这个类加载器加载的任何类),元空间中对应的类元数据区域就可以被释放。虽然元空间的回收机制不完全等同于堆上对象的回收,但JVM确实会对元空间进行管理和回收以维持其高效利用。
  • 常量池位于元空间,也可以被回收。每个加载到JVM中的类或接口都会在其对应的类结构中包含一个常量池,这个常量池包含了编译器生成的各种字面量和符号引用。当类加载时,这些常量池内容会被加载到元空间,作为该类的元信息的一部分进行管理。因此,现在常量池不再位于堆内存而是直接内存(Native Memory)管理的元空间中。这样设计的好处在于可以更灵活地管理类的元数据,并且减少了因为永久代大小固定而导致的内存溢出问题。

1.1 传统关闭资源的方式

为了正常关闭程序中打开的物理资源,应该使用finally块来保证回收。

错误写法1:资源可能初始化失败,关闭资源时未判空

public class IODemo {
    public static void main(String[] args) throws Exception {
        PersonVO personVO = new PersonVO();

        ObjectInputStream objectInputStream = null;
        try {
            // 定义输入流,将资源加载到内存中
            // 资源初始化可能会失败
            objectInputStream = new ObjectInputStream(new FileInputStream("a.bin"));

            // 反序列化将加载到内存的资源形成对象
            personVO = (PersonVO)objectInputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
            // 处理异常...
        } finally {
            // 若资源初始化失败,这里会造成空指针异常
            objectInputStream.close();
        }
    }
}

在这里插入图片描述

错误写法2:关闭多个资源时,有资源关闭不正常时,影响剩下资源的关闭

  • 下面代码,如果objectInputStream.close();出现异常,那么objectOutputStream.close();将无法正确执行,从而导致更多的资源泄漏
public class IODemo {
    public static void main(String[] args) throws Exception {
        PersonVO personVO = new PersonVO();

        ObjectInputStream objectInputStream = null;
        ObjectOutputStream objectOutputStream = null;
        try {
            // 定义输入流,将资源加载到内存中
            // 资源初始化可能会失败
            objectInputStream = new ObjectInputStream(new FileInputStream("a.bin"));
            objectOutputStream = new ObjectOutputStream(new FileOutputStream("a.bin"));

            // 反序列化将加载到内存的资源形成对象
            personVO = (PersonVO)objectInputStream.readObject();

            // 序列化输出对象到资源中
            objectOutputStream.writeObject(personVO);
            // 调用flush()方法的目的是强制将缓冲区中的所有数据立即发送到目标设备,并清空缓冲区。这对于实时性要求较高的场景尤其重要
            // flush在计算机编程中,特别是在输入/输出(I/O)操作中,是一个非常重要的概念。当程序向输出流(如文件流、标准输出流或网络流)写入数据时,
            // 这些数据通常会被缓冲在内存中,而不是立即写入到最终的目的地(如硬盘、屏幕或远程服务器)。这样做可以提高性能,因为批量写入比逐字节写入效率更高。
            objectOutputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
            // 处理异常...
        } finally {
            if (objectInputStream != null) {
                objectInputStream.close();
            }

            if (objectOutputStream != null) {
                objectOutputStream.close();
            }
        }
    }
}

另:flush()的作用

  • 调用flush()方法的目的是强制将缓冲区中的所有数据立即发送到目标设备,并清空缓冲区。这对于实时性要求较高的场景尤其重要
  • flush在计算机编程中,特别是在输入/输出(I/O)操作中,是一个非常重要的概念。当程序向输出流(如文件流、标准输出流或网络流)写入数据时,这些数据通常会被缓冲在内存中,而不是立即写入到最终的目的地(如硬盘、屏幕或远程服务器)。这样做可以提高性能,因为批量写入比逐字节写入效率更高。

正确写法

  • 使用finally块来关闭物理资源,保证关闭操作总会被执行
  • 关闭每个资源之前首先保证引用该资源的引用变量不为null
  • 保证关闭资源时出现的异常不会相互影响,每一处关闭资源的操作都单独加上try catch
public class IODemo {
    public static void main(String[] args) throws Exception {
        PersonVO personVO = new PersonVO();

        ObjectInputStream objectInputStream = null;
        ObjectOutputStream objectOutputStream = null;
        try {
            // 定义输入流,将资源加载到内存中
            // 资源初始化可能会失败
            objectInputStream = new ObjectInputStream(new FileInputStream("a.bin"));
            objectOutputStream = new ObjectOutputStream(new FileOutputStream("a.bin"));

            // 反序列化将加载到内存的资源形成对象
            personVO = (PersonVO)objectInputStream.readObject();

            // 序列化输出对象到资源中
            objectOutputStream.writeObject(personVO);
            // 调用flush()方法的目的是强制将缓冲区中的所有数据立即发送到目标设备,并清空缓冲区。这对于实时性要求较高的场景尤其重要
            // flush在计算机编程中,特别是在输入/输出(I/O)操作中,是一个非常重要的概念。当程序向输出流(如文件流、标准输出流或网络流)写入数据时,
            // 这些数据通常会被缓冲在内存中,而不是立即写入到最终的目的地(如硬盘、屏幕或远程服务器)。这样做可以提高性能,因为批量写入比逐字节写入效率更高。
            objectOutputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
            // 处理异常...
        } finally {
            if (objectInputStream != null) {
                try {
                    objectInputStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                    // 处理close()方法抛出的异常...
                    // 可以选择重试或者不处理
                }
                
            }

            if (objectOutputStream != null) {
                try {
                    objectOutputStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                    // 处理close()方法抛出的异常...
                    // 可以选择重试或者不处理
                }
            }
        }
    }
}

1.2 try-with-resources语句(自动关闭资源)

try-with-resources语句是从Java 7版本开始引入的一种资源管理机制。它旨在自动关闭那些实现了AutoCloseable接口的资源对象(例如文件流、数据库连接等),以避免资源泄露。

传统的手动资源管理通常需要在finally块中关闭资源,如下所示:

InputStream is = null;
try {
    is = new FileInputStream("file.txt");
    // 使用is...
} catch (IOException e) {
    // 处理异常...
} finally {
    if (is != null) {
        try {
            is.close();
        } catch (IOException e) {
            // 处理close()方法抛出的异常...
        }
    }
}

而使用try-with-resources语句后,可以简化代码并确保资源始终会被正确关闭:

try (InputStream is = new FileInputStream("file.txt")) {
    // 使用is...
} catch (IOException e) {
    // 处理异常...
}

在这个例子中,当离开try块时,无论是正常结束还是因为异常退出,Java都会自动调用InputStream对象的close()方法来关闭文件流。如果close()方法抛出了异常,这个异常会与原始的try块中可能发生的任何其他异常进行合并或者替代(取决于具体实现和异常处理策略)。

将之前的finally块中关闭资源改造成 try-with-resources语句如下:

public class IODemo1 {
    public static void main(String[] args) throws Exception {
        PersonVO personVO = new PersonVO();


        try (
            // 定义输入流,将资源加载到内存中
            // 资源初始化可能会失败
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("a.bin"));
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("a.bin"));
        ) {
            // 反序列化将加载到内存的资源形成对象
            personVO = (PersonVO) objectInputStream.readObject();

            // 序列化输出对象到资源中
            objectOutputStream.writeObject(personVO);
            // 调用flush()方法的目的是强制将缓冲区中的所有数据立即发送到目标设备,并清空缓冲区。这对于实时性要求较高的场景尤其重要
            // flush在计算机编程中,特别是在输入/输出(I/O)操作中,是一个非常重要的概念。当程序向输出流(如文件流、标准输出流或网络流)写入数据时,
            // 这些数据通常会被缓冲在内存中,而不是立即写入到最终的目的地(如硬盘、屏幕或远程服务器)。这样做可以提高性能,因为批量写入比逐字节写入效率更高。
            objectOutputStream.flush();
        } catch (Exception e) {
            e.printStackTrace();
            // 处理异常...
        }
    }
}
  • try-with-resources语句语法更简洁
  • 被自动关闭的资源必须实现AutoCloseable或Closeable接口

在这里插入图片描述

  • 被自动关闭的资源必须放在try语句后的圆括号中声明初始化
  • 其实try-with-resources语句是语法糖,内部实现还是在finally中关闭资源,我们可以将文件编译再反编译查看:
public class IODemo1 {
    public IODemo1() {
    }

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

        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("a.bin"));
            Throwable var3 = null;

            try {
                ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("a.bin"));
                Throwable var5 = null;

                try {
                    PersonVO personVO = (PersonVO)objectInputStream.readObject();
                    objectOutputStream.writeObject(personVO);
                    objectOutputStream.flush();
                } catch (Throwable var30) {
                    var5 = var30;
                    throw var30;
                } finally {
                    if (objectOutputStream != null) {
                        if (var5 != null) {
                            try {
                                objectOutputStream.close();
                            } catch (Throwable var29) {
                                var5.addSuppressed(var29);
                            }
                        } else {
                            objectOutputStream.close();
                        }
                    }

                }
            } catch (Throwable var32) {
                var3 = var32;
                throw var32;
            } finally {
                if (objectInputStream != null) {
                    if (var3 != null) {
                        try {
                            objectInputStream.close();
                        } catch (Throwable var28) {
                            var3.addSuppressed(var28);
                        }
                    } else {
                        objectInputStream.close();
                    }
                }

            }
        } catch (Exception var34) {
            var34.printStackTrace();
        }

    }
}

– 不过两者还是有区别的
我们写的代码若未使用try-with-resources语句,在finally中关闭资源,我们可以单独为每个资源关闭不成功后单独定制处理策略,不过一般也就是trycatch不做其他任何处理,也不会重试,这样的话,其实也是没区别的

– 我们日常开发中使用try-with-resources语句完全ok,建议使用

2. 避免在catch块中无限重试引起无限递归导致StackOverflowError

2.1 无限次递归调用导致StackOverflowError

在catch块中递归调用

public class TryCatchDemo4 {
    public static void main(String[] args) {
        test();
    }
    public static void test() {
        try {
            Class.forName("com.haha.Stu");
        } catch (Exception e) {
            test();
        }
    }
}

打印

在这里插入图片描述

在finally块中递归调用也一样

在这里插入图片描述

2.2 设置最大重试次数

public class TryCatchDemo4 {
    private static final int RETRY_MAX = 3;
    public static void main(String[] args) {
        LongAdder longAdder = new LongAdder();
        test(longAdder);
    }

    public static void test(LongAdder longAdder) {
        try {
            longAdder.increment();
            if (longAdder.intValue() > RETRY_MAX) {
                System.out.println("已经超过最大重试次数" + RETRY_MAX + "了,不能再重试了");
                return;
            }

            System.out.println("第" + longAdder.intValue() + "次重试");
            Class.forName("com.haha.Stu");
        } catch (ClassNotFoundException e) {
            test(longAdder);
        }
    }
}

执行打印

在这里插入图片描述

文章来源:https://blog.csdn.net/weixin_43024834/article/details/135398518
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。