3

A long standing issue (some call it - arguably - feature :) is the weakness of all listeners installed by all fx-bindings. As a consequence, we can't build "chains" of properties without keeping a strong reference to each link of the chain.

A particular type of such a chain link is a JavaBeanProperty: its purpose is to adapt a javabean property to a fx-property. Typically, nobody is interested in the adapter as such, so its usage would do something like

private Parent createContentBean() {
    ...
    // local ref only
    Property property = createJavaBeanProperty();
    Bindings.bindBidirectional(label.textProperty(), property, NumberFormat.getInstance());

.. wondering why the label isn't updated. Changing property to a strong reference will work as expected (leaving me puzzeld as to who is responsible to feed the dummy, but that's another question):

Property property;
private Parent createContentBean() {
    ...
    // instantiate the field
    property = createJavaBeanProperty();
    Bindings.bindBidirectional(label.textProperty(), property, NumberFormat.getInstance());

Long intro, but nearly there: jdk8 somehow changed the implementation so that the first approach is now working, there's no longer any need to keep a strong reference to a JavaBeanProperty. On the other hand, custom implementations of "chain links" still need a strong reference.

Questions:

  • is the change of behaviour intentional and if so, why?
  • how is it achieved? The code looks very similar ... and I would love to try something similar in custom adapters

A complete example to play with:

public class BeanAdapterExample extends Application {

    private Counter counter;

    public BeanAdapterExample() {
        this.counter = new Counter();
    }

    Property property;
    private Parent createContentBean() {
        VBox content = new VBox();
        Label label = new Label();
        // strong ref
        property = createJavaBeanProperty();
        // local property
        Property property = createJavaBeanProperty();
        Bindings.bindBidirectional(label.textProperty(), property, NumberFormat.getInstance());
        Slider slider = new Slider();
        slider.valueProperty().bindBidirectional(property);
        Button button = new Button("increase");
        button.setOnAction(new EventHandler<ActionEvent>() {

            @Override
            public void handle(ActionEvent paramT) {
                counter.increase();
            }

        });
        content.getChildren().add(label);
        content.getChildren().add(slider);
        content.getChildren().add(button);
        return content;
    }

    protected JavaBeanDoubleProperty createJavaBeanProperty(){
        try {
            return JavaBeanDoublePropertyBuilder.create()
                    .bean(counter).name("count").build();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void start(Stage stage) throws Exception {
        Scene scene = new Scene(createContentBean());
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        Application.launch(args);
    }


    public static class Counter {

        private double count;

        public Counter() {
            this(0);
        }

        public Counter(double count) {
            this.count = count;
        }

        /**
         * Increases the counter by 1.
         */
        public void increase() {
            setCount(getCount()+ 1.);
        }

        /**
         * @return the count
         */
        public double getCount() {
            return count;
        }

        /**
         * @param count the count to set
         */
        public void setCount(double count) {
            double old = getCount();
            this.count = count;
            firePropertyChange("count", old, getCount());
        }

        PropertyChangeSupport support = new PropertyChangeSupport(this);

        public void addPropertyChangeListener(PropertyChangeListener l) {
            support.addPropertyChangeListener(l);
        }

        public void removePropertyChangeListener(PropertyChangeListener l) {
            support.removePropertyChangeListener(l);
        }

        protected void firePropertyChange(String name, Object oldValue,
                Object newValue) {
            support.firePropertyChange(name, oldValue, newValue);
        }

    }

}

BTW: added the Swing tag because adapting core beans will be a frequent task in migration

Community
  • 1
  • 1
kleopatra
  • 51,061
  • 28
  • 99
  • 211

2 Answers2

2

Reminds me on an issue I've stumbled across last year - a binding does not create a strong reference so the property will be garbage collected if the property is a method local field.

wzberger
  • 923
  • 6
  • 15
  • Interesting, does indeed look like the same basic reason. So we have many variants how it can bubble up as unexpected behaviour in an application. Thanks for link. – kleopatra Feb 04 '14 at 19:04
1

Gropingly trying to answer part of my own answer:

  • tentative guess: it's not intentional. Looks like now the JavaBeanProperty is never garbage collected, which couldn't have been the requirement.
  • the only difference I could find is a phantomReference (Cleaner in the snippet) to the property, created in its constructor: that seems to keep it strong enough to never (?) be released. If I mimic that in custom properties, they "work" in a chain but are not garbage collected as well. Not an option, IMO.

The jdk8 constructor of the property:

JavaBeanDoubleProperty(PropertyDescriptor descriptor, Object bean) {
    this.descriptor = descriptor;
    this.listener = descriptor.new Listener<Number>(bean, this);
    descriptor.addListener(listener);
    Cleaner.create(this, new Runnable() {
        @Override
        public void run() {
            JavaBeanDoubleProperty.this.descriptor.removeListener(listener);
        }
    });
}

The other way round: if I add such a reference to an arbitrary custom property, then it's stuck in memory just the same way as the javabeanProperty:

protected SimpleDoubleProperty createPhantomedProperty(final boolean phantomed) {
    SimpleDoubleProperty adapter = new SimpleDoubleProperty(){
        {
            // prevents the property from being garbage collected
            // must be done here in the constructor
            // otherwise reclaimed immediately
            if (phantomed) {
                Cleaner.create(this, new Runnable() {
                    @Override
                    public void run() {
                        // empty, could do what here?
                        LOG.info("runnable in cleaner");
                    }
                });
            }
        }

    };

    return adapter;
}

To reproduce the non-collection, add the code snippet below to my example code in the question, run in jdk7/8 and monitor with your favourite tool (used VisualVM): while running, click the "create" to create 100k of free-flying JavaBeanProperties. In jdk7, they never even show up in the memory sampler. In jdk8, they are created (sloooowly! so you might reduce the number) and build up. Forced garbage collection has no effect, even after nulling the underlying bean they are bound to.

Button create100K = new Button("create 100k properties");
create100K.setOnAction(new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent paramT) {
        Property propertyFX;
        /// can't measure any effect
        for (int i = 0; i < 100000; i++) {
            propertyFX = createCountProperty();
        }
        LOG.info("created 100k adapters");
    }

});
Button releaseCounter = new Button("release counter");
releaseCounter.setOnAction(new EventHandler<ActionEvent>() {

    @Override
    public void handle(ActionEvent paramT) {
        counter = null;
    }

});

Just FYI: created an issue for the potential memory leak - which is already marked as fixed, that was quick! Unfortunately, the fix-version is 8u20, not sure what to do until then. The only thingy coming to my mind is to c&p all JavaBeanXXProperty/Builders and add the fix. At the price of heavy warnings and unavailability in security-restricted environments. Also, we are back to the jdk7 behaviour (would have been too lucky, eating the cake and still have it :-)

kleopatra
  • 51,061
  • 28
  • 99
  • 211