Effective Java笔记第一章创建和销毁对象第七节避免使用终结方法

本文深入探讨Java中的终结方法(finalize),分析其不可预测性和潜在风险,包括行为不稳定、性能下降及可移植性问题。文章提供了多个示例说明如何正确使用终结方法,并探讨了替代方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Effective Java笔记第一章创建和销毁对象

第七节避免使用终结方法

本节稍稍提了一下本地方法,如果有兴趣详细了解的话可以看一下这两篇文章,相信会对你有一定的帮助。
使用Visual Studio 2019和IntelliJ IDEA 2018实现JAVA调用本地代码
Visual Studio 2019 使用 CMake 开发 JNI 动态库实现JAVA调用本地代码

1.终结方法(finalize())通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定,降低性能,以及可移植性问题。
finalize()方法是Object类中的一个方法,在GC(垃圾收集器)准备释放对象所占用的内存空间之前,他将首先调用finalize()方法,以下是Object中的源码:

protected void finalize() throws Throwable { }

要记住finalize()方法最多只会被调用一次。

public class FinalierDemo {

    public static void main(String[] args) {
        User user = new User();
        user = null;
        //调用User的finalize()方法,为User.user重新赋值了。
        System.gc();
        try {
            Thread.sleep(1000);
            user = User.user;
            //输出true
            System.out.println(user != null);
            user = null;
            //不再调用User的finalize()方法,User.user=null;
            System.gc();
            Thread.sleep(1000);
            //输出false
            System.out.println(user != null);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

class User {

    public static User user = null;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("User-->finalize()");
        user = this;
    }

}

2.C++的析构器可以被用来回收其他的非内存资源,而在java中,一般使用try-finally块来完成类似的工作。

3.终结方法的缺点在于不能保证会被及时地执行,从一个对象变得不可到达开始,到他的终结方法被执行,所花费的这段时间是任意长的。这意味着,注重时间的任务不应该由终结方法来完成。除此之外,也不应该依赖终结方法来更新重要的持久状态。
下面我们举个例子:

public class Finalizer {

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Finalizer-->finalize");
    }
	
	//无输出
    public static void main(String[] args) {
        Finalizer finalizer=new Finalizer();
        finalizer=null;
    }

}

调用GC(垃圾收集器),System.gc()之后。在GC(垃圾收集器)准备释放对象所占用的内存空间之前,他将首先调用finalize()方法。

public class Finalizer {

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Finalizer-->finalize");
    }

	//输出Finalizer-->finalize
    public static void main(String[] args) {
        Finalizer finalizer=new Finalizer();
        finalizer=null;
        System.gc();
    }

}

不要被System.gc()和 System.runFinalization()这两个方法所诱惑,他们确实增加了终结方法被执行的机会,但是并不保证终结方法一定被执行。
首先,只有当GC(垃圾回收器)释放该对象的内存时,才会执行finalize()方法,如果在Applet或应用程序退出之前GC没有释放内存,GC将不会调用finalize()方法。
其次,除非GC认为你的Applet或应用程序需要额外的内存,否则他不会试图释放不再使用的对象的内存。换句话说很有可能发生:一个Applet给少量的对象分配内存,没有造成严重的内存需求,于是垃圾回收器没有释放这些对象的内存就退出了。
显然,如果你为某个对象定义了finalize()方法,JVM可能不会调用它,因为GC没有释放那些对象的内存,就算调用System.gc()也没用,因为他仅仅是给JVM一个建议而不是命令。
下面我们举个例子:

public class FinalizerDemo2 {

    public static void main(String[] args) {
        Man man1=new Man(1);
        Man man2=new Man(2);
        Man man3=new Man(3);
        man2=man3=null;
        System.gc();
    }

}

class Man {

    private int id;

    public Man(int id) {
        this.id = id;
        System.out.println("编号为" + id + "的人已经创建了");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("编号为" + id + "的人被销毁了");
    }

}

上面的代码输出为:
编号为1的人已经创建了
编号为2的人已经创建了
编号为3的人已经创建了
编号为3的人被销毁了
编号为2的人被销毁了
很明显,只有man2和man3两个对象调用了finalize()方法,而man1没有调用,这是因为只有man2和man3赋值了null,GC释放了他们的内存,所以才会调用finalize()方法。而man1没有释放内存,所以即使调用System.gc()也没用,因为他仅仅是给JVM一个建议而不是命令。

4.如果类的对象中封装的资源确实需要终止,我们可以提供一个显式的终结方法,并要求该类的客户端在每个实例不在有用的时候调用这个方法,显式的终止方法必须在一个私有域中记录下“该对象已经不再有效”。如果这些方法是在对象已经终止之后被调用的,其他的方法就必须检查这个域,并抛出IllegalStateException异常。显式的终止方法通常和try-finally结合使用,已确保及时终止。
下面举个例子:

public static void main(String[] args) {
        try {
            //创建一个流
            FileInputStream fis=new FileInputStream("");
            try {
                //关闭流
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

以下是FileInputStream中的close源码:

public void close() throws IOException {
        synchronized (closeLock) {
            if (closed) {
                return;
            }
            closed = true;
        }
        if (channel != null) {
           channel.close();
        }

        fd.closeAll(new Closeable() {
            public void close() throws IOException {
               close0();
           }
        });
    }

当我们调用到close0();方法时,他会调用本类的以下代码,调用被native 修饰的本地语言方法close0()。

private native void close0() throws IOException;

5.终结方法的用途:
1)当对象的所有者忘记调用前面段落中建议的显式终止方法时,终结方法可以充当"安全网"。虽然这样做并不能保证终结方法会及时的调用,但是在客户端无法通过调用显式的终止方法来正常结束操作的情况下,迟一点释放关键资源总比永远不被释放要好。但是如果终结方法发现资源还未被终止,则应该在日志中记录一条警告。
比如说FileInputStream类中就有重写的finalize()方法:

protected void finalize() throws IOException {
        if ((fd != null) &&  (fd != FileDescriptor.in)) {
            /* if fd is shared, the references in FileDescriptor
             * will ensure that finalizer is only called when
             * safe to do so. All references using the fd have
             * become unreachable. We can call close()
             */
            close();
        }
    }

2)与对象的本地对等体有关。本地对等体是一个本地对象,普通对象通过本地方法委托给一个本地对象。

6.有一点很重要,"终结方法链"并不会自动执行。如果类有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法。你应该在一个try块中终结子类,并在相应的finally块中调用超类的终结方法。
下面我们举个例子:

public class FinalizerDemo3 extends Father {

    @Override
    protected void finalize() throws Throwable {
        System.out.println("子类终结方法执行");
    }

    public static void main(String[] args) {
        FinalizerDemo3 f3 = new FinalizerDemo3();
        f3 = null;
        System.gc();
    }

}

class Father {

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("父类终结方法执行");
    }
}

输出为:
子类终结方法执行
对此我们应该修改子类的复写方法。

 @Override
    protected void finalize() throws Throwable {
        try {
            System.out.println("子类终结方法执行");
        } finally {
            super.finalize();
        }
    }

这样做即使子类的终结过程抛出异常,超类的终结方法也会被执行。

7.如果子类覆盖了超类的终结方发,但是没有手动调用超类的终结方法,那么超类的终结方法永远不会被调用。如果要防范的话就要为每一个将被终结的对象创建一个附加的对象。不是把终结方法放在需要终结处理的类中,而是放在一个匿名类中,该匿名类唯一用处是终结他的外围实例。该匿名类被称为终结方法守护者
下面我们举个例子:

public class FinalizerGuardian {

    //匿名内部类--终结方法守卫者
    private final Object ft = new Object() {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("匿名内部类的终结方法执行了");
        }
    };

    //由于终结方法被子类覆盖,并且子类没有调用超类终结方法,所以不会被执行。
    @Override
    protected void finalize() throws Throwable {
        System.out.println("超类的终结方法执行了");
    }

    public static void main(String[] args) {
        FinalizerGuardianSon fgs=new FinalizerGuardianSon();
        fgs=null;
        System.gc();
    }

}

class FinalizerGuardianSon extends FinalizerGuardian {

    @Override
    protected void finalize() throws Throwable {
        System.out.println("子类的终结方法执行了");
    }

}

输出为:
匿名内部类的终结方法执行了
子类的终结方法执行了
这样写的话超类有没有复写终结方法都一样,子类终结方法是否调用super.finalize并不重要。

8.除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在一些很少见的情况下,既然使用了终结方法,就要记住调用super.finalize。如果用终结方法作为安全网,要记得记录终结方法的非法用法。如果需要把终结方法与共有的final类关联起来,请考虑使用终结方法守卫者,以确保及时子类的终结方法未能调用super.finalize,该终结方法也会被执行。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值