FULL PRODUCT VERSION :
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
ADDITIONAL OS VERSION INFORMATION :
Windows 7x64
Mac OS X 10.11.6
A DESCRIPTION OF THE PROBLEM :
If you access the document and nodes of a javafx.scene.web.WebEngine from Java, the SelfDisposer of com.sun.webkit.dom.NodeImpl does not get called. Thus, the underlying JNI-Object is not disposed and the memory used by the native code increases but never decreases with each new document loaded.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Create a new Swing applikation
2. Add a JFXPanel with a WebView to the applikation
3. Optional: set the history maximum size of the WebEngine to 0
4. Load new html document into the WebEngine
5. Access the document and its nodes
6. Repeat no. 4 and no. 5 at leisure
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Memory-Usage of the JVM stays more or less the same.
Amount of NodeImpl#SelfDisposer in heap stays more or less the same
ACTUAL -
Memory-Usage of the JVM increases steadily
Heap-Usage stays more or less the same
Amount of NodeImpl#SelfDisposer in heap successively increases
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.html.HTMLElement;
import org.w3c.dom.html.HTMLInputElement;
import org.w3c.dom.html.HTMLSelectElement;
import org.w3c.dom.html.HTMLTextAreaElement;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
/**
 * Created by mathias on 09.03.17.
 */
public class NodeImplMemoryLeak {
    public static void main(String[] args) throws Exception {
        new NodeImplMemoryLeak();
    }
    private BorderPane borderPane;
    private WebView webView;
    public NodeImplMemoryLeak() throws Exception {
        final URL resource1 = NodeImplMemoryLeak.class.getResource("page1.html");
        final URL resource2 = NodeImplMemoryLeak.class.getResource("page2.html");
        final ArrayList<URL> resources = new ArrayList<>(Arrays.asList(resource1, resource2));
        new JFXPanel();
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        Platform.runLater(() -> {
            webView = new WebView();
            final WebEngine engine = webView.getEngine();
            webView.setContextMenuEnabled(false);
            webView.getEngine().getHistory().setMaxSize(0);
            webView.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
                @Override
                public void changed(final ObservableValue<? extends Worker.State> observable, final Worker.State oldValue, final Worker.State newValue) {
                    if (newValue == Worker.State.SUCCEEDED) {
                        contentEditable(engine.getDocument());
                    }
                }
            });
            final URL resource = resources.get(1);
            webView.getEngine().load(resource.toExternalForm());
            countDownLatch.countDown();
            borderPane = new BorderPane();
            borderPane.setCenter(webView);
        });
        countDownLatch.await();
        try {
            SwingUtilities.invokeAndWait(() -> {
                final JFXPanel jfxPanel = new JFXPanel();
                final Scene scene = new Scene(borderPane);
                jfxPanel.setScene(scene);
                final JButton button = new JButton(new AbstractAction("Toggle") {
                    @Override
                    public void actionPerformed(final ActionEvent e) {
                        final URL resource = resources.remove(0);
                        resources.add(resource);
                        Platform.runLater(() -> webView.getEngine().load(resource.toExternalForm()));
                    }
                });
                final JPanel content = new JPanel(new BorderLayout());
                content.add(jfxPanel, BorderLayout.CENTER);
                content.add(button, BorderLayout.SOUTH);
                final JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
                frame.setContentPane(content);
                frame.pack();
                frame.setVisible(true);
            });
        } catch (final InterruptedException | InvocationTargetException e) {
            throw new IllegalStateException(e);
        }
    }
    private static void contentEditable(final Document document) {
        if (document == null) {
            return;
        }
        final NodeList bodyList = document.getElementsByTagName("body");
        final Element element = (Element) bodyList.item(0);
        element.setAttribute("contenteditable", "false");
        checkElement(element);
    }
    private static void checkElement(final Element element) {
        final NodeList list = element.getChildNodes();
        for (int n = 0; n < list.getLength(); n++) {
            final Node child = list.item(n);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                final HTMLElement htmlElement = (HTMLElement) child;
                if (htmlElement instanceof HTMLInputElement) {
                    ((HTMLInputElement) htmlElement).setDisabled(true);
                } else if (htmlElement instanceof HTMLTextAreaElement) {
                    ((HTMLTextAreaElement) htmlElement).setReadOnly(true);
                } else if (htmlElement instanceof HTMLSelectElement) {
                    ((HTMLSelectElement) htmlElement).setDisabled(true);
                }
                checkElement(htmlElement);
            }
        }
    }
}
page1.html: (replace ... in img src)
<html dir="ltr"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body contenteditable="true"><h1><font face="Segoe UI" size="6">Test 2</font></h1><h2><font face="Segoe UI">Bild</font></h2><p><font face="Segoe UI"><br></font></p><p style="margin-top: 0"><img src="���"/></p></body></html>
page2.html: (replace ... in img src)
<html dir="ltr"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body contenteditable="true"><h1><font face="Segoe UI" size="6">Test 1</font></h1><h2><font face="Segoe UI" size="5">Input</font></h2><p style="margin-top: 0"><input type="text">
<input type="radio"></p><h2><font face="Segoe UI" size="5">Bild</font></h2><p style="margin-top: 0"><img src="..."/></p></body></html>
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Using the following workaround, the SelfDisposer gets called periodically.
The memory usage still increases but much slower.
import com.sun.webkit.Disposer;
import com.sun.webkit.dom.NodeImpl;
import javafx.application.Platform;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NonNls;
import securiton.uls.utilities.lang.concurrent.NamedExecutorServiceFactory;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
 * Ruf, wenn initialisiert, zyklisch die SelfDisposer von {@link NodeImpl} auf, da JavaFX dies nicht selbst tut.
 * Ansonsten w��rde der Speicher, der von WebKit im nativen Teil (JNI) reserviert wurde, nicht freigegeben.
 * <p/>
 * Siehe UMSRELEASE-6045
 * <p/>
 * Created by mathias on 09.03.17.
 */
public final class NodeDisposalWorkaround {
    @NonNls
    private static final Log log = LogFactory.getLog(NodeDisposalWorkaround.class);
    private NodeDisposalWorkaround() {
    }
    private static final class Worker {
        private static final Worker INSTANCE = new Worker();
        private Worker() {
            final ScheduledExecutorService executor =
                    NamedExecutorServiceFactory.createForDaemon().getSingleThreadScheduledExecutor(Worker.class);
            executor.scheduleWithFixedDelay(() -> Platform.runLater(this::disposeAllCollected),
                    1, 1, TimeUnit.SECONDS);
        }
        private void disposeAllCollected() {
            try {
                final Field hashTableField = NodeImpl.class.getDeclaredField("hashTable");
                hashTableField.setAccessible(true);
                final Object hashTable = hashTableField.get(null);
                for (int i = 0; i < Array.getLength(hashTable); i++) {
                    final Disposer.WeakDisposerRecord selfDisposer = (Disposer.WeakDisposerRecord) Array.get(hashTable, i);
                    dispose(selfDisposer);
                }
            } catch (final Exception e) {
                log.warn("Error doing dispose", e);
            }
        }
        private void dispose(final Disposer.WeakDisposerRecord selfDisposer) throws Exception {
            if (selfDisposer == null) {
                return;
            }
            final Field nextField = selfDisposer.getClass().getDeclaredField("next");
            nextField.setAccessible(true);
            final Disposer.WeakDisposerRecord nextDisposer = (Disposer.WeakDisposerRecord) nextField.get(selfDisposer);
            if (selfDisposer.get() == null) {
                selfDisposer.dispose();
            }
            dispose(nextDisposer);
        }
        public static Worker create() {
            return INSTANCE;
        }
    }
    public static void disposeAllCollected() {
        Worker.create();
    }
}