如何确保始终调用finalize()方法 (Thinking in Java练习)

16

我正在慢慢地学习Bruce Eckel的《Java编程思想》第四版,以下问题让我感到困惑:

创建一个带有finalize()方法并打印消息的类。在main()中,创建该类的一个对象。修改上一个练习,使您的finalize()方法始终被调用。

这是我的代码:

public class Horse {
    boolean inStable;
    Horse(boolean in){
        inStable = in;
    }   
    public void finalize(){
        if (!inStable) System.out.print("Error: A horse is out of its stable!");
    }
}
public class MainWindow {
    public static void main(String[] args) {
        Horse h = new Horse(false);
        h = new Horse(true);
        System.gc();
    }
}

该代码创建一个新的 Horse 对象,并将布尔值 inStable 设置为 false。现在,在 finalize() 方法中,它检查 inStable 是否为 false。如果是,则打印一条消息。

不幸的是,没有消息被打印出来。由于条件计算结果为 true,我猜测 finalize() 一开始就没有被调用。我已经运行了这个程序很多次,只有几次看到错误消息被打印出来。我原本以为当调用 System.gc() 时,垃圾收集器会回收任何未被引用的对象。

搜索得到一个正确答案的链接:这里,提供了更详细、更复杂的代码。它使用了我之前没见过的方法,如 System.runFinalization()Runtime.getRuntime()System.runFinalizersOnExit()

能否有人帮助我更好地理解 finalize() 如何工作,如何强制其运行,或者指导我如何理解解决方案代码?


我曾经认为当调用System.gc()时,垃圾回收器会回收任何未被引用的对象。但在你的情况下,实际上确实存在对你的对象的引用,那么为什么应该销毁它呢?此外,我不认为保证System.gc()实际上会回收任何东西。 - Niklas B.
2
在这行代码中:h = new Horse(true);我将 h 设置为一个新的对象,使之前的对象(其中 inStable 被设置为 false)成为无引用状态。 - MattDs17
是的,在这种情况下,请参考我的评论的后半部分。没有任何保证。那个外部页面的解决方案实际上不是真正的解决方案。 - Niklas B.
5个回答

19
当垃圾回收器发现一个符合回收条件但具有finalizer的对象时,它不会立即释放它。垃圾回收器尽可能地快速完成,因此它只是将对象添加到待处理finalizer列表中。finalizer稍后在单独的线程上调用。
您可以通过在垃圾回收后调用方法System.runFinalization来告诉系统尝试立即运行待处理的finalizers。
但是,如果您想要强制运行finalizer,则必须自己调用它。垃圾回收器不保证任何对象将被回收或finalizer将被调用。它只是尽力而为。但是在真实代码中几乎不需要强制运行finalizer。

2
我现在认为我理解了。我查阅了官方的解决方案指南,其中使用了System.gc(); System.runFinalization(); 当我尝试时,这个方法是有效的。我假设需要在调用 System.runFinalization() 之前调用 **System.gc()**,因为它本质上将对象标记为“待完成”,以便在运行 System.runFinalization() 时,它有一个实际运行的对象。我的观察是否准确? - MattDs17
2
@MattDs17:这可能是你特定的JVM实现方式,但它并没有规定要这样工作。 - Niklas B.
3
当然,这就是你应该一开始就避免使用终结方法的部分原因。 - Louis Wasserman
1
@Elenasys:谢谢,已修复链接指向最新文档。 - Mark Byers
2
然而,在实际代码中,你很少需要强制运行终结器。这是因为缺乏保证意味着终结器对于任何关键性的事情,比如写入数据或释放系统锁定,基本上是毫无用处的。如果终结器真正起作用,它们将有大量的用途。 - Tom Swirly
显示剩余4条评论

1
除了玩具场景外,通常无法确保对于没有“有意义”的引用存在的对象调用finalize,因为垃圾回收器无法知道哪些引用是“有意义”的。例如,类似于ArrayList的对象可能具有“清除”方法,该方法将其计数设置为零,并使支持数组中的所有元素有资格被未来的Add调用覆盖,但实际上并不清除该支持数组中的元素。如果对象的大小为50,并且其Count为23,则可能不存在任何执行路径,通过该路径代码可以检查数组的最后27个插槽中存储的引用,但垃圾回收器无法知道这一点。因此,垃圾回收器将永远不会在那些插槽中的对象上调用finalize,除非容器覆盖了那些数组插槽,容器放弃了数组(也许是为了更小的数组),或者所有根引用到容器本身被销毁或以其他方式停止存在。
有多种方法可以鼓励系统调用finalize,以释放那些没有强根引用的对象(这似乎是问题的重点,并且其他答案已经涵盖了这一点),但我认为有必要注意存在强根引用的对象集合和代码可能感兴趣的对象集合之间的区别。这两个集合大部分重叠,但每个集合都可能包含另一个集合中不存在的对象。对象的终结器在GC确定对象将不再存在,但由于终结器的存在而运行;这可能与它们对任何人不再有兴趣的时间是否一致。虽然如果能够使所有不再有兴趣的对象的终结器运行将会很有帮助,但通常情况下这是不可能的。

0

这是对我有用的(部分地,但它确实说明了这个想法):

class OLoad {

    public void finalize() {
        System.out.println("I'm melting!");
    }
}

public class TempClass {

    public static void main(String[] args) {
        new OLoad();
        System.gc();
    }
}

代码行 new OLoad(); 所做的就是创建一个没有引用附加的对象。这有助于 System.gc() 运行 finalize() 方法,因为它检测到一个没有引用的对象。像 OLoad o1 = new OLoad(); 这样说不起作用,因为它会创建一个在 main() 结束之前一直存在的引用。不幸的是,这种方法大多数情况下有效。正如其他人指出的那样,除非自己调用,否则无法确保 finalize() 总是被调用。


0

运行new constructor()和System.gc()超过两次。

public class Horse {
    boolean inStable;
    Horse(boolean in){
        inStable = in;
    }   
    public void finalize(){
        if (!inStable) System.out.print("Error: A horse is out of its stable!");
    }
}
public class MainWindow {
    public static void main(String[] args) {
        for (int i=0;i<100;i++){
            Horse h = new Horse(false);
            h = new Horse(true);
            System.gc();
        }
    }
}  

0
调用垃圾回收器(System.gc())方法“建议”Java虚拟机花费精力回收未使用的对象,以使它们当前占用的内存可用于快速重用(即仅是对jvm的建议,不强制其立即执行该操作,可能会执行也可能不执行)。当从方法调用返回时,Java虚拟机已尽最大努力从所有丢弃的对象中回收空间。垃圾收集器在确定不再有对该对象的引用时,在对象上调用finalize()。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接