Java: Model-View-Controller without memory leaks

When doing MVC programming in Java, there is a problem that most people don't know about. I've ignored it myself much too long. The problem is that when you bind a model class to an UI component you will get a giant memory leak.

What happens?

Well, imagine a model class supporting listening for property changes. A simple example might look like this (I extend from PropertyChangeSupport here so that I don't have to delegate all the methods, normally you wouldn't do so, of course):

  1. public class Model extends PropertyChangeSupport {
  2. private String name;
  3. public Model(String name) {
  4. super(new Object()); // Only an example! Don't do this in RL!
  5. this.name = name;
  6. }
  7.  
  8. public String getName() {
  9. return name;
  10. }
  11.  
  12. public void setName(String name) {
  13. firePropertyChange("name", this.name, this.name = name);
  14. }
  15. }

Next, imagine a view for this model class. It has an input field that is binded in both direction with the model (very simplified example here, I normally don't code this way ;-)):

  1. public class ModelView extends JPanel {
  2. public ModelView(final Model model) {
  3. super(new BorderLayout());
  4. add(new JLabel("Name: "), BorderLayout.WEST);
  5.  
  6. final JTextField textfield = new JTextField(model.getName());
  7. add(textfield, BorderLayout.CENTER);
  8.  
  9. // Bind textfield => model (harmless)
  10. textfield.addActionListener(new ActionListener() {
  11. public void actionPerformed(ActionEvent e) {
  12. model.setName(textfield.getText());
  13. }
  14. });
  15.  
  16. // Bind model => textfield (introduces memory leak)
  17. model.addPropertyChangeListener("name", new PropertyChangeListener() {
  18. public void propertyChange(PropertyChangeEvent e) {
  19. textfield.setText(model.getName());
  20. }
  21. });
  22. }
  23. }

From now on we have a memory leak, because the JPanel, the JLabel and the JTextField (and all other referenced) objects will never be removed by the garbage collector as long as the model exists (which might be until the applications end of life). The reason is that the anonymous inner PropertyChangeListener instance has (and must have!) an implicit reference to the JPanel. And even if you replace it by a static class it does have to know the textfield which is a child object of the panel and has a reference back to it. So even if nobody else references the panel the garbage collector sees:

  Model => PropertyChangeListener => JPanel

There is a little known class called WeakReference (and his brother WeakHashMap). It contains an object, but the object could still be removed by the garbage collector as long as there is no other (non-weak) reference to it.

A naive idea is to just encapsulate the PropertyChangeListener within a WeakReference object:

  1. public class WeakListener implements PropertyChangeListener {
  2. private final WeakReference<PropertyChangeListener> listener;
  3. public WeakListener(PropertyChangeListener listener) {
  4. this.listener = new WeakReference<PropertyChangeListener>(listener);
  5. }
  6.  
  7. public void propertyChange(PropertyChangeEvent e) {
  8. PropertyChangeListener l = this.listener.get();
  9. if (l != null) {
  10. l.propertyChange(e);
  11. }
  12. }
  13. }

Code in ModelView constructor:

// Bind model => textfield (with little memory leak)
model.addPropertyChangeListener("name", new WeakListener(new PropertyChangeListener() {
    public void propertyChange(PropertyChangeEvent e) {
        textfield.setText(model.getName());
    }
}));

The idea is that you still have this simple and small WeakListener object within your model's listener list, but the really big part (the original listener, the panel, label, textfield, etc) could be free'd. Sadly, this will not work, because as soon as you gave the anonymous PropertyChangeListener to WeakListener's constructor nobody else does reference to it, so the garbage collector will remove the object immediately.

The trick is to create a single reference to it which belongs the parent object:

public class ModelView extends JPanel {
    private PropertyChangeListener modelTextfieldListener;

    public ModelView(final Model model) {
        super(new BorderLayout());
        add(new JLabel("Name: "), BorderLayout.WEST);

        final JTextField textfield = new JTextField(model.getName());
        add(textfield, BorderLayout.CENTER);

        // Bind textfield => model (harmless)
        textfield.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                model.setName(textfield.getText());
            }
        });

        // Bind model => textfield (with little memory leak)
        model.addPropertyChangeListener("name", new WeakListener(modelTextfieldListener = new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent e) {
                textfield.setText(model.getName());
            }
        }));
    }
}

That's it. To make this easier I've created a little support class WeakListenerSupport that's very helpful if you have to deal with multiple input-property-bindings. Also it tries to remove ("unlink") itself from the model if the listener doesn't exist any longer. It even allows to unlink all weak listeners from a single object.

With this class the above example would look like:

public class ModelView extends JPanel {
    private final WeakListenerSupport wls = new WeakListenerSupport();

    public ModelView(final Model model) {
        super(new BorderLayout());
        add(new JLabel("Name: "), BorderLayout.WEST);

        final JTextField textfield = new JTextField(model.getName());
        add(textfield, BorderLayout.CENTER);

        // Bind textfield => model (harmless)
        textfield.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                model.setName(textfield.getText());
            }
        });

        // Bind model => textfield (without memory leak)
        model.addPropertyChangeListener("name", wls.propertyChange(new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent e) {
                textfield.setText(model.getName());
            }
        }, model));
    }
}

Comments

Großes Kino :) Wirklich elegante Lösung.

Add new comment