内容更改时 SwingNode 的内容未被垃圾收集

Content of SwingNode not garbage collected when content changed

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

一切正常,只是我发现 swing 组件永远不会被垃圾回收。它们包含大量数据,所以一段时间后它会变成非常严重的内存泄漏。

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

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 中的一个问题,但我想在将其报告为错误之前先检查一下这里。

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

好的,我明白了。

简答

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

长答案

感谢此答案中的建议: 我能够确定泄漏是由 GlassStage 引起的,它是非 api class除其他事项外,保留 GlassStage 所有实现的静态列表。 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);
    }
}

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

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

问题是由于 JLightweightFrame 从未被 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 中即可获得良好的结果。