When the listener is listening too much

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,

One thought on “When the listener is listening too much”

Leave a Reply

Your email address will not be published. Required fields are marked *