JDK-8198540 : Dynalink leaks memory when generating type converters
  • Type: Bug
  • Component: core-libs
  • Sub-Component: jdk.nashorn
  • Affected Version: 8,9,10
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: generic
  • CPU: x86_64
  • Submitted: 2018-02-21
  • Updated: 2024-12-09
  • Resolved: 2021-02-09
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 13 JDK 16 JDK 17
13.0.7Fixed 16.0.1Fixed 17 b09Fixed
Related Reports
Duplicate :  
Relates :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "9.0.4"
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 6.1.7601]

A DESCRIPTION OF THE PROBLEM :
We're using Nashorn in OSGi environment, where each bundle has own class-loader and on update the bundle gets a new class-loader. We noticed that memory leak occurred when updating bundles which generate interface implementations from script objects, using javax.scripting.Invocable.getInterface methods.

A test application for reproducing the leak is provided. It consist of 3 files:
  - TestService.java - a simple interface with a single method - "callMe". This interface is implemented by the test script
  - CustomClassLoader.java - custom class loader, which loads the TestService class
  - Test.java - the main test class. It start a thread that repeatedly evaluates a test script - object with single "callMe" function - and generates interface implementation from the resulting object. On each execution the TestService class is loaded anew and new ScriptEngine is used. Periodically the used heap memory is printed.

On java version "9.0.4" we observe constant increase in used heap memory. 

The leak is even more severe on java version "1.8.0_121", where constant increase in loaded classes and heap memory is observed.

On java 1.7, with Rhino engine, the leaks is not observed.

REGRESSION.  Last worked in version 7u79

ADDITIONAL REGRESSION INFORMATION: 
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Compile the provided java files - TestService.java, CustomClassLoader.java and Test.java - and run Test class.



EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Used heap remains in some boundaries.
ACTUAL -
Used heap increases constantly.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
---------- BEGIN TestService.java ----------

public interface TestService {

  public int callMe(int num);
}

---------- END TestService.java ----------

---------- BEGIN CustomClassLoader.java ----------

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class CustomClassLoader extends ClassLoader {

  public static final String THE_CLASS = "the_class";

  public CustomClassLoader(ClassLoader parent) {
    super(parent);
  }

  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    if (THE_CLASS.equals(name)) {
      try {
        byte[] data = loadClassData("TestService.class");

        return defineClass("TestService", data, 0, data.length);
      } catch (IOException ex) {
        ex.printStackTrace();
        return null;
      }
    }

    return super.loadClass(name);
  }

  private byte[] loadClassData(String name) throws IOException {
    InputStream in = getClass().getResourceAsStream(name);

    try {
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      byte[] buff = new byte[1024];

      int read = 0;
      while ((read = in.read(buff)) > 0) {
        out.write(buff, 0, read);
      }

      return out.toByteArray();
    } finally {
      in.close();
    }

  }

}

---------- END CustomClassLoader.java ----------

---------- BEGIN Test .java ----------

import java.lang.reflect.Method;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class Test {

  private static final String SCRIPT      = "var impl = { \r\n" +
      "  callMe : function(number) {\r\n" +
      "    return number + 1;\r\n" +
      "  }\r\n" +
      "}\r\n" +
      "  \r\n" +
      "impl";

  private static final long   DUMP_PERIOD = 1000 * 2;
  private static final int    MB          = 1024 * 1024;

  public static void main(String[] args) throws Exception {
    System.out.format("Java Version: %s %n", System.getProperty("java.version"));

    long mark = 0;
    int counter = 0;

    while (true) {
      test(counter);

      if (System.currentTimeMillis() - mark > DUMP_PERIOD) {
        System.gc();
        long used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();

        System.out.format("%.2f MB%n", (float) used / MB);

        mark = System.currentTimeMillis();
      }

      Thread.sleep(50);

      counter++;
    }
  }

  public static void test(int index) throws Exception {
    ClassLoader cl = new CustomClassLoader(Test.class.getClassLoader());

    Class<?> clazz = cl.loadClass(CustomClassLoader.THE_CLASS);
    Method method = clazz.getMethod("callMe", int.class);

    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine engine = manager.getEngineByExtension("js");
    Invocable invocable = (Invocable) engine;

    Object obj = engine.eval(SCRIPT);

    Object service = invocable.getInterface(obj, clazz);

    int result = (Integer) method.invoke(service, index);

    if (result != index + 1) {
      System.err.format("Wrong result: expected %d, but was %d %n", index + 1, result);
    }
  }
}


---------- END Test .java ----------

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


Comments
Seems like the backport request for 11u went stale for some reason. I also don't see a Github PR. I'm removing the fix request label for now. Feel free to open a Github PR and request this again.
25-06-2021

Hi [~attila], just came back to this 11u fix request. Can you pls. also request JDK-8261483 for 11u and post an RFR if necessary? We should commit the test fix as a different patch but together with this one.
05-04-2021

[~clanger] thank you for the instructions and for the description of expectations! I agree it was reasonable to wait for the resolution to JDK-8261745 (in fact, JDK-8261483 of which the other one was a duplicate.) This is now done and I added a Fix Request comment as well.
05-03-2021

Fix Request ---------- As the issue itself describes, it is very easy for Nashorn execution context instances to remain anchored to GC roots through ClassValues. This makes Nashorn (and any other dynamic language using Dynalink type conversion mechanisms) to cause memory leaks when execution contexts are frequently recreated – the original poster of the issue reported this happening in OSGi containers with dynamic code reloading, but it can happen under different circumstances. The fix comes with its own set of regression tests: test/jdk/jdk/dynalink/TypeConverterFactoryMemoryLeakTest.java and test/jdk/jdk/dynalink/TypeConverterFactoryRetentionTests.java. These tests ensure that no longer used objects are, in fact, reclaimed by GC as expected, thus proving the memory leaks are gone. Those tests failed before the fix, and pass after the fix. I can see no associated risk in fixing this issue. Somewhat ridiculously, there is a risk in committing the tests, which were originally timing-based and could thus fail when starved of CPU on CI boxes with heavy CPU usage. This was fixed with JDK-8261483. If it is acceptable – since this issue will need public review anyway – I could roll JDK-8261483 fixes into the backport of this issue thus ensuring the tests don't have even the smallest chance of causing intermittent failure.
05-03-2021

[~attila] can you please add a Fix request comment for 11u, stating whether the patch applies cleanly or not, which regression tests you ran and how you'd rate the risk. Thanks! Other than that I'm inclined to wait for resolution of JDK-8261745 before approving...
16-02-2021

Changeset: 8f4c15f6 Author: Attila Szegedi <attila@openjdk.org> Date: 2021-02-09 16:06:49 +0000 URL: https://git.openjdk.java.net/jdk/commit/8f4c15f6
09-02-2021

This is actually a memory leak in Dynalink's TypeConverterFactory. It uses instance-specific ClassValue anonymous inner classes that use ClassMap anonymous inner classes. This results in strong reference from ClassValue$Entry in the Class.classValueMap to the TypeConverterFactory object that holds the ClassValue. Care must be taken when ClassValue objects are instance-specific to not have them retain a reference to the outer instance. After refactoring TypeConverterFactory's ClassValue-s the supplied reproducer stabilizes at 5.93MB; without it the memory usage grows without bounds.
30-12-2020

Nashorn is deprecated.
15-01-2019

There is no reclaim of memory, it is evident from the output logs. 8 GA - Fail 8u172 ea b06 - Fail 9.0.4 GA - Fail 10 ea b42 - Fail Below are the snapshot executed on 10 ea b42 == -sh-4.2$ /scratch/fairoz/JAVA/jdk10/jdk-10-ea+42/bin/java Test Java Version: 10-ea 2.96 MB 3.07 MB 3.14 MB 3.22 MB 3.30 MB 3.41 MB 3.46 MB 3.61 MB 3.68 MB 3.74 MB 3.81 MB 3.89 MB 3.95 MB 4.03 MB 4.11 MB 4.18 MB 4.25 MB 4.32 MB 4.39 MB 4.45 MB 4.52 MB 4.60 MB 4.68 MB 4.75 MB 4.83 MB 4.91 MB 4.98 MB 5.05 MB 5.12 MB 5.20 MB 5.27 MB 5.34 MB 5.41 MB 5.48 MB 5.55 MB 5.60 MB 5.67 MB 5.74 MB 5.81 MB == Issue exist from the integration of Nashorn engine, there is no memory leak in Rhino
22-02-2018