diff a/modules/javafx.base/src/main/java/com/sun/javafx/binding/ExpressionHelper.java b/modules/javafx.base/src/main/java/com/sun/javafx/binding/ExpressionHelper.java
--- a/modules/javafx.base/src/main/java/com/sun/javafx/binding/ExpressionHelper.java
+++ b/modules/javafx.base/src/main/java/com/sun/javafx/binding/ExpressionHelper.java
@@ -23,16 +23,18 @@
  * questions.
  */
 
 package com.sun.javafx.binding;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
 import javafx.beans.InvalidationListener;
 import javafx.beans.value.ChangeListener;
 import javafx.beans.value.ObservableValue;
 
-import java.util.Arrays;
-
 /**
  * A convenience class for creating implementations of {@link javafx.beans.value.ObservableValue}.
  * It contains all of the infrastructure support for value invalidation- and
  * change event notification.
  *
@@ -186,188 +188,136 @@
         }
     }
 
     private static class Generic<T> extends ExpressionHelper<T> {
 
-        private InvalidationListener[] invalidationListeners;
-        private ChangeListener<? super T>[] changeListeners;
-        private int invalidationSize;
-        private int changeSize;
-        private boolean locked;
+        private Map<InvalidationListener, Integer> invalidationListeners = new LinkedHashMap<>();
+        private Map<ChangeListener<? super T>, Integer> changeListeners = new LinkedHashMap<>();
         private T currentValue;
+        private int weakChangeListenerGcCount = 2;
+        private int weakInvalidationListenerGcCount = 2;
 
         private Generic(ObservableValue<T> observable, InvalidationListener listener0, InvalidationListener listener1) {
             super(observable);
-            this.invalidationListeners = new InvalidationListener[] {listener0, listener1};
-            this.invalidationSize = 2;
+            this.invalidationListeners.put(listener0, 1);
+            // use merge here in case listener1 == listener0
+            this.invalidationListeners.merge(listener1, 1, Integer::sum);
         }
 
         private Generic(ObservableValue<T> observable, ChangeListener<? super T> listener0, ChangeListener<? super T> listener1) {
             super(observable);
-            this.changeListeners = new ChangeListener[] {listener0, listener1};
-            this.changeSize = 2;
+            this.changeListeners.put(listener0, 1);
+            // use merge here in case listener1 == listener0
+            this.changeListeners.merge(listener1, 1, Integer::sum);
             this.currentValue = observable.getValue();
         }
 
         private Generic(ObservableValue<T> observable, InvalidationListener invalidationListener, ChangeListener<? super T> changeListener) {
             super(observable);
-            this.invalidationListeners = new InvalidationListener[] {invalidationListener};
-            this.invalidationSize = 1;
-            this.changeListeners = new ChangeListener[] {changeListener};
-            this.changeSize = 1;
+            this.invalidationListeners.put(invalidationListener, 1);
+            this.changeListeners.put(changeListener, 1);
             this.currentValue = observable.getValue();
         }
 
         @Override
         protected Generic<T> addListener(InvalidationListener listener) {
-            if (invalidationListeners == null) {
-                invalidationListeners = new InvalidationListener[] {listener};
-                invalidationSize = 1;
-            } else {
-                final int oldCapacity = invalidationListeners.length;
-                if (locked) {
-                    final int newCapacity = (invalidationSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1;
-                    invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity);
-                } else if (invalidationSize == oldCapacity) {
-                    invalidationSize = trim(invalidationSize, invalidationListeners);
-                    if (invalidationSize == oldCapacity) {
-                        final int newCapacity = (oldCapacity * 3)/2 + 1;
-                        invalidationListeners = Arrays.copyOf(invalidationListeners, newCapacity);
-                    }
+            if (invalidationListeners.size() == weakInvalidationListenerGcCount) {
+                removeWeakListeners(invalidationListeners);
+                if (invalidationListeners.size() == weakInvalidationListenerGcCount) {
+                    weakInvalidationListenerGcCount = (weakInvalidationListenerGcCount * 3)/2 + 1;
                 }
-                invalidationListeners[invalidationSize++] = listener;
             }
+            invalidationListeners.merge(listener, 1, Integer::sum);
             return this;
         }
 
         @Override
         protected ExpressionHelper<T> removeListener(InvalidationListener listener) {
-            if (invalidationListeners != null) {
-                for (int index = 0; index < invalidationSize; index++) {
-                    if (listener.equals(invalidationListeners[index])) {
-                        if (invalidationSize == 1) {
-                            if (changeSize == 1) {
-                                return new SingleChange<T>(observable, changeListeners[0]);
-                            }
-                            invalidationListeners = null;
-                            invalidationSize = 0;
-                        } else if ((invalidationSize == 2) && (changeSize == 0)) {
-                            return new SingleInvalidation<T>(observable, invalidationListeners[1-index]);
-                        } else {
-                            final int numMoved = invalidationSize - index - 1;
-                            final InvalidationListener[] oldListeners = invalidationListeners;
-                            if (locked) {
-                                invalidationListeners = new InvalidationListener[invalidationListeners.length];
-                                System.arraycopy(oldListeners, 0, invalidationListeners, 0, index);
-                            }
-                            if (numMoved > 0) {
-                                System.arraycopy(oldListeners, index+1, invalidationListeners, index, numMoved);
-                            }
-                            invalidationSize--;
-                            if (!locked) {
-                                invalidationListeners[invalidationSize] = null; // Let gc do its work
-                            }
-                        }
-                        break;
+            if (invalidationListeners.containsKey(listener)) {
+                if (invalidationListeners.merge(listener, -1, Integer::sum) == 0) {
+                    invalidationListeners.remove(listener);
+                    if (invalidationListeners.isEmpty() && changeListeners.size() == 1) {
+                        return new SingleChange<T>(observable, changeListeners.keySet().iterator().next());
+                    } else if ((invalidationListeners.size() == 1) && changeListeners.isEmpty()) {
+                        return new SingleInvalidation<T>(observable, invalidationListeners.keySet().iterator().next());
                     }
                 }
             }
             return this;
         }
 
         @Override
         protected ExpressionHelper<T> addListener(ChangeListener<? super T> listener) {
-            if (changeListeners == null) {
-                changeListeners = new ChangeListener[] {listener};
-                changeSize = 1;
-            } else {
-                final int oldCapacity = changeListeners.length;
-                if (locked) {
-                    final int newCapacity = (changeSize < oldCapacity)? oldCapacity : (oldCapacity * 3)/2 + 1;
-                    changeListeners = Arrays.copyOf(changeListeners, newCapacity);
-                } else if (changeSize == oldCapacity) {
-                    changeSize = trim(changeSize, changeListeners);
-                    if (changeSize == oldCapacity) {
-                        final int newCapacity = (oldCapacity * 3)/2 + 1;
-                        changeListeners = Arrays.copyOf(changeListeners, newCapacity);
-                    }
+            if (changeListeners.size() == weakChangeListenerGcCount) {
+                removeWeakListeners(changeListeners);
+                if (changeListeners.size() == weakChangeListenerGcCount) {
+                    weakChangeListenerGcCount = (weakChangeListenerGcCount * 3)/2 + 1;
                 }
-                changeListeners[changeSize++] = listener;
             }
-            if (changeSize == 1) {
+            changeListeners.merge(listener, 1, Integer::sum);
+            if (changeListeners.size() == 1) {
                 currentValue = observable.getValue();
             }
             return this;
         }
 
         @Override
         protected ExpressionHelper<T> removeListener(ChangeListener<? super T> listener) {
-            if (changeListeners != null) {
-                for (int index = 0; index < changeSize; index++) {
-                    if (listener.equals(changeListeners[index])) {
-                        if (changeSize == 1) {
-                            if (invalidationSize == 1) {
-                                return new SingleInvalidation<T>(observable, invalidationListeners[0]);
-                            }
-                            changeListeners = null;
-                            changeSize = 0;
-                        } else if ((changeSize == 2) && (invalidationSize == 0)) {
-                            return new SingleChange<T>(observable, changeListeners[1-index]);
-                        } else {
-                            final int numMoved = changeSize - index - 1;
-                            final ChangeListener<? super T>[] oldListeners = changeListeners;
-                            if (locked) {
-                                changeListeners = new ChangeListener[changeListeners.length];
-                                System.arraycopy(oldListeners, 0, changeListeners, 0, index);
-                            }
-                            if (numMoved > 0) {
-                                System.arraycopy(oldListeners, index+1, changeListeners, index, numMoved);
-                            }
-                            changeSize--;
-                            if (!locked) {
-                                changeListeners[changeSize] = null; // Let gc do its work
-                            }
-                        }
-                        break;
+            if (changeListeners.containsKey(listener)) {
+                if (changeListeners.merge(listener, -1, Integer::sum) == 0) {
+                    changeListeners.remove(listener);
+                    if (changeListeners.isEmpty() && invalidationListeners.size() == 1) {
+                        return new SingleInvalidation<T>(observable, invalidationListeners.keySet().iterator().next());
+                    } else if ((changeListeners.size() == 1) && invalidationListeners.isEmpty()) {
+                        return new SingleChange<T>(observable, changeListeners.keySet().iterator().next());
                     }
                 }
             }
             return this;
         }
 
         @Override
         protected void fireValueChangedEvent() {
-            final InvalidationListener[] curInvalidationList = invalidationListeners;
-            final int curInvalidationSize = invalidationSize;
-            final ChangeListener<? super T>[] curChangeList = changeListeners;
-            final int curChangeSize = changeSize;
+            // Take a copy of listeners to ensure adding and removing listeners
+            // while the observers are being notified is safe
+            final Map<InvalidationListener, Integer> curInvalidationList = new LinkedHashMap<>(invalidationListeners);
+            final Map<ChangeListener<? super T>, Integer> curChangeList = new LinkedHashMap<>(changeListeners);
+
+            curInvalidationList.entrySet().forEach(entry -> fireInvalidationListeners(entry));
+
+            if (!curChangeList.isEmpty()) {
+                final T oldValue = currentValue;
+                currentValue = observable.getValue();
+                final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
+                if (changed) {
+                    curChangeList.entrySet().forEach(entry -> fireChangeListeners(oldValue, entry));
+                }
+            }
+        }
 
+        private void fireInvalidationListeners(Entry<InvalidationListener, Integer> entry) {
+            final InvalidationListener listener = entry.getKey();
+            final int registrationCount = entry.getValue();
             try {
-                locked = true;
-                for (int i = 0; i < curInvalidationSize; i++) {
-                    try {
-                        curInvalidationList[i].invalidated(observable);
-                    } catch (Exception e) {
-                        Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
-                    }
+                for (int i = 0; i < registrationCount; i++) {
+                    listener.invalidated(observable);
                 }
-                if (curChangeSize > 0) {
-                    final T oldValue = currentValue;
-                    currentValue = observable.getValue();
-                    final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
-                    if (changed) {
-                        for (int i = 0; i < curChangeSize; i++) {
-                            try {
-                                curChangeList[i].changed(observable, oldValue, currentValue);
-                            } catch (Exception e) {
-                                Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
-                            }
-                        }
-                    }
+            } catch (Exception e) {
+                Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(
+                    Thread.currentThread(), e);
+            }
+        }
+
+        private void fireChangeListeners(final T oldValue, Entry<ChangeListener<? super T>, Integer> entry) {
+            final ChangeListener<? super T> listener = entry.getKey();
+            final int registrationCount = entry.getValue();
+            try {
+                for (int i  = 0; i < registrationCount; i++) {
+                    listener.changed(observable, oldValue, currentValue);
                 }
-            } finally {
-                locked = false;
+            } catch (Exception e) {
+                Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(
+                    Thread.currentThread(), e);
             }
         }
     }
-
 }
