清理JavaFX属性监听器和绑定(内存泄漏)

24
我还没有找到这两个问题的简单答案:
  1. 在删除属性实例之前,我是否需要先移除监听器(该监听器在其他任何地方都没有使用)?

    BooleanProperty bool = new SimpleBooleanProperty();
    bool.addListener(myListener);
    bool.removeListener(myListener); // is it necessary to do this?
    bool = null;
    
  2. 在删除属性实例之前,我是否需要解绑定一个单向有界属性?

  3. BooleanProperty bool = new SimpleBooleanProperty();
    bool.bind(otherBool);
    bool.unbind(); // is it necessary to do this?
    bool = null;
    

1
不变量,这是我在谷歌上输入关键词后得到的第一个结果。如果答案真的在那里,我肯定不会问的 :) - dpelisek
2个回答

32

案例1

假设myListener“没有在任何其他地方使用”,因此我认为它是一个[方法]局部变量,答案是否定的。然而,在一般情况下,答案通常是否定的,但有时可以是肯定的。

只要myListener是强可到达的,它就永远不会变得可终结,而且将继续占用内存。例如,如果myListener是一个“通常”声明的static变量(在Java中,所有“正常”的引用都是强引用),那么这就是情况。然而,如果myListener是局部变量,则对象在当前方法调用返回后将不再可访问,bool.removeListener(myListener)有点毫无意义的过度工程。观察者和Observable都超出了范围,最终将被终结。从我自己的博客文章中摘取一句话可能更好地说明问题:

理论

为了完全理解这里的情况,我们必须想起一个 Java 对象的生命周期(source):

如果一个对象可以被某个线程访问到而不需要遍历任何引用对象,则该对象是强可达的。新创建的对象由创建它的线程强可达。[...] 如果一个对象不是强可达的,但可以通过遍历弱引用来访问,则该对象是弱可达的。当对弱可达对象的弱引用被清除时,该对象就变得可以进行终结。

如果你把盒子扔进海洋里,那么盒子里有没有猫都无所谓。如果盒子无法被找到,那么猫也是无法被找到的。

在静态变量的情况下,只要类被加载,这些变量就始终可访问,因此是可达的。如果我们不希望静态引用阻碍垃圾回收器的工作,那么我们可以声明变量使用WeakReference。JavaDoc中说:

弱引用对象[..]不会阻止其引用对象被标记为可终结、终结和回收。[..]假设垃圾回收器在某个时间点确定一个对象是弱可达的。那时,它将原子性地清除对该对象的所有弱引用[..]同时,它将声明所有以前弱可达的对象为可终结。

显式管理

举个例子,假设我们编写了一个JavaFX太空模拟游戏。每当一个可观察的星球进入飞船观察者的视野时,游戏引擎会将飞船与星球注册。很明显,每当星球离开视野时,游戏引擎也应该使用Observable.removeListener()将飞船作为星球的观察者移除。否则,随着飞船继续在太空中飞行,内存将泄漏。最终,游戏无法处理50亿个被观察的星球,并且会因为OutOfMemoryError而崩溃。
请注意,对于绝大多数JavaFX监听器和事件处理程序,它们的生命周期与其Observable并行,因此应用程序开发人员无需担心。例如,我们可以构造一个{{link1:TextField}}并向文本字段的textProperty注册一个验证用户输入的监听器。只要文本字段存在,我们希望监听器也存在。早晚,当文本字段不再使用时,它将被垃圾回收,验证监听器也将被垃圾回收。

自动管理

继续以太空模拟为例,假设我们的游戏有限的多人支持,所有玩家都需要观察彼此。也许每个玩家都会保留一个本地的击杀指标积分牌,或者他们需要观察广播的聊天消息。原因并不重要。当一个玩家退出游戏时会发生什么?显然,如果监听器没有被明确管理(删除),那么退出游戏的玩家将无法成为最终化的对象。其他玩家将保持对离线玩家的强引用。明确删除监听器仍然是一个有效的选择,可能是我们游戏中最受欢迎的选择,但是假设它感觉有点突兀,我们想找到更加流畅的解决方案。
我们知道游戏引擎会在玩家在线的时间内一直保持对所有在线玩家的强引用。因此,我们希望飞船只在游戏引擎保持强引用的时间内监听彼此的变化或事件。如果您阅读了“理论”部分,那么肯定会认为弱引用是一个解决方案。
然而,仅仅将某物包装在WeakReference中并不能解决问题。它很少能够解决问题。当对“referent”的最后一个强引用设置为null或无法访问时,参考对象就有资格进行垃圾回收(假设使用SoftReference也无法访问参考对象)。但是WeakReference仍然存在。应用程序开发人员需要添加一些管道,以便从他放置的数据结构中删除WeakReference本身。如果没有这样做,则我们可能已经减轻了内存泄漏的严重程度,但由于动态添加的WeakReference也会消耗内存,因此仍然存在内存泄漏问题。

JavaFX为我们提供了接口WeakListener和类WeakEventHandler,作为一种“自动删除”的机制。所有相关类的构造函数都接受客户端代码提供的真实监听器/处理程序,但它们使用弱引用存储监听器/处理程序。

如果您查看WeakEventHandler的JavaDoc,您会注意到该类实现了EventHandler,因此可以在任何需要EventHandler的地方使用WeakEventHandler。同样,已知的WeakListener的实现可以在任何需要InvalidationListenerChangeListener的地方使用。

如果您查看WeakEventHandler的源代码,您会注意到该类实际上只是一个包装器。当它的引用(真正的事件处理程序)被垃圾回收时,WeakEventHandler通过在调用 WeakEventHandler.handle() 时不执行任何操作来“停止工作”。WeakEventHandler并不知道它已连接到哪个对象,即使它知道,移除事件处理程序也不是均匀的。然而,所有已知的实现WeakListener的类都具有竞争优势。当它们的回调被调用时,它们被隐式或显式地提供了一个对其注册的Observable的引用。因此,当WeakListener的引用被垃圾回收时,WeakListener实现最终会确保从Observable中删除WeakListener本身。
如果尚不清楚,我们太空模拟游戏的解决方案将是让游戏引擎对所有在线飞船使用强引用。当飞船上线时,使用弱侦听器(例如WeakInvalidationListener)向新玩家注册所有其他在线飞船。当玩家离线时,游戏引擎会删除他对玩家的强引用,并使该玩家有资格进行垃圾回收。游戏引擎无需费心将离线玩家作为其他玩家的侦听器明确删除。

情境2

不行。 为更好地理解接下来要说的,请先阅读我的情境1答案。

BooleanPropertyBase 存储了对 otherBool 的强引用。这本身并不会导致 otherBool 总是可达,从而潜在地导致内存泄漏。当 bool 变得不可达时,所有存储的引用也将变得不可达(假设它们没有被存储在其他地方)。

BooleanPropertyBase 还通过将自身作为绑定属性的观察者来工作。然而,它通过包装自己在一个类中来实现,该类几乎与我在第一种情况中描述的 WeakListener 相同。因此,一旦你将 bool 置空,它将很快从 otherBool 中移除。


4
说实话,我不理解这个答案。情况1-是或否。情况2-不是,见情况1。 - Pavel_K
1
第二种情况的答案是否定的。对于直接滚动到第二种情况的新读者,对第一种情况的回溯只是一个提示,他们应该先阅读第一种情况以更好地理解第二种情况的解释。这些东西本质上很复杂,我很讨厌自己无法简化文本。同时,我觉得这个主题应该使用技术上正确的语言来撰写,涵盖所有方面,否则留下空白只会引发新问题,使读者更加不安和困惑。 - Martin Andersson
我明白了。谢谢你的解释。 - Pavel_K
我相信大多数人需要反复阅读这样的答案才能理解它,并可能在一旁进行广泛的谷歌搜索。希望您能理解答案,但如果您发现可以改进,请随意编辑。简化总是一种改进!=)此外,请告诉我是否无法理解它。如果是这样,我会尝试提供替代方案。 - Martin Andersson
1
非常感谢您的愿意提供帮助。我认为,乐于助人是一个人最重要的品质之一。前段时间读完这篇文章后,我得出结论:属性监听器和绑定(特别是双向绑定)的情况并不那么清晰。幸运的是,我使用MVVM模式,我的组件中有视图和视图模型,它们与它们的属性密切耦合。然而,模型没有任何FX属性,因此当我销毁我的组件时,我不在乎视图是否仍与视图模型属性相关联。我同时销毁它们两个。 - Pavel_K
这是否也适用于自定义绑定?例如,如果我使用Bindings.createBooleanBinding()创建一个绑定,那么在绑定被垃圾回收之前没有必要解除依赖关系,对吗? - Vladimir M.

8
我完全同意案例1的答案,但案例2有些棘手。bool.unbind()调用是必要的。如果省略,它引起小的内存泄漏。

如果运行以下循环,应用程序最终会耗尽内存。

BooleanProperty p1 = new SimpleBooleanProperty();
while(true) {
    BooleanProperty p2 = new SimpleBooleanProperty();
    p2.bind(p1)
}

BooleanPropertyBase在内部并未使用真正的WeakListener(实现WeakListener接口的类),而是使用了一个不完整的解决方案。所有“p2”实例最终都会被垃圾回收,但是每个“p2”都会在内存中保留一个持有空WeakReference的监听器,这些监听器将永远驻留在内存中。对于所有属性,不仅限于BooleanPropertyBase,都是如此。这里详细解释了它,并且他们说在Java 9中已经修复了该问题。
在大多数情况下,您不会注意到这种内存泄漏,因为它只会为每个未解除绑定的绑定留下几十个字节。但在某些情况下,它给我带来了真正的麻烦。一个很好的例子是频繁更新的表格的单元格。这些单元格一直重新绑定到不同的属性,而这些内存中的剩余部分会快速积累。

你是在引用@Martin Andersson的答案吗? - Neuron

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