Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8176729

com.sun.webkit.dom.NodeImpl#SelfDisposer is not called

XMLWordPrintable

    • web
    • x86_64
    • windows_7

        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();
            }
        }


              mbilla Murali Billa
              webbuggrp Webbug Group
              Votes:
              0 Vote for this issue
              Watchers:
              6 Start watching this issue

                Created:
                Updated:
                Resolved: