天天看点

Java核心技术之final,finally和finalize有什么不同

final

final可以用来修饰类,方法和变量。当final用来修饰类时,代表这个类不能被继承;当final用来修饰方法时,代表这个类不能被重写;当final用来修饰变量时则代表这个变量不能被修改。

在日常使用中,我们推荐使用final关键字来明确表示我们代码的语义,逻辑的意图,我们可以将一些方法或者类声明为final,这样就可以明确告诉别人,这些行为是不允许被修改的;我们也可以使用final修饰参数或者变量避免意外赋值,而且这样也可以在一定程度上进行一些并发操作。

但是我们需要注意,final并不是immutable

final List<String> strList = new ArrayList<>();
 strList.add("Hello");
 strList.add("world");  
 List<String> unmodifiableStrList = List.of("hello", "world");
 unmodifiableStrList.add("again");
           

上面这段代码中strList就不是不可变的,只是其引用不可变。

如果要实现不可变类,我们需要做到:

  • 将class自身声明为finally
  • 将所有成员变量定义为private和final,并且不要实现setter方法
  • 通常构造对象时,成员变量使用深度拷贝赋值,而不是直接赋值
  • 如果确实需要实现getter方法,或者其他可能会返回内部状态的方法,使用copy-on-write原则,创建私有的copy

    在一些JVM实现中,利用final还能有助于将方法进行内联操作(JIT编译优化),但大部分情况下不需要考虑这些。

finally

finally则是Java保证重点代码一定要被执行的一种机制。我们可以用try-finally或者try-catch-finally来进行类似关闭JDBC链接,保证unlock锁等操作。

finalize

finalize时基础类Object的一个方法,其设计是为保证对象在被进行垃圾回收之前完成对特定资源的操作,但因为它回收对象前要先执行Object.finalize()中的逻辑,降低了内存回收的效率,而且它不能保证被及时执行在JDK9中已经被抛弃了。当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。如果需要进行回收资源的操作可以使用上面的finally或者Java提供的Cleaner机制。我们最多将finalize当作最后守门员。

finalize 的执行是和垃圾收集关联在一起的,一旦实现了非空的 finalize 方法,就会导致相应对象回收呈现数量级上的变慢,有人专门做过 benchmark,大概是 40~50 倍的下降。

因为,finalize 被设计成在对象被垃圾收集前调用,这就意味着实现了 finalize 方法的对象是个“特殊公民”,JVM 要对它进行额外处理。finalize 本质上成为了快速回收的阻碍者,可能导致你的对象经过多个垃圾收集周期才能被回收。

有人也许会问,我用 System.runFinalization​() 告诉 JVM 积极一点,是不是就可以了?也许有点用,但是问题在于,这还是不可预测、不能保证的,所以本质上还是不能指望。实践中,因为 finalize 拖慢垃圾收集,导致大量对象堆积。

我们可以看一下下面的代码:

private void runFinalizer(JavaLangAccess jla) {
 //  ... 省略部分代码
 try {
    Object finalizee = this.get(); 
    if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
       jla.invokeFinalize(finalizee);
       // Clear stack slot containing this variable, to decrease
       // the chances of false retention with a conservative GC
       finalizee = null;
    }
  } catch (Throwable x) { }
    super.clear(); 
 }
           

我们可以看到,在runFinalizer中生吞了异常。

Java平台目前逐步开始使用java.lang.ref.Cleaner来替换掉原有的finalize实现。Cleaner机制使用了幻想引用和引用队列,这样我们可以保证在对象被彻底销毁前做一些类似资源回收的工作。吸取了finalize里的教训,每个Cleaner操作都是独立的,它有自己的线程,可以避免意外死锁。但同样如果因为一些原因导致幻象引用堆积还是会出现资源无法及时回收的情况,所以我们也不应该过多依赖Cleaner机制。

public class CleaningExample implements AutoCloseable {
        // A cleaner, preferably one shared within a library
        private static final Cleaner cleaner = <cleaner>;
        static class State implements Runnable { 
            State(...) {
                // initialize State needed for cleaning action
            }
            public void run() {
                // cleanup action accessing State, executed at most once
            }
        }
        private final State;
        private final Cleaner.Cleanable cleanable
        public CleaningExample() {
            this.state = new State(...);
            this.cleanable = cleaner.register(this, state);
        }
        public void close() {
            cleanable.clean();
        }
    }