龙珠

修炼自己与发现世界

Java:finalize方法介绍和为什么不要使用它?【存疑】

如果查看Java中的Object.java文件,其中可以重写的有5个方法:clone(), equals(), hashcode(), finalize(), toString()。我们已经讲过了equals()和hashcode()方法,toString()方法很容易理解,而这次来看finalize()方法(clone()方法以后再讲吧)。

finalize方法是Java中在对象结束之前进行清除工作的方法。那么在Java中,是如何在对象结束之前进行消除呢?我们先来看一下。

一、Java中如何对对象结束进行清除处理?

finalize()方法的文档如下:

/**
     * Called by the garbage collector on an object when garbage collection
     * determines that there are no more references to the object.
     * A subclass overrides the {@code finalize} method to dispose of
     * system resources or to perform other cleanup.
     * <p>
     * The general contract of {@code finalize} is that it is invoked
     * if and when the Java<font size="-2"><sup>TM</sup></font> virtual
     * machine has determined that there is no longer any
     * means by which this object can be accessed by any thread that has
     * not yet died, except as a result of an action taken by the
     * finalization of some other object or class which is ready to be
     * finalized. The {@code finalize} method may take any action, including
     * making this object available again to other threads; the usual purpose
     * of {@code finalize}, however, is to perform cleanup actions before
     * the object is irrevocably discarded. For example, the finalize method
     * for an object that represents an input/output connection might perform
     * explicit I/O transactions to break the connection before the object is
     * permanently discarded.
     * <p>
     * The {@code finalize} method of class {@code Object} performs no
     * special action; it simply returns normally. Subclasses of
     * {@code Object} may override this definition.
     * <p>
     * The Java programming language does not guarantee which thread will
     * invoke the {@code finalize} method for any given object. It is
     * guaranteed, however, that the thread that invokes finalize will not
     * be holding any user-visible synchronization locks when finalize is
     * invoked. If an uncaught exception is thrown by the finalize method,
     * the exception is ignored and finalization of that object terminates.
     * <p>
     * After the {@code finalize} method has been invoked for an object, no
     * further action is taken until the Java virtual machine has again
     * determined that there is no longer any means by which this object can
     * be accessed by any thread that has not yet died, including possible
     * actions by other objects or classes which are ready to be finalized,
     * at which point the object may be discarded.
     * <p>
     * The {@code finalize} method is never invoked more than once by a Java
     * virtual machine for any given object.
     * <p>
     * Any exception thrown by the {@code finalize} method causes
     * the finalization of this object to be halted, but is otherwise
     * ignored.
     *
     * @throws Throwable the {@code Exception} raised by this method
     *

可以看出,finalize()方法是由Java中的垃圾收集器来调用的。当GC认为没有其他引用指向该对象时,则调用对象的finalize()方法,进行垃圾回收前的清理工作,如:显式的关闭I/O连接等。

另外,指的注意的是,内存的回收并不是在调用finalize()方法之后立即进行的。实际情况是:一旦垃圾回收器准备好释放对象占用的空间,将首先调用其finalize()方法,并且在下一次垃圾回收的动作发生之时,才会真正的回收对象所占用的内存。因此,finalize()方法的意义是,在JVM垃圾回收时刻做一些重要的清理工作。

Java语言并不保证由哪个线程调用该方法,但可以保证的是,在调用该方法前,调用的线程不会持有任何用户可见的同步锁。如果在调用finalize()方法期间出现异常,该异常会被忽略,同时该对象的finalize()方法会终止。并且,对于任何对象而言,finalize()方法最多只能被JVM调用一次。

我们知道,System.gc()是建议JVM进行垃圾清除工作(但JVM并不保证一定执行),从而强行进行终结动作。那么我们测试下代码看看finalize()方法是如何工作的,代码如下:

package com.arthur.test;
                                                                                                                                                                                                 
public class Test {
                                                                                                                                                                                                     
    public static void main(String[] argv){
        TestFinalize tf = new TestFinalize();      
        System.gc();
        System.out.println("GC1"); 
                                                                                                                                                                                                         
        tf = null;
        System.gc();
        System.out.println("GC2");
    }
}
                                                                                                                                                                                                 
class TestFinalize{
                                                                                                                                                                                                     
    @Override
    protected void finalize() throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("Invoke TestFinalize finalize method.");
    }
                                                                                                                                                                                                 
    public TestFinalize(){
        System.out.println("Create a new TestFinalize.");
    }
}

输出结果 为:

Create a new TestFinalize.
GC1
GC2
Invoke TestFinalize finalize method.

在tf不为null时,使用GC1并没有效果;在将tf置为null之后,调用GC2,JVM调用finalize()方法,从而输出“Invoke TestFinalize finalize method.”。

讲完了finalize方法,那为什么不要使用它呢?

在Effective Java中有谈到:

终结方法(finalize)通常是不可预测的,也是很危险的,一般情况下是不必要的。

finalize方法的缺点:总结起来就有几点:

  1. 不保证及时执行:因为终结方法线程的优先级比其他线程要低得多,所以对象的终结速度赶不上其他线程进入队列的速度。

  2. 不保证是否执行:当一个程序终止的时候,某些已经无法访问的对象上的终结方法却根本没有被执行,这是完全有可能的。

  3. 如果未被捕获的异常在终结过程中被抛出来,那么这种异常可以被忽略,并且该对象的终结过程也会终止。未被捕获的异常会使对象处于破坏的状态,如果另一个线程企图使用这种被破坏的对象,则可能发生任何不确定的行为。而且,在终结方法中未被捕获的异常不会打印出stacktrace,甚至连警告都不会打印出来。不易发现和调试bug。

  4. 使用终结方法会有非常严重的性能损失。Effective Java中提到,作者创建和销毁一个简单对象只需要5.4ns,而增加了终结方法之后,增加到2400ns。性能损失了400多倍。

因此,不应该以来终结方法来更新重要的持久状态。

我对缺点3和4写代码测试了下。对缺点3的测试代码如下:

class TestFinalize{
             
    @Override
    protected void finalize() throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("Invoke TestFinalize finalize method.");
        throw new IllegalArgumentException("test throw exception. Arthur");
    }
         
    public TestFinalize(){
        System.out.println("Create a new TestFinalize.");
    }
         
         
    public static void main(String[] argv){
                 
        TestFinalize tf = new TestFinalize();
        System.gc();
                 
    }
}

运行结果如下:

Create a new TestFinalize.

可以看出,并没有throw IllegalArgumentException。

对于缺点4的测试代码如下:

class TestFinalize{
       
    public static void main(String[] argv){
               
        long startTime = System.nanoTime();
        TestFinalize tf = new TestFinalize(5);
        tf = null;
        long endTime = System.nanoTime();
        System.out.println("Cost nanoTime: "+(endTime - startTime)+"ns");
    }
           
    private int age;
    public TestFinalize(int age){
        this.age = age;
        System.out.println("Create a new TestFinalize. Age:"+this.age);
    }
           
    @Override
    protected void finalize() throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("Invoke TestFinalize finalize method.");
        throw new IllegalArgumentException("test throw exception. Arthur");
    }
           
}

不过运行代码,在分别对finalize方法进行注释前后,运行结果均和如下差不多:

Create a new TestFinalize. Age:5
      
Cost nanoTime: 387059ns

也就是说,注释与否,创建和消除TestFinalize对象消耗的ns数在30w~50w ns之间,偶尔会升到80w左右。因此,此处和书中表述的有出入。书中说是对“简单对象”的创建和消除,难道不是指的这个?【此处存疑】

既然finalize方法无法确定回收资源,那么应该怎么办呢?

对于这个问题解决办法是:

1.对于资源的回收工作放到try-finally块中进行。

2.显式地定义结束的方法,如InputStream中的close()方法。

不过有个疑问,既然finalize方法这么不好,为什么还要有它呢?其实还是有一定好处的。

finalize方法的好处:

1.当对象的所有者忘记调用前面建议的显式终止方法时,终结方法(finalize)可以充当“安全网(safety net)”。虽然finalize方法并不保证及时执行,但是迟点总比没有好。即便最后没有执行,情况也不会更坏。

2.终结方法的第二种合理用途与对象的本地对等体(native peer)有关。【唔,这个现在还不懂】

另外,指的注意的一点是,“终结方法链(finalizer chaining)”并不会被自动执行。如果类(不是Object)有终结方法,子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法。

参考资料:

  1. 《Effective Java》

  2. 《Java编程思想》