JavaFX和监听器内存泄漏问题

5
我对JavaFx 8和监听器内存泄漏问题有些困惑。官方文档说:

ObservableValue存储了一个对监听器的强引用,这会阻止监听器被垃圾回收,并可能导致内存泄漏。

我希望有一个例子能够说明使用ObservableValue<T>addListener方法会导致内存泄漏。

例如,如果我有一个像这样的类:

public class ConfigurationPane extends AnchorPane {
    @FXML
    private Label titleLabel;

    public ConfigurationPane () {
        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("view/ConfigurationPane .fxml"));
    fxmlLoader.setRoot(this);
    fxmlLoader.setController(this);
    try {
        fxmlLoader.load();
    } catch (IOException e) {
          e.printStackTrace();
      }
}

    @FXML
    private void initialize() {
        titleLabel.sceneProperty().addListener(new MyListener());
    }
}

我会翻译中文。当一个ConfigurationPane对象被垃圾回收时,MyListener对象也将被垃圾回收,我会有内存泄漏吗?我无法看到一个场景能够防止监听器被垃圾回收的强引用。顺便说一下,我看到其他关于这个问题的S.O.问题,但是没有一个对我理解问题有所帮助。谢谢。
1个回答

3

这意味着存储侦听器的地图没有使用弱引用,您需要自己删除侦听器以避免内存泄漏。

在下面的示例中,尽管相应的TextField已从场景中移除,但LeakingListener对象永远不会被释放:

public class LeakListener extends Application {

    private static class LeakingListener implements InvalidationListener {

        private final TextField tf;
        private final int[] placeHolder = new int[50000]; // to simplify monitoring

        public LeakingListener(TextField tf) {
            this.tf = tf;
        }

        public void invalidated(Observable i) {
            tf.setText(tf.getText() + ".");
        }
    }

    @Override
    public void start(Stage primaryStage) {
        final Pane root = new VBox(3);

        final Button btnType = new Button("Type in all");

        Button btnAdd = new Button("Add");
        btnAdd.setOnAction((e) -> {
            TextField tf = new TextField();
            root.getChildren().add(tf);
            // memory leaking listener which never gets cleaned
            btnType.armedProperty().addListener(new LeakingListener(tf));
        });

        Button btnRemove = new Button("Remove");
        btnRemove.setOnAction((ActionEvent e) -> {
            // find random TextEdit element
            Optional<Node> toRemove = root.getChildren().stream().filter((Node t) -> t instanceof TextField).findAny();
            // if any, and remove it
            if (toRemove.isPresent()) {
                root.getChildren().remove(toRemove.get());
            }
        });

        Button btnMemory = new Button("Check Memory");
        btnMemory.setOnAction((e) -> {
            System.gc();
            System.out.println("Free memory (bytes): " + Runtime.getRuntime().freeMemory());
        });

        root.getChildren().addAll(btnAdd, btnRemove, btnType, btnMemory);
        Scene scene = new Scene(root, 200, 350);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

如果ObservableValue存储了一个对监听器的弱引用,那么你就不会遇到问题。这可以通过下面的示例来模拟:
public class LeakListener extends Application {

    private static class NonLeakingListener implements InvalidationListener {

        // we need listener to don't hold reference on TextField as well
        private final WeakReference<TextField> wtf;
        private final int[] placeHolder = new int[10000];

        public NonLeakingListener(TextField tf) {
            this.wtf = new WeakReference<>(tf);
        }

        public void invalidated(Observable i) {
            if (wtf.get() != null) {
                wtf.get().setText(wtf.get().getText() + ".");
            }
        }
    }

    @Override
    public void start(Stage primaryStage) {
        final Pane root = new VBox(3);

        final Button btnType = new Button("Type in all");

        // Here is rough weak listeners list implementation
        WeakHashMap<TextField, NonLeakingListener > m = new WeakHashMap<>();
        btnType.armedProperty().addListener((e)-> {
            for (TextField tf : m.keySet()) {
                m.get(tf).invalidated(null);
            }
        });


        Button btnAdd = new Button("Add");
        btnAdd.setOnAction((e) -> {
            TextField tf = new TextField();
            root.getChildren().add(tf);
            m.put(tf, new NonLeakingListener(tf));
        });

        Button btnRemove = new Button("Remove");
        btnRemove.setOnAction((e) -> {
            // find random TextEdit element
            Optional<Node> toRemove = root.getChildren().stream().filter((Node t) -> t instanceof TextField).findAny();
            // if any, and remove it
            if (toRemove.isPresent()) {
                root.getChildren().remove(toRemove.get());
            }
        });

        Button btnMemory = new Button("Check Memory");
        btnMemory.setOnAction((e)-> {
            System.gc();
            System.out.println("Free memory (bytes): " + Runtime.getRuntime().freeMemory());
        });

        root.getChildren().addAll(btnAdd, btnRemove, btnType, btnMemory);
        Scene scene = new Scene(root, 200, 350);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

为什么LeakingListener会造成内存泄漏?问题在于它对tf TextField保持了强引用。那么,在我上面的例子中,MyListener不会造成内存泄漏吗? - Giorgio
没错,你的例子是安全的。只有在动态添加/删除组件并使用监听器时才可能出现泄漏。 - Sergey Grinev
好的。谢谢你,但我仍然不明白为什么如果我动态添加/删除带有监听器的组件,就会创建泄漏。 :-( - Giorgio
@SergeyGrinev 我知道这个问题很老了,但是第一个例子并没有产生内存泄漏——至少在JDK/JavaFX 16上没有。此外,GC不应该检测到循环引用吗(https://dev59.com/I3VC5IYBdhLWcg3w0EoD)?根据链接问题中被接受的答案,即使自从Java 1.2以来,GC也能够删除这些循环依赖关系。这就引出了一个问题,为什么JavaFX文档会提到潜在的内存泄漏。也许他们使用必须要释放的本地代码?我想知道你对此有什么看法。 - Expert Thinker El Rey

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