Java异常处理的陷阱
2024-01-07 18:32:28
文章目录
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
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!