内存是计算机很宝贵的资源,我们在使用资源时如果不关闭打开的资源,就有可能导致内存泄露的风险,下面浅析下Java中几种常见的资源关闭方案
先定义一个资源类表示需要关闭的资源
public class MyResource implements Closeable {
@Override
public void close() throws IOException {
System.out.println("MyResource的close方法被调用!");
}
}
一、直接在try块中关闭,如下代码
public class FinallyTest {
public static void main(String[] args) {
try {
MyResource myResource = new MyResource();
System.out.println("抛出异常前...");
int i = 1 / 0;
myResource.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
我们可以看到输出的结果是
其实我们可以看到,当抛出异常后try块中异常后续的代码就不会被执行,如当我们执行代码int i = 1 / 0时,这句代码肯定会抛出异常,当抛出异常后try块中的代码就不会被执行了,即后面的myRosource.close()方法就不会被调用。如果代码中存在大量的资源调用,但是抛出了异常,就会导致内存泄露的风险,其实我们从字节码文件中也可以知道结果字节码如下图所示。
二、接下来我们看看使用try{...}catch{....}finally{...}的形式,接下来我们把代码改为如下形式,把调用close的代码移动到finally中。
public class FinallyTest {
public static void main(String[] args) throws IOException {
MyResource myResource = null;
try {
myResource = new MyResource();
System.out.println("抛出异常前...");
int i = 1 / 0;
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (myResource != null) {
myResource.close();
}
}
}
}
输出结果如下,此时我们从输出结果中可以看出,不管try块中是否抛出异常,finally最终都会被执行
接下来我们从字节码的层面看看调用机制。其实我们可以看到,不管是走哪一个分支,最终都会执行finally的语句
三、try-with-resources 但是从JDK1.7开始,有新的方式。我们看接口AutoCloseable,是从JDK1.7开始提供的
/*
* @author Josh Bloch
* @since 1.7
*/
public interface AutoCloseable {
void close() throws Exception;
}
我们定义如下测试代码
public class FinallyTest {
public static void main(String[] args) throws IOException {
try (MyResource myResource = new MyResource()) {
System.out.println("抛出异常前...");
int i = 1 / 0;
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
执行结果如下,我们不需要显示调用close方法,close会被调用,执行结果如下。
那java是如何实现的呢?我们反编译字节码看看结果,这就是try...catch...finally的形式。try-catch-resource这种写法只是一个语法糖。
但是细心的朋友可能发现var2.addSuppressed(var11)这个是什么鬼?我们去看看Throwable中方法addSuppressed的注释,如下所示。可以看到,这个方法也是从JDK1.7开始的。省略了部分注释,其实就是为了避免异常覆盖。
/**
* Appends the specified exception to the exceptions that were
* suppressed in order to deliver this exception. This method is
* thread-safe and typically called (automatically and implicitly)
* by the {@code try}-with-resources statement.
* @since 1.7
*/
public final synchronized void addSuppressed(Throwable exception) {
if (exception == this)
throw new IllegalArgumentException(SELF_SUPPRESSION_MESSAGE, exception);
if (exception == null)
throw new NullPointerException(NULL_CAUSE_MESSAGE);
if (suppressedExceptions == null) // Suppressed exceptions not recorded
return;
if (suppressedExceptions == SUPPRESSED_SENTINEL)
suppressedExceptions = new ArrayList<>(1);
suppressedExceptions.add(exception);
}
接下来我们把MyResource的代码改一下,让close方法抛出异常。如下所示。
public class MyResource implements Closeable {
@Override
public void close() throws IOException {
System.out.println("MyResource的close方法被调用!");
throw new RuntimeException("我这里抛出异常啦....");
}
}
然后我们用传统的try{...}catch{...}finally{...}看看输出结果是什么样的。
public static void main(String[] args) throws Exception {
MyResource myResource = new MyResource();
try {
System.out.println("抛出异常前....");
int i = 1 / 0;
} catch (Exception ex) {
throw ex;
} finally {
if (myResource != null) {
myResource.close();
}
}
}
输出结果如下。
发现了吗?堆栈里面没有int i = 1 / 0 ,所产生的异常。异常被屏蔽了,针对这个问题,addSuppressed就可以派上用场啦。更多addSuppressed的使用可参考。
接下来我们看看try...with...Resource...的形式的解决方案。代码如下
public static void main(String[] args) throws Exception {
try (MyResource myResource = new MyResource()) {
System.out.println("抛出异常前....");
int i = 1 / 0;
} catch (Exception ex) {
throw ex;
}
}
输出结果如下:
看到了吗?被append后面啦。
个人学习记录