If it is possible I want to make a ChangeListener
automatically removed from Property
after it is garbage collected. I tried to use WeakChangeListener
but it didn't work. This is my code:
public class NewMain extends Application {
private StringProperty property = new SimpleStringProperty("Initial Value");
private WeakChangeListener<String> weakChangeListener = new WeakChangeListener<>((obs, oldVal, newVal) ->
System.out.println("Value changed: " + oldVal + " -> " + newVal));
@Override
public void start(Stage primaryStage) {
Button changeButton = new Button("Change Value");
Button clearButton = new Button("Clear");
property.addListener(weakChangeListener);
changeButton.setOnAction(e -> property.set("New Value " + Math.random()));
clearButton.setOnAction(e -> {
weakChangeListener = null;
System.gc();
System.runFinalization();
});
VBox root = new VBox(10, changeButton, clearButton);
Scene scene = new Scene(root, 300, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I expect, that after pressing the Clear button the change listener will be removed from the property and won't be called anymore. But it is not removed.
Could anyone say how to do it, if it is possible?
If it is possible I want to make a ChangeListener
automatically removed from Property
after it is garbage collected. I tried to use WeakChangeListener
but it didn't work. This is my code:
public class NewMain extends Application {
private StringProperty property = new SimpleStringProperty("Initial Value");
private WeakChangeListener<String> weakChangeListener = new WeakChangeListener<>((obs, oldVal, newVal) ->
System.out.println("Value changed: " + oldVal + " -> " + newVal));
@Override
public void start(Stage primaryStage) {
Button changeButton = new Button("Change Value");
Button clearButton = new Button("Clear");
property.addListener(weakChangeListener);
changeButton.setOnAction(e -> property.set("New Value " + Math.random()));
clearButton.setOnAction(e -> {
weakChangeListener = null;
System.gc();
System.runFinalization();
});
VBox root = new VBox(10, changeButton, clearButton);
Scene scene = new Scene(root, 300, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I expect, that after pressing the Clear button the change listener will be removed from the property and won't be called anymore. But it is not removed.
Could anyone say how to do it, if it is possible?
Share Improve this question asked Mar 15 at 10:55 SilverCubeSilverCube 6443 silver badges12 bronze badges 2 |1 Answer
Reset to default 4Using implementations of WeakListener
is exactly how you do this. All standard implementations of that interface will remove themselves if invoked after the wrapped listener was garbage collected. Though as far as I can tell, neither the interface nor the implementations guarantee that behavior. Of course, the weak listener not being removed is less likely to be an issue because it's a small, lightweight object. The important thing is that the "real listener" (the one wrapped by the weak listener) was allowed to be garbage collected, as that listener might have captured large, heavyweight objects (e.g., JavaFX nodes) as state. And once the real listener has been garbage collected it obviously cannot be invoked anymore.
The problem with your test is that System::gc
is only a suggestion. It does not guarantee the garbage collector actually runs, let alone actually collects anything. From that method's documentation:
Runs the garbage collector in the Java Virtual Machine.
Calling the
gc
method suggests that the Java Virtual Machine expend effort toward recycling unused objects in order to make the memory they currently occupy available for reuse by the Java Virtual Machine. When control returns from the method call, the Java Virtual Machine has made a best effort to reclaim space from all unused objects. There is no guarantee that this effort will recycle any particular number of unused objects, reclaim any particular amount of space, or complete at any particular time, if at all, before the method returns or ever. There is also no guarantee that this effort will determine the change of reachability in any particular number of objects, or that any particular number ofReference
objects will be cleared and enqueued.
One thing you can try when testing this sort of thing is calling System::gc
in a loop with a delay between each iteration. That will likely increase the chances of the GC running, but of course still doesn't guarantee that it will. Here's an example:
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.event.ActionEvent;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
private final IntegerProperty count = new SimpleIntegerProperty();
// You must keep a strong reference to the wrapped listener for as
// long as the listener should be in use. If the only reference to
// the listener is held by the WeakChangeListener, then it could
// be garbage collected prematurely.
private ChangeListener<Number> listener;
private WeakChangeListener<Number> weakListener;
@Override
public void start(Stage primaryStage) {
var label = new Label("Count: 0");
listener = (_, _, newVal) -> {
var text = "Count: %,d".formatted(newVal);
label.setText(text);
};
weakListener = new WeakChangeListener<>(listener);
count.addListener(weakListener);
var incrementBtn = new Button("Increment count");
incrementBtn.setOnAction(this::onIncrementCount);
var clearBtn = new Button("Clear listener");
clearBtn.setOnAction(this::onClearListener);
var toolbar = new ToolBar(incrementBtn, clearBtn);
var root = new BorderPane();
root.setTop(toolbar);
root.setCenter(label);
primaryStage.setScene(new Scene(root, 500, 300));
primaryStage.show();
}
private void onIncrementCount(ActionEvent event) {
event.consume();
count.set(count.get() + 1);
}
private void onClearListener(ActionEvent event) {
event.consume();
var source = (Node) event.getSource();
source.setDisable(true);
listener = null; // Remove all strong references to listener
// Call 'gc()' in a loop a certain number of times, or until the
// the listener has been garbage collected. There's nothing special
// about the number '20' here. It was chosen arbitrarily. Same with
// choosing to sleep for '100' milliseconds.
//
// Note this method is called on the JavaFX Application Thread, so
// the application will freeze for as long as this loop runs (up to
// 2 seconds in this case).
for (int i = 0; i < 20 && !weakListener.wasGarbageCollected(); i++) {
System.gc();
try {
Thread.sleep(100L);
} catch (InterruptedException ignore) {
}
}
// Notify the user of the result of trying to garbage collect the listener.
var alert = new Alert(AlertType.INFORMATION);
alert.initOwner(source.getScene().getWindow());
alert.setTitle("Clear Listener Result");
alert.setHeaderText(null);
if (weakListener.wasGarbageCollected()) {
alert.setContentText("Listener was garbage collected.");
} else {
alert.setContentText("Listener WAS NOT garbage collected!");
source.setDisable(false); // Allow retries
}
alert.show();
}
public static void main(String[] args) {
launch(Main.class);
}
}
Using Eclipse Temurin-23.0.1+11 (HotSpot VM, default garbage collector), the listener is garbage collected for me upon pressing the "Clear listener" button.
As an aside, note that System::runFinalization
has been deprecated for removal since Java 18. Also, in real code, it is extremely rare that you will ever need, or even want, to use System::gc
.
If you're creating a custom control and your skin inherits from SkinBase
, then within the skin I recommend using the registerXXXListener
methods instead of directly using weak listeners if able. That API uses weak listeners for you and will also remove them when the skin is disposed.
System
,gc()
is not guaranteed, andrunFinalization()
is deprecated. – trashgod Commented Mar 15 at 13:26