-
Bug
-
Resolution: Fixed
-
P3
-
8, 9
-
x86_64
-
windows_7
Issue | Fix Version | Assignee | Priority | Status | Resolution | Resolved In Build |
---|---|---|---|---|---|---|
JDK-8183092 | 9.0.4 | Murali Billa | P3 | Resolved | Fixed | |
JDK-8184410 | 9.0.1 | Murali Billa | P3 | Resolved | Fixed | b02 |
JDK-8179205 | 8u152 | Murali Billa | P3 | Resolved | Fixed | b04 |
JDK-8184399 | 8u151 | Murali Billa | P3 | Resolved | Fixed | b05 |
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();
}
}
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();
}
}
- backported by
-
JDK-8179205 com.sun.webkit.dom.NodeImpl#SelfDisposer is not called
-
- Resolved
-
-
JDK-8183092 com.sun.webkit.dom.NodeImpl#SelfDisposer is not called
-
- Resolved
-
-
JDK-8184399 com.sun.webkit.dom.NodeImpl#SelfDisposer is not called
-
- Resolved
-
-
JDK-8184410 com.sun.webkit.dom.NodeImpl#SelfDisposer is not called
-
- Resolved
-
- relates to
-
JDK-8170938 Memory leak in JavaFX WebView
-
- Resolved
-