该知识点是自己从书籍中学习的笔记
背景
终结器的理念是允许 Java 方法释放任何需要返回到操作系统的本机资源。使用Finalizers会带来一些不可预期的危险、古怪的结果、性能降低、移植性问题,所以通常情况下,作为一种规则,都不要使用Finalizers。当然Finalizers是有一点作用的。
在c++,通常是使用析构函数来回收已经分配给对象的资源或者其他资源。在java中,是自动回收不资源的,当然也可以通过try/finally代码块来实现和C++的析构函数的功能。下面将对使用Finalizers的优缺做说明。
不使用Finalizers的原因
1. Finalizers并不能够保证立即执行,一个对象变为不可获取到finalizers执行的这段时间是不确定的。也就意味着,在finalizers为执行的时候,你不能够做任何事情。比如在finalizers中关闭文件的问题,如果finalizers未执行完毕的时候,再次打开文件的时候就会出现问题。
2. Finalizers不能够立即执行,还可能导致内存溢出问题,因为释放的资源都需要在finalizers中执行,但是finalizers却不能够保证立即执行,这样就导致内存积压过多,程序就死掉了。
3. 不仅Java语言规范没有提供Finalizers将被立即执行的保证,而且也没有说Finalizers最终也会执行的保证。也就是说一些不可用的对象上的finalizer不一定会执行。因此,不要依靠finalizer来更新系统关键性的状态。比如说,使用finalizer来释放一个共享资源的永久锁(数据库)会导致你的分布式系统垮掉。
4.System.gc 和 System.runFinalization虽然可以提高finalizer的执行机遇,但是并不能够保证一定执行。System.runFinalizersOnExit和 Runtime.runFinalizersOnExit可以保证finalizer一定执行,但是这两个方法有致命的危险,已经被舍弃了。
5. 正常情况一个未捕获的异常都将被打印出来,但是如果将未捕获的异常发生在finalizer的话,则什么也不会打印出来。也就是说出了问题,我们都无法明白其原因。
6. 使用finalizers的话,会导致性能下降。
对一个类的对象封装了要求中断的资源,比如说fileds或者threads,替换finalizer的方法:在类中明确地提供一个
中断的方法,当实例不再使用的时候,类的使用者对调用该方法。这种方法在:InputStream、OutputStream,java.sql.Connection
的close方法是很常见的;在java.util.Timer的cancel方法也这样的;还有Graphics.dispose和Window.dispose。
明确地中断方法经常是和try-finally配合着使用的。
// try-finally block guarantees execution of termination method
Foo foo = new Foo(...);
try {
// Do what must be done with foo
...
} finally {
foo.terminate(); // Explicit termination method
}
Finalizer带来的好处:
1.充当“安全网”,当一个对象忘记调用明确地中断方法的时候。使用这种方式总比不使用释放资源要强。但是使用finalizer的时候一定要考虑是否要这样做。同时需要在
finalizer中写一个日志记录,方便调试。
2.对native peers对象来说,finalizer是有用的。因为native object是不能够被jvm回收的。
“Finalizer chaining”性能不是特别好,因此在子类中要通过super.finalizer()手动调用finalizer方法,并且要覆写父类的finalizer方法:
// Manual finalizer chaining
@Override protected void finalize() throws Throwable {
try {
... // Finalize subclass state
} finally {
super.finalize();
}
}
如果子类覆写了父类的finalizer方法,但是忘记了调用。它的父类的finalizer方法是不会被执行的。这种情况下,最好在类中声明一个匿名类来终结资源。
额外知识
在一个类的finalize()方法中创建对象是很不好的,会造成意想不到的问题,如下:
public class Finalizers {
static Finalizers finalizer;
int value;
public Finalizers(int value) {
if (value < 0) {
throw new IllegalArgumentException("Negative Finalizers value");
}
this.value = value;
}
public void finalize() {
finalizer = this;
}
public static void main(String[] args) {
try{
Finalizers f = new Finalizers(-1);
} catch (Exception e) {
}
System.gc();
System.runFinalization();
System.out.println(finalizer.value);
}
}
执行结果是:0.
为什么会是0呢,Finalizers的构造方法明显就做了检查啊?这是因为System.gc()
和 System.runFinalization()
的调用促使 JVM 运行一个垃圾回收周期并运行一些终结器finalize(),就创建了一个包含无效值的 Finalizers对象。看到原因了吧。因此 ,不要在finalize中创建对象。
为了更容易避免此类攻击,而无需引入额外的代码或限制,Java 设计人员修改了 JLS,声明如果在构造 java.lang.Object 之前在构造函数中抛出了一个异常,该方法的 finalize() 方法将不会执行。
但是如何在构造 java.lang.Object 之前抛出异常呢?毕竟,任何构造函数中的第一行都必须是对 this() 或 super() 的调用。如果构造函数没有包含这样的显式调用,将隐式添加对 super() 的调用。所以在创建对象之前,必须构造相同类或其超类的另一个对象。这最终导致了对 java.lang.Object 本身的构造,然后在执行所构造方法的任何代码之前,构造所有子类。
要理解如何在构造 java.lang.Object 之前抛出异常,需要理解准确的对象构造顺序。JLS 明确给出了这一顺序。当创建对象时,JVM:
1.为对象分配空间。
2.将对象中所有的实例变量设置为它们的默认值。这包括对象超类中的实例变量。
3.分配对象的参数变量。
4.处理任何显式或隐式构造函数调用(在构造函数中调用 this() 或 super())。
5.初始化类中的变量。
6.执行构造函数的剩余部分。
重要的是构造函数的参数在处理构造函数内的任何代码之前被处理。这意味着,如果在处理参数时执行验证,可以通过抛出异常预防类被终结。
举例如下:
class Invulnerable {
int value = 0;
public Invulnerable(int value) {
this(checkValues(value));
this.value = value;
}
private Invulnerable(Void checkValues) {
}
static Void checkValues(int value) {
if (value < 0) {
throw new IllegalArgumentException("Negative Finalizers value");
}
return null;
}
@Override
public String toString() {
return (Integer.toString(value));
}
}
public class AttackInvulnerable extends Invulnerable {
static Invulnerable vulnerable;
public AttackInvulnerable(int value) {
super(value);
}
public void finalize() {
vulnerable = this;
}
public static void main(String[] args) {
try {
new AttackInvulnerable(-1);
} catch (Exception e) {
System.out.println(e);
}
System.gc();
System.runFinalization();
if (vulnerable != null) {
System.out
.println("Invulnerable object " + vulnerable + "created!");
} else {
System.out.println("Attack failed");
}
}
}
Invulnerable 的公共构造函数调用一个私有构造函数,而后者调用 checkValues 方法来创建其参数。此方法在构造函数执行调用来构造其超类之前调用,该构造函数是 Object 的构造函数。所以如果 checkValues 中抛出了一个异常,那么将不会终结 Invulnerable 对象。
输出结果:
java.lang.IllegalArgumentException: Negative Finalizers value
Attack failed
总结
终结器是 Java 语言的一种不太幸运的功能。尽管垃圾收集器可自动回收 Java 对象不再使用的任何内存,但不存在回收本机内存、文件描述符或套接字等本机资源的机制。Java 提供了与这些本机资源交互的标准库通常有一个 close() 方法,允许执行恰当的清理,但它们也使用了终结器来确保在对象错误关闭时,没有资源泄漏。
对于其他对象,通常最好避免终结器。无法保证终结器将在何时运行,或者甚至它是否会运行。终结器的存在意味着在终结器运行之前,不会对无法访问的对象执行垃圾收集,而且此对象可能使更多对象存活。这导致活动对象数量增加,进而导致 Java 对象的堆使用率增加。
终结器恢复即将被垃圾收集的能力无疑是终结机制工作方式的一种意外后果。较新的 JVM 实现现在保护代码免遭此类安全隐患。