【问题标题】:Content of SwingNode not garbage collected when content changedSwingNode 的内容在内容更改时不会被垃圾收集
【发布时间】:2021-02-19 01:20:37
【问题描述】:

我有一个通过 SwingNode 显示 Swing 图的 JavaFX 应用程序。由于摆动组件的编写方式以及我不愿意对其进行重构,因此每次用户需要更新数据时,我都会创建一个新的摆动图实例。换句话说,每当用户重新生成绘图时,它都会创建一个新的 Swing 组件并将 SwingNode 的内容设置为新组件。

除了我发现摆动组件永远不会被垃圾收集之外,一切都很好。它们包含大量数据,因此一段时间后它会成为非常严重的内存泄漏。

我已经能够通过这个最小的可重现示例来证明问题:

public class LeakDemo extends Application {

    //Keep week references to all panels that we've ever generated to see if any
    //of them get collected.
    private Collection<WeakReference<JPanel>> panels = new CopyOnWriteArrayList<>();
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        
        SwingNode node = new SwingNode();
        
        Pane root = new Pane();
        root.getChildren().add(node);
        
        //Kick off a thread that repeatedly creates new JPanels and resets the swing node's content
        new Thread(() -> {
            
            while(true) {
                
                //Lets throw in a little sleep so we can read the output
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                SwingUtilities.invokeLater(() -> {
                    JPanel panel = new JPanel();
                    panels.add(new WeakReference<>(panel));
                    node.setContent(panel);
                });
                
                System.out.println("Panels in memory: " + panels.stream().filter(ref -> ref.get() != null).count());
                
                //I know this doesn't guarantee anything, but prompting a GC gives me more confidence that this
                //truly is a bug.
                System.gc();
            }
            
        }).start();
        
        primaryStage.setScene(new Scene(root, 100, 100));
        
        primaryStage.show();
        
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

程序的输出是

Panels in memory: 0
Panels in memory: 1
Panels in memory: 2
Panels in memory: 3
Panels in memory: 4
Panels in memory: 5
Panels in memory: 6
Panels in memory: 7
Panels in memory: 8
Panels in memory: 9
Panels in memory: 10

并且会继续这样直到成千上万。

我尝试检查来自 jvisualvm 的堆转储,但在引用的海洋中迷失了方向。

我怀疑这是 JavaFX 中的一个问题,但我想在将其报告为错误之前先检查一下。

【问题讨论】:

  • 如果你删除了弱引用列表等,内存泄漏是否仍然发生?如果你只是构建面板并添加弱引用,引用会被清除吗?
  • 当您通过调用revalidate() 告诉容器更新其引用时,行为是否仍然相同?
  • @Charlie 是的,如果我删除 WeakReference 的东西,泄漏仍然会发生。它只是有点难以演示,因为我必须查看 jvisualvm 中的堆转储。
  • @Koenigsberg,JavaFX 组件都没有revalidate() 方法,但swing 组件有。我尝试获取旧的 SwingNode 内容,然后在将其替换为新内容后调用revalidate(),但这并没有成功。
  • 我从未在 visualvm 中调试过内存泄漏,但(非常昂贵)jprofiler 非常有用。您可以简单地找到所有 JPanel 并追溯到 GC 根。

标签: java swing javafx


【解决方案1】:

好的,我想通了。

简答

只需将摇摆内容包装在JPanel(或其他一些JComponent)中。然后只调用一次SwingNode.setContent() 来添加包装器。当您需要更新摇摆内容时,请在您的包装器上调用removeAll(),然后使用适当的内容调用add()

长答案

感谢此答案中的建议:https://stackoverflow.com/a/66283491/2423283 我能够确定泄漏是由GlassStage 引起的,这是一个非 api 类,其中保留了 @987654329 的所有实现的静态列表@。 SwingNode 的内容由 EmbeddedScene 的实例管理,该实例是 GlassStage 的子类型。

当对它们调用close() 时,它们会从静态列表中删除。 SwingNode.setContent() 不会关闭任何预先存在的内容,但 Container.removeAll() 会。

工作代码

以下是固定代码的示例:

public class LeakDemoFixed extends Application {

    //Keep week references to all panels that we've ever generated to see if any
    //of them get collected.
    private Collection<WeakReference<JPanel>> panels = new CopyOnWriteArrayList<>();
    
    @Override
    public void start(Stage primaryStage) throws Exception {
        
        SwingNode node = new SwingNode();
        
        //These 2 lines were added
        JComponent swingContent = new JPanel();
        node.setContent(swingContent);
        
        Pane root = new Pane();
        root.getChildren().add(node);
        
        //Kick off a thread that repeatedly creates new JPanels and resets the swing node's content
        new Thread(() -> {
            
            while(true) {
                
                //Lets throw in a little sleep so we can read the output
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                SwingUtilities.invokeLater(() -> {
                    JPanel panel = new JPanel();
                    panels.add(new WeakReference<>(panel));
                    //Removed the line below
                    //node.setContent(panel);
                    
                    //Added these 2 lines
                    swingContent.removeAll();
                    swingContent.add(panel);
                });
                
                System.out.println("Panels in memory: " + panels.stream().filter(ref -> ref.get() != null).count());
                
                //I know this doesn't guarantee anything, but prompting a GC gives me more confidence that this
                //truly is a bug.
                System.gc();
            }
            
        }).start();
        
        primaryStage.setScene(new Scene(root, 100, 100));
        
        primaryStage.show();
        
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

【讨论】:

  • 不错的发现!这可能值得向 JavaFX 人员提交错误报告;至少,应该在 setContent 方法中更好地记录该行为。
  • 我提交了一个错误报告。完成提交过程后,我将在此处发布链接以供参考。我有点怀疑它是否会采取行动,因为有一些类似的错误被标记为已解决。我不确定这些其他错误是否真的得到了解决,因为我认为这可能是同一个问题。猜猜我们会看到。
【解决方案2】:

我之前没有遇到过这个问题,但这是我为纠正这个问题所做的。我希望将此修复程序集成到 JavaFX 的 Java8 中,但我不知道如何完成,他们关闭了问题(因为重复)

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8284352

问题是由于 JLightweightFrame 从未被 de SwingNodeDisposer 释放。

SwingNodeDisposer 持有一个指向 JLightweightFrame 的硬指针,以防止其被抑制。

我已修改 SwingNode.setContentImpl() 函数以使用 Wea​​kReference 而不是硬指针,现在内存已正确释放。

...

    WeakReference<JLightweightFrame> lwFramePtr = new WeakReference<JLightweightFrame>(lwFrame);
    SwingNodeDisposer disposeRec = new SwingNodeDisposer(lwFramePtr);
    Disposer.addRecord(this, disposeRec);

...

无需使用单个 JPanel 或将其封装在 WeakReference 中即可获得良好的结果。

【讨论】:

  • hmm .. 必须有一个非常令人信服的理由(例如 f.i. 安全问题)才能将修复程序向后移植。这个站点并不是修复错误的好地方(解决方法还可以,但感觉不像,是吗?) - 您可以尝试在 openjfx 开发邮件列表中寻求支持
  • 哦,只是看到副本尚未修复..但已经引用了此 QA,因此您的解决方案不会丢失 :)
  • 是的,问题是这个错误对我们的应用程序至关重要,它由于各种原因卡在 Java8 中。所以我正在尽我所能将它整合到官方版本中,但我猜一个人可以梦想......
【解决方案3】:

使用 visualvm 拍摄快照,然后使用 Eclipse Memory Analyzer (MAT) http://www.eclipse.org/mat/ 分析 GC 根

【讨论】:

  • 感谢您的推荐。我对此表示赞同,因为它确实帮助我解决了这个问题。我不接受它作为答案,因为它更多的是调试技术而不是问题的实际答案。
猜你喜欢
  • 2014-03-04
  • 1970-01-01
  • 1970-01-01
  • 2019-01-18
  • 1970-01-01
  • 1970-01-01
  • 2012-06-12
  • 2012-05-15
  • 1970-01-01
相关资源
最近更新 更多