Java:Effective java学习笔记之 避免使用终结方法

避免使用终结方法

1、finalize()基本概念

学习过C++的同学看到终结方法(finalizer)应该马上就能想到C++中的析构函数(destructor)。Java中的终结方法果然和C++中的析构函数类似,会在对象被垃圾回收之前执行,也就是对象被销毁之前执行。在C++中,析构函数常用于回收对象所占用的资源。但由于GC机制的存在,在Java中,我们无法预知对象会在何时被回收,也就是说我们无法预知终结方法会在何时被执行,Java语言规范也不保证终结方法会被执行。这是十分危险的。

所谓的终结方法其实是指finalize()。

我们通过下面的例子演示一下析构函数和终结方法。
c++

#include <iostream>
​
using namespace std;
​
class A {
public:
    A(); // 构造函数,同Java
    ~A(); // 析构函数,删除对象时被执行
};
​
A::A() {
    cout << "创建对象" << endl;
}
​
A::~A() {
    cout << "删除对象" << endl;
}
​
​
int main() {
    A *a = new A();
    delete a; // 删除对象return 0;
}

输出

创建对象
删除对象

Java

public class A {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("A调用了终结方法...");
    }
}

public class B {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("B调用了终结方法...");
    }
}

public class Main {
    public static void main(String[] args) {
        WeakReference<A> weakReference = new WeakReference<>(new A());
        B b = new B();
        System.gc(); // 弱引用在GC时,会被回收
    }
}

输出

A调用了终结方法...

在Java9以后,终结方法就已经被抛弃了。如果你尝试重写这个方法,会发现它是过时的。
在这里插入图片描述
Java9用清除方法( cleaner)替代了终结方法,但是由于清除方法的执行时间也是不确定的、也不保证会被执行,所以同样不推荐使用。

永远不要用终结方法或者清除方法来释放资源。

2、finalize()的执行过程

当对象不可达时,GC会判断该对象是否重写了finalize()方法,如没有重写则直接将其回收,否则,若对象未执行过finalize()方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize()方法。执行finalize()方法完后,GC会再次判断该对象是否可达,若不可达则进行回收。否则对象“复活”。

3、为什么要避免覆盖并使用finalize方法?

(1)finalize方法不能保证它能被及时的执行。

(2)finalize方法甚至都不会被执行。

(3)System.gcSystem.runFinalization这两个方法只是能增加finalize方法被调用的几率。

(4)唯一能保证finalize方法被执行的方法有两个,
System.runFinalizersOnExitRuntime.runFinalizersOnExit但是这两个方法已经被弃用。

(5)覆盖并使用终结方法会造成严重的性能损失。

4、如果类中的资源确实需要被释放,我们应该怎么做?

一般来说,需要释放的资源有线程或者文件还有一下涉及到本地的资源的对象。

  • 我们只需要提供一个public修饰的终止方法,用来释放资源。
  • 并要求这类的使用者在不再使用这个类的时候调用这个方法,并且在类中添加一个标志,来标记资源是否已经释放。
    • 如果已经被释放了,那这个类中的方法如果在被调用的话就抛出IllegalStateException异常,一个很好的例子就是InputStream和OutputStream。

多说一句,在调用我们自己定义的public修饰的终止方法的时候最好和try—finally一起使用,就像下面这样:

class MyObject{
    private boolean isClosed = false;
    //public修饰的终止方法
    public void close(){
        //资源释放操作
        ...
        isClosed = true;
    }
}
public static void main(String... args) {
    MyObject object = new MyObject();
    try{
        //在这里面使用object;
        ...
    }  finally {
        //在这里面关闭object;
        object.close();
    }
}

5、终结方法的利弊

5.1、终结方法的好处

终结方法第一种合法用途:

当对象所有者忘记调用前面建议的显式终止方法时,终结方法可以充当 “安全网”(safety net) 。

虽然这样做不能保证终结方法会被及时执行,但在客户端无法通过显式调用终止方法来正常结束操作的情况下,迟一点释放关键资源总永不释放要好(如果终结方法发现资源仍未被终止,应该在日志中记录一条警告 )。

显式终止方法的实例(四个类:FileInputStreamFileOutputStreamConnectionTimer)都具有终结方法,当终止方法不起作用,这些终结方法便当了安全网。

“安全网”的作用是当我们提供的public修饰的终结方法被在外部忘记调用的时候提供一种安全保障,如下:

class MyObject{
    private boolean isClosed = false;
    //public修饰的终止方法
    public void close(){
        //资源释放操作
        ...
        isClosed = true;
    }

    //安全网
    @Overried
    protected void finalize() throws Throwable {
        try{
            close();
        }  finally  {
            super.finalize();
        }
    }}

终结方法的第二种合理用途与对象的本地对等体(native peer)有关

本地对等体是一个本地对象(native object),普通对象通过本地方法委托给一个本地对象,因为本地对等体不是一个普通对象,所以垃圾回收期并不知道它。因此,在本地对等体并不拥有关键资源时,终结方法正是执行这项任务的最合适工具。

如果本地对等体拥有必须被及时终止的资源,那么该类就应该具有一个显式的终止方法。这个终止方法就是完成必要的工作并释放关键资源。终止方法可以是本地方法或者它调用本地方法。

“终结方法链”(finalizer)同样不会被自动执行,如果类有定义终结方法,并且子类覆盖了该终结方法,那么子类的终结方法就得手工调用父类的终结方法:以确保即使子类的终结方法过程抛出异常,父类的终结方法也会得以执行(这也是规避常见的代码攻击)。

// Manual finalizer chaining
  @Override
  protected void finalize() throws Throwable {
    try {
      // Finalize subclass state
    } finally {
      super.finalize();
    }
  }

另一种方式是使用终结方法守卫者(finalizer guardian)

我们不将终结方法封装在一个要求终结处理的类中,而是放在一个匿名类里,该匿名类唯一用途是终结它的外围实例(enclosing instance),该匿名类的单个实例就被称为终结方法守卫者。

如果子类实现者覆盖了超类的终结方法,但是忘了调用超类的终结方法,那么超类的终结方法永远不会调用。为了防止此种情况出现,可以使用终结方法守卫者,即为每个将被终结的对象创建一个附加的对象,该附加对象是一个匿名类实例,将外围类的终结操作如释放资源放入该匿名类的终结方法中。外围实例在它的私有实例域中保存着一个对其终结方法守卫者的唯一引用,因此终结方法守卫者与外围实例可以同时启动终结过程。当守卫者被终结时,它执行外围实例所期望的终结行为,就好像它的终结方法是外围对象上的一个方法一样。

public class A {

    // 终结守卫者
    private final Object finalizerGuardian = new Object() {

        @Override
        // 终结守卫者的终结方法将被执行
        protected void finalize() {
            System.out.println("A finalize by the finalizerGuardian");
        }
    };


    @Override
    // 由于终结方法被子类覆盖,该终结方法并不会被执行
    protected void finalize() {
        System.out.println("A finalize by the finalize method");
    }


    public static void main(String[] args) throws Exception {
        B b = new B();
        b = null;
        System.gc();
        Thread.sleep(500);
    }
}

class B extends A {

    @Override
    public void finalize() {
        System.out.println("B finalize by the finalize method");
    }

}

输出

1 A finalize by the finalizerGuardian
2 B finalize by the finalize method

5.2、终结方法的弊端

终结方法缺点一:

终结方法在于不能保证会被及时的执行。从一个对象变得不可达开始,到它的终结方法被执行,所花费时间是任意长的。这意味着,注重时间(time-critical)的任务不应该由终结方法来完成。

终结方法缺点二:

如果未被捕获的异常在终结过程中被抛出,那么这种异常可以被忽略,并且该对象的终结过程也会被终结。未被捕获的异常会使对象处于破坏的状态(a corrupt state),如果另一个线程企图使用该对象,则可能发生任何不确定的行为。

正常情况未捕获的异常会使线程终止并打印堆栈轨迹,但如果异常发生在终结方法中,甚至不会打印警告!!

终结方法缺点三:

使用了终结方法,会导致严重的性能损失。例如,在某个机器上创建并销毁一个简单对象时间约为5.6ns,增加一个终结方法则会增加到2400ns。

6、总结

总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。

  • 1.在很少见的情况下,既然使用了终结方法,就要记住使用super.finalize。
  • 2.如果用作安全网,要记得记录终结方法的非法用法。
  • 3.如果需要将终结方法与公有的非final类关联起来,请考虑使用终结方法守卫者以确保:即使子类的终结方法没有调用super.finalize,该终结方法也会被执行。

参考

1、java代码优化——避免使用终结方法
2、《Effective Java》阅读笔记7 避免使用终结方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值