JDK-8161053 : Passing objects between JavaScript (JavaFX / WebKit) and Java causes a memory leak
  • Type: Bug
  • Component: javafx
  • Sub-Component: application-lifecycle
  • Affected Version: 8u92,9
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_7
  • CPU: x86_64
  • Submitted: 2016-07-07
  • Updated: 2016-09-14
  • Resolved: 2016-07-14
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 8 JDK 9
8u112Fixed 9Fixed
Related Reports
Duplicate :  
Duplicate :  
Relates :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.8.0_92"
Java(TM) SE Runtime Environment (build 1.8.0_92-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.92-b14, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 6.1.7601]

A DESCRIPTION OF THE PROBLEM :
We have a JavaFX application that passes some data between Java and JavaScript, we have noticed that this application's process memory was increasing and eventually crashing. After running a JVM memory profiler we couldn't identify the problem with Java, but we were able to pinpoint the problem to Webkit. It seems that there is a memory leak outside of JVM's memory space.

There are 2 cases: one is passing data from Java to JavaScript and the second is from JavaScript to Java. For this bug, we will focus on the case from Java to JavaScript.

We're passing the data as strings (JSON in our app) and it seems that overtime something isn't getting released.

The test has 2 payloads: a java string and a char array. The java string tests leaks, the char array isn't.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run MemoryLeak2.java.
Click on "Start Simulation" button
Watch the process' memory in the task manager.



EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The process' memory shouldn't increase.
ACTUAL -
The process' memory was increasing non stop.


REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
test.html
======

<!DOCTYPE HTML>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
<div style="position:relative">
	<h2>Controls</h2>
	<button onclick="simulateRealTimeUpdates()">Start simulation</button>
	<button onclick="stopRealtime()">Stop simulation</button>
	<button onclick="openNewWindow()">Open new window</button>
</div>

<script>
    function simulateRealTimeUpdates() {
        window.service.data({
            callback: function(dataStr) {
                dataStr = null;
            }
        });
    }

    function openNewWindow() {
        window.open('grid.html');
    }

    function stopRealtime() {
        window.service.cancelTimer();
    }
</script>
</body>
</html>

MemoryLeak2Service.java
=====================

package memoryLeak2;

import javafx.application.Platform;
import netscape.javascript.JSObject;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;

public class MemoryLeak2Service {
    private final Timer timer = new java.util.Timer();
    private final int size = 1000000;
    private final String strBuffer;
    //private final char[] chrBuffer;
    
    public MemoryLeak2Service()
    {
    	StringBuffer buffer = new StringBuffer(size);
    	for (int i = 0; i < size; i++){
    		buffer.append("A");
     	}    
    	
    	strBuffer = buffer.toString();
    	//chrBuffer = strBuffer.toCharArray();
    }
    
    public void data(JSObject cb) {
        TimerTask tt = new TimerTask() {
            public void run() {
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                    	// strBuffer - LEAKS, chrBuffer isn't leaking.
                    	cb.call("callback", strBuffer);
                    	//cb.call("callback", chrBuffer);
                    }
                });
            }
        };
        timer.schedule(tt, 1000, 100);
    }

    /**
     * Cancel timer
     */
    public void cancelTimer() {
        timer.cancel();
        timer.purge();

    }
}

MemoryLeak2.java
===============

package memoryLeak2;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.web.PopupFeatures;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebEvent;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import javafx.util.Callback;
import netscape.javascript.JSObject;

public class MemoryLeak2 extends Application {

    public static final int WIDTH = 1000;
    public static final int HEIGHT = 600;

    @Override
    public void start(Stage stage) {
        create(stage);
    }

    public static void create(Stage stage) {
        // create the scene
        stage.setTitle("Original Web View");
        Scene scene = new Scene(new Browser(), WIDTH, HEIGHT, Color.web("#666970"));
        stage.setScene(scene);
        stage.show();


        stage.addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, new EventHandler<WindowEvent>() {

            @Override
            public void handle(WindowEvent event) {
                //Exit the application on first window close
                System.exit(0);
            }

        });



    }

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

}

class Browser extends Region {

    /**
     * html file to load
     */
    private String fileToLoad = "test.html";
    final WebView browser = new WebView();
    final WebEngine webEngine = browser.getEngine();
    private MemoryLeak2Service myService;

    public Browser() {
        //apply the styles
        getStyleClass().add("browser");



        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {

            @Override
            public void changed(ObservableValue<? extends State> stateParam, State oldState, State newState) {

                switch (newState) {
                case READY:
                    break;
                case SCHEDULED:
                    break;

                case RUNNING:
                    break;

                case CANCELLED:
                    break;

                case SUCCEEDED:
                    if(!"about:blank".equals(webEngine.getLocation())) {
                        final JSObject window = (JSObject) webEngine.executeScript("window");
                        myService = new MemoryLeak2Service();
                        //set service as widow member
                        window.setMember("service", myService);
                        window.call("init");
                    }
                    break;

                default:
                    break;
                }
            }
        });

        //alert to display some javascript logs
        webEngine.setOnAlert(new EventHandler<WebEvent<String>>() {

            @Override
            public void handle(WebEvent<String> webEvent) {
                System.out.println(webEvent.getData());
            }

        });

        // load the web page
        webEngine.load(Browser.class.getResource(fileToLoad).toExternalForm());

        getChildren().add(browser);
        webEngine.setCreatePopupHandler(new Callback<PopupFeatures, WebEngine>() {
            @Override
            public WebEngine call(PopupFeatures param) {
                Stage stg = new Stage();
                stg.setTitle("Popup View");
                final Browser root = new Browser();
                Scene scene = new Scene(root, 800, 800, Color.web("#666970"));
                stg.setScene(scene);
                stg.show();
                stg.addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, new EventHandler<WindowEvent>() {

                    @Override
                    public void handle(WindowEvent event) {
                        if(root != null) {
                            //On close cancel te timer
                            root.getMyService().cancelTimer();
                            //load null to free some memory
                            root.getWebEngine().load(null);    
                        }
                    }

                });

                return root.getWebEngine();
            }
        });
    }

    public WebEngine getWebEngine() {
        return webEngine;
    }

    public MemoryLeak2Service getMyService() {
        return myService;
    }

    @Override protected void layoutChildren() {
        double w = getWidth();
        double h = getHeight();
        layoutInArea(browser,0,0,w,h,0, HPos.CENTER, VPos.CENTER);
    }

    @Override protected double computePrefWidth(double height) {
        return MemoryLeak2.HEIGHT;
    }

    @Override protected double computePrefHeight(double width) {
        return MemoryLeak2.WIDTH;
    }
}


---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Use char array instead:

cb.call("callback", chrBuffer);

This will not cause a memory leak.



Comments
Changeset: b24704ff8ba8 Author: mbilla Date: 2016-07-14 14:16 +0530 URL: http://hg.openjdk.java.net/openjfx/9-dev/rt/rev/b24704ff8ba8
14-07-2016

+1 Looks fine ..
14-07-2016

+1
14-07-2016

webrev: http://cr.openjdk.java.net/~mbilla/8161053/webrev.01/
14-07-2016

lgtm. Do we need to handle the same in WebPage.cpp Java_com_sun_webkit_WebPage_twkAddJavaScriptBinding method?
14-07-2016

Verified the fix with reporter and confirmed that after fix, leak is not observed.
13-07-2016

There is a leak while passing String from Java to JavaScript. When we call JSValueMakeString(), which inturn calls string->string(). string() method creates a new copy of string. modules/web/src/main/native/Source/JavaScriptCore/API/JSValueRef.cpp JSValueRef JSValueMakeString(JSContextRef ctx, JSStringRef string) { if (!ctx) { ASSERT_NOT_REACHED(); return 0; } ExecState* exec = toJS(ctx); APIEntryShim entryShim(exec); return toRef(exec, jsString(exec, string->string())); } modules/web/src/main/native/Source/JavaScriptCore/API/OpaqueJSString.cpp String OpaqueJSString::string() const { if (!this) return String(); // Return a copy of the wrapped string, because the caller may make it an Identifier. return m_string.isolatedCopy(); } As per the mentioned code in the issue, in MemoryLeak2Service(), large amount of "A" are appended to StringBuffer and passed to JavaScript. But this string is never deleted. In OpaqueJSString::~OpaqueJSString() method, m_characters and m_string both point to same memory address and the string buffer is stored in this location. Without Fix, we can see only "service" , "init" in m_characters . "A" are not appended to m_characters and never deleted. When "void fastFree(void* p)" is called, pointer p does not cotain "A". With Fix, verified that m_characters contains String Buffer passed from application and fastFree(void* p) is able to free the String Buffer.
13-07-2016

Most probably this issue is fixed as part of JDK-8089681. Anyway I will test the current issue and will confirm.
11-07-2016