Effective Java 8.避免使用终结方法和清除方法

187 阅读3分钟

避免使用终结方法和清除方法

终结方法(Object.finalize()) 通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定、性能降低,以及可移植性问题。根据经验,应该避免使用终结方法。 在 Java9 中用清除方法Cleaner代替了终结方法。Cleaner虽然没有finalize()那么危险,但仍然是不可预测、运行缓慢,一般情况下也是不必要的。

在 Java 中,当一个对象变得不可到达的时候,垃圾回收器会回收与该对象相关联的存储空间,并不需要我们做专门的工作。而对于回收其他的非内存资源

  • 什么叫对象不可达?这个要涉及对死亡对象的判断方法。堆中几乎放着所有的对象实例,对堆回收前的第一步就要判断哪些对象已经死亡(即不能再被任何途径使用的对象)
  • 可达性分析算法(Java中用来判断对象是否可达的算法):这个算法的基本思想就是在堆中选取一些名为“GC Roots”的对象作为起点,从这些节点向下搜索,这些节点引用着的对象说明是科大的。当一个对象到 GC Roots 没有任何引用链相连,证明该对象不可用,需要被回收
  • 详细内容请翻阅:可达性分析算法

缺点

终结方法和清除方法的缺点在于不能保证会被及时执行。 可以尝试一下案例代码

public class FinalizeExample {
    public static void main(String[] args) throws IOException {
        System.out.println("正常");
        FinalizeExample example1 = new FinalizeExample();
        example1 = null;
        // 注释掉这一行代码finalize()就不会执行了
        System.gc();
    }
​
    @Override
    protected void finalize() throws Throwable {
        System.out.println("----------finalize执行------------");
        super.finalize();
    }
}

在 Java 中,由于 GC 的自动回收机制,因而并不能保证finalize方法会被及时执行(垃圾对象的回收时机具有不确定性)。并且 Java 语言规范不仅不保证终结方法或者清除方法会被及时执行,且根本就不保证它们会被执行

虽然上面案例中System.gc()可以使finalize()执行,但它并不保证终结方法或清除方法一定会被执行。能够做到这一功能的方法是System.runFinalizersOnExitRuntime.runFinalizersOnExit。这两个方法都有致命缺点,并且被废弃好久了

书中所说使用终结方法和清除方法有一个非常严重的性能损失,从实用角度,已经用很大篇幅去讲不要用终结方法和清除方法,本人也就没有进行具体测试了

安全问题

终结方法有一个严重的安全问题:终结方法攻击

此处具体案例借鉴:Finalizer attack

容易遭受攻击的案例

class Vulnerable {
    String password;
​
    Vulnerable(String password) {
        if (!checkVerification(password)) {
            throw new IllegalArgumentException("Fail to verification");
        }
        this.password = password;
    }
​
    private static boolean checkVerification(String password) {
        return "123".equals(password);
    }
}
  • 攻击类
public class Attack extends Vulnerable{
    public static Attack instance;
    Attack(String password){
        super(password);
    }
    
    public void finalize(){
        instance = this;
    }  
    
    public static void main(String[] args){
        try{
            new Attack("23");       
        }catch(Exception e){
            e.printStackTrace();
        } 
        System.gc();
        System.runFinalization();
        if(instance != null){
            System.out.println("instance is created!");
        }
    }
}

最终输出

java.lang.IllegalArgumentException: Fail to verification
instance is created!

从构造器抛出的异常,应该足以防止对象继续存在,但有了终结方法的存在,使对象又复活了

而final类不会受到终结方法攻击,因为不会有子类

为防止非final类受到终结方法攻击,要编写一个空的finalfinalize方法


如果类的对象中封装的资源确实需要终止,只需要让类实现AutoCloseable,并要求客户端在每个实例不需要的时候调用close()方法,通常使用try-with-resources

好处

终结方法和清除方法的好处有两个

  1. 当资源的所有者忘记调用close()方法,终结方法或清除方法可以充当“安全网”,虽然并不能保证这些方法会被及时运行,但迟一点释放资源总比永远不释放要好

参考文献

可达性分析算法

Finalizer attack