A quick post to warn about the listeners in JavaFX. They are very useful but they can harm your application if used in a wrong way.
I recently profiled my application and noticed that it would not last more than 30 minutes in stressful conditions! Intrigued, I investigated a bit and saw that most of the memory was used by InvalidationListener or ChangeListener.
In my application, I have a SimpleObjectProperty
holding a bundle for i18n and any objects can listen to its modification in order to update their labels to the new locale. So basically, I used to have this code :
Menu item = new Menu(controller.getStringBundle(I18nLabels.VALIDATIONMODE.getBundleKey())); controller.getI18nManager().bundleProperty().addListener((Observable o) -> item.setText(controller.getStringBundle(I18nLabels.VALIDATIONMODE.getBundleKey())));
But the issue was that I was constantly re-creating this MenuItem and it was always adding a new Listener to the bundle property. Therefore, the listeners were never garbage collected thus creating a leak.
So I have two advice for this situation. If possible, create and reference a WeakListener. This is possible when you have a real class where you can hold the listeners. If you can’t hold them, they will be garbage collected too soon. It should look like that for example:
private WeakChangeListener weakListener; private ChangeListener<TreeItem<TreeViewElement>> listener; private void myMethod(){ listener = new ChangeListener<TreeItem<TreeViewElement>>() { @Override public void changed(ObservableValue<? extends TreeItem<TreeViewElement>> ov, TreeItem<TreeViewElement> oldItem, TreeItem<TreeViewElement> newItem) { //Do something } }; weakListener = new WeakChangeListener(listener); p.getTreeTableView().getSelectionModel().selectedItemProperty().addListener(weakListener); }
But sometimes, you can’t hold them in a variable. That’s the case for my MenuItem above I was recreating.
Basically, I was simply adding a listener for changing the label. And I was doing that in a lot of places. I decided to create only one listener inside my I18n class, that will change the labels of all the MenuItem. So instead of creating a listener, I simply give to the i18n class the MenuItem itself along with the bundle key.
The trick was to use a WeakHashMap, thus the keys are hold by a WeakReference. In that manner, I could create as many MenuItem as I wanted, when they will only be referenced from the WeakHashMap they will be garbage collected :
private Map<MenuItem, String> bundleListenerMap = new WeakHashMap<>(); public I18nManager(String language) { bundle.addListener(new ChangeListener<ResourceBundle>() { @Override public void changed(ObservableValue<? extends ResourceBundle> ov, ResourceBundle t, ResourceBundle newBundle) { //Changing all the menuItems labels. for (Entry<MenuItem, String> entry : bundleListenerMap.entrySet()) { if (entry.getKey() != null) { entry.getKey().setText(newBundle.getString(entry.getValue())); } } } }); } @Override public void addBundleListener(MenuItem object, String bundleKey) { bundleListenerMap.put(object, bundleKey); }
I was worried that the values will then stay in the WeakHashMap. But it seems that the class has a expungeStaleEntries()
that clear values that have null key on a regular basis.
If you have any comment, any tips, feel free to comment.
Cheers,
Good explanation. Thanks.