清理 JavaFX 属性侦听器和绑定(内存泄漏)
Posted
技术标签:
【中文标题】清理 JavaFX 属性侦听器和绑定(内存泄漏)【英文标题】:Clean JavaFX property listeners and bindings (memory leaks) 【发布时间】:2013-01-11 13:39:24 【问题描述】:我还没有找到这两个问题的简单答案:
在删除属性实例之前是否必须删除监听器(监听器不在其他任何地方使用)?
BooleanProperty bool = new SimpleBooleanProperty();
bool.addListener(myListener);
bool.removeListener(myListener); // is it necessary to do this?
bool = null;
在删除属性实例之前是否必须取消绑定单向有界属性?
BooleanProperty bool = new SimpleBooleanProperty();
bool.bind(otherBool);
bool.unbind(); // is it necessary to do this?
bool = null;
【问题讨论】:
在这里查看类似的讨论:forums.oracle.com/forums/… 不变量,这是我把关键词放到google上的第一个结果。确保我不会问答案是否真的存在:) 【参考方案1】:我完全同意 case 1 的答案,但是 case 2 有点棘手。 The bool.unbind()
电话是必要的。如果省略,它确实会导致少量内存泄漏。
如果运行以下循环,应用程序最终会耗尽内存。
BooleanProperty p1 = new SimpleBooleanProperty();
while(true)
BooleanProperty p2 = new SimpleBooleanProperty();
p2.bind(p1)
BooleanPropertyBase 本质上不使用真正的 WeakListener(WeakListener 接口的实现),它使用的是半生不熟的解决方案。所有“p2”实例最终都会被垃圾回收,但是对于每个“p2”,持有空 WeakReference 的侦听器将永远保留在内存中。这同样适用于所有属性,不仅是 BooleanPropertyBase。 It's explained here 的详细信息,他们说它已在 Java 9 中修复。
在大多数情况下,您不会注意到这种内存泄漏,因为它只为每个未解除绑定的绑定留下了几十个字节。但在某些情况下,它给我带来了真正的麻烦。一个很好的例子是经常更新的表格的表格单元格。然后细胞会一直重新绑定到不同的属性,这些残留在记忆中的东西会迅速积累。
【讨论】:
您是否引用了@Martin Andersson 的答案?【参考方案2】:案例一
鉴于myListener
“未在其他任何地方使用”,因此我假设是一个 [method-] 局部变量,答案是 否。但在一般情况下,答案大多是否,但有时可能是是。
只要myListener
是强可达的,那么它就永远不会有资格进行终结,并且会继续消耗内存。例如,如果myListener
是“正常”声明的static
变量(*Java 中的所有“正常”引用都是strong 引用*),就会出现这种情况。但是,如果myListener
是一个局部变量,那么在当前方法调用返回后,对象将不再可达,bool.removeListener(myListener)
有点无意义的过度工程。观察者和Observable
都超出了范围,最终将被最终确定。我自己的 blog post 关于这个答案的引用可能会描绘出更好的画面:
盒子是否知道里面的猫并不重要,如果你 把盒子扔进海里。如果无法访问该框,也无法访问该框 猫。
理论
要完全理解这里的情况,我们必须提醒自己Java对象的生命周期(source):
如果某个对象可以被某个线程访问,则它是强可达的 不遍历任何参考对象。一个新创建的对象是 创建它的线程可强烈访问。 [..] 一个对象是 如果 [not] 强烈 [..] 可到达但可以 通过遍历弱引用到达。当弱引用 弱可达对象被清除,对象变得有资格 最终确定。
在静态变量的情况下,只要类被加载,它们就总是可以访问的,因此是可以访问的。如果我们不希望静态引用成为阻碍垃圾收集器完成工作的引用,那么我们可以将变量声明为使用WeakReference
。 JavaDoc 说:
弱引用对象 [..] 不会阻止它们的引用对象 最终化,最终化,然后回收。 [..] 假设 垃圾收集器在某个时间点确定一个对象 是弱可达的。到那时它将原子地清除所有弱 对该对象的引用[..]。同时它将宣布所有 以前弱可达对象的数量将被最终确定。
显式管理
为了说明,假设我们编写了一个 JavaFX 空间模拟游戏。每当Observable
行星进入宇宙飞船观察者的视野时,游戏引擎就会将宇宙飞船注册到该行星。很明显,每当行星离开视野时,游戏引擎也应该使用Observable.removeListener()
将宇宙飞船作为行星的观察者移除。否则,随着飞船继续在太空中飞行,内存就会泄漏。最终,游戏无法处理 50 亿颗观测到的行星,它会以OutOfMemoryError
崩溃。
请注意,对于绝大多数 JavaFX 侦听器和事件处理程序,它们的生命周期与 Observable
的生命周期平行,因此应用程序开发人员无需担心。例如,我们可以构造一个TextField
并使用文本字段的textProperty
注册一个验证用户输入的侦听器。只要文本字段存在,我们就希望听众持续存在。迟早,文本字段不再使用,当他被垃圾收集时,验证侦听器也被垃圾收集。
自动管理
继续空间模拟示例,假设我们的游戏对多人游戏的支持有限,并且所有玩家都需要互相观察。也许每个玩家都有一个本地的击杀指标计分板,或者他们可能需要观察广播的聊天消息。原因不是这里的重点。当玩家退出游戏时会发生什么?显然,如果监听器没有被明确管理(移除),那么退出的玩家将没有资格进行最终确定。其他玩家将保持对离线玩家的强烈参考。显式删除侦听器仍然是一个有效的选项,并且可能是我们游戏的最首选选择,但假设它感觉有点突兀,我们希望找到一个更巧妙的解决方案。
我们知道游戏引擎会强烈引用所有在线玩家,只要他们在线。所以我们希望宇宙飞船只在游戏引擎保持强引用的情况下监听彼此的变化或事件。如果您阅读“理论”部分,那么WeakReference
听起来肯定是一个解决方案。
但是,仅将某些内容包装在 WeakReference 中并不是完整的解决方案。它很少是。确实,当对“referent”的最后一个强引用设置为null
或变得无法访问时,该referent 将有资格进行垃圾回收(假设无法使用SoftReference
访问该referent)。但是 WeakReference 仍然存在。应用程序开发人员需要添加一些管道,以便将 WeakReference 本身从他放入的数据结构中删除。如果没有,那么我们可能已经降低了内存泄漏的严重性,但内存泄漏仍然存在,因为动态添加了弱引用也会消耗内存。
幸运的是,JavaFX 添加了接口WeakListener
和类WeakEventHandler
作为“自动删除”的机制。所有相关类的构造函数都接受客户端代码提供的真实监听器/处理程序,但它们使用弱引用存储监听器/处理程序。
如果您查看WeakEventHandler
的JavaDoc,您会注意到该类实现了EventHandler
,因此可以在任何需要EventHandler 的地方使用WeakEventHandler。同样,WeakListener
的已知实现可以在任何需要 InvalidationListener
或 ChangeListener
的地方使用。
如果您查看WeakEventHandler
的源代码,您会注意到该类基本上只是一个包装器。当他的所指对象(真正的事件处理程序)被垃圾收集时,WeakEventHandler
在调用WeakEventHandler.handle()
时根本不做任何事情来“停止工作”。 WeakEventHandler
不知道他连接了哪个对象,即使他知道了,事件处理程序的删除也不是同质的。不过,所有已知的WeakListener
实现类都具有竞争优势。当它们的回调被调用时,它们被隐式或显式地提供了对它们注册的Observable
的引用。因此,当WeakListener
的所指对象被垃圾回收时,最终WeakListener
实现将确保WeakListener
本身从Observable
中删除。
如果还不清楚,我们的太空模拟游戏的解决方案是让游戏引擎使用对所有在线宇宙飞船的强引用。当一艘宇宙飞船上线时,所有其他在线宇宙飞船都会使用弱监听器向新玩家注册,例如WeakInvalidationListener
。当玩家下线时,游戏引擎会删除他对玩家的强引用,玩家将有资格进行垃圾回收。游戏引擎不必费心将离线玩家显式移除为其他玩家的侦听器。
案例 2
否。为了更好地理解我接下来要说的内容,请先阅读我的案例 1 答案。
BooleanPropertyBase
存储对otherBool
的强引用。这本身不会导致otherBool
始终可访问,因此可能会导致内存泄漏。当bool
变得无法访问时,其所有存储的引用也会这样做(假设它们没有存储在其他任何地方)。
BooleanPropertyBase
也可以通过将自身添加为您绑定到的属性的Observer
来工作。但是,它通过将自身包装在一个几乎与我的案例 1 答案中描述的 WeakListener
s 完全相同的类中来实现这一点。因此,一旦您取消了bool
,它从otherBool
中删除只是时间问题。
【讨论】:
说实话我不明白这个答案。案例 1 - 否或是。案例 2 - 不,请参阅案例 1。 案例 2 的答案是“否”。对案例 1 的反向引用仅用于直接滚动到案例 2 的新读者,作为提示他们应该先阅读案例 1 以更好地理解案例 2 中的解释。这些东西本质上很复杂,我讨厌我没有无法进一步简化文本。同时,我觉得应该使用涵盖所有方面的技术正确的语言来编写该主题,因为否则,留下空白只会引发新的问题,使读者更加不安全和困惑。 我明白了。谢谢你的解释。 我相信我们大多数人有必要重新阅读这样的答案几次才能理解它,并且可能会在旁边进行广泛的谷歌搜索。希望您能理解答案,但如果您发现可以进行改进,请随时对其进行编辑。简化总是一种改进! =)另外,如果您找不到理解它的方法,请告诉我。如果是这样,我会尝试想出一个替代方案。 非常感谢您的帮助。我认为这是一个人最重要的品质之一——愿意帮助别人。前段时间阅读后,我得出结论,属性侦听器和绑定(特别是双向绑定)的情况并不那么清楚。幸运的是,我使用 MVVM 模式,并且组件中的视图和视图模型与其属性紧密耦合。但是,模型没有任何 fx 属性,所以当我销毁我的组件时,我不在乎视图是否仍然链接到视图模型属性。我把他们两个都毁了。以上是关于清理 JavaFX 属性侦听器和绑定(内存泄漏)的主要内容,如果未能解决你的问题,请参考以下文章