6788096: ImageIO SreamCloser causes memory leak in FX applets
Reviewed-by: igor, prr
--- a/jdk/src/share/classes/com/sun/imageio/stream/StreamCloser.java Tue Jan 13 18:38:44 2009 +0300
+++ b/jdk/src/share/classes/com/sun/imageio/stream/StreamCloser.java Thu Jan 15 13:55:30 2009 +0300
@@ -94,6 +94,10 @@
tgn != null;
tg = tgn, tgn = tg.getParent());
streamCloser = new Thread(tg, streamCloserRunnable);
+ /* Set context class loader to null in order to avoid
+ * keeping a strong reference to an application classloader.
+ */
+ streamCloser.setContextClassLoader(null);
Runtime.getRuntime().addShutdownHook(streamCloser);
return null;
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/imageio/stream/StreamCloserLeak/run_test.sh Thu Jan 15 13:55:30 2009 +0300
@@ -0,0 +1,205 @@
+#!/bin/ksh -p
+#
+# Copyright 2009 Sun Microsystems, Inc. All Rights Reserved.
+# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+#
+# This code is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 2 only, as
+# published by the Free Software Foundation.
+#
+# This code is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# version 2 for more details (a copy is included in the LICENSE file that
+# accompanied this code).
+#
+# You should have received a copy of the GNU General Public License version
+# 2 along with this work; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+# CA 95054 USA or visit www.sun.com if you need additional information or
+# have any questions.
+#
+
+#
+# @test
+# @bug 6788096
+# @summary Test simulates the case of multiple applets executed in
+# the same VM and verifies that ImageIO shutdown hook
+# StreamCloser does not cause a leak of classloaders.
+#
+# @build test.Main
+# @build testapp.Main
+# @run shell run_test.sh
+
+# There are several resources which need to be present before many
+# shell scripts can run. Following are examples of how to check for
+# many common ones.
+#
+# Note that the shell used is the Korn Shell, KSH
+#
+# Also note, it is recommended that make files NOT be used. Rather,
+# put the individual commands directly into this file. That way,
+# it is possible to use command line arguments and other shell tech-
+# niques to find the compiler, etc on different systems. For example,
+# a different path could be used depending on whether this were a
+# Solaris or Win32 machine, which is more difficult (if even possible)
+# in a make file.
+
+
+# Beginning of subroutines:
+status=1
+
+#Call this from anywhere to fail the test with an error message
+# usage: fail "reason why the test failed"
+fail()
+ { echo "The test failed :-("
+ echo "$*" 1>&2
+ echo "exit status was $status"
+ exit $status
+ } #end of fail()
+
+#Call this from anywhere to pass the test with a message
+# usage: pass "reason why the test passed if applicable"
+pass()
+ { echo "The test passed!!!"
+ echo "$*" 1>&2
+ exit 0
+ } #end of pass()
+
+# end of subroutines
+
+
+# The beginning of the script proper
+
+# Checking for proper OS
+OS=`uname -s`
+case "$OS" in
+ SunOS )
+ VAR="One value for Sun"
+ DEFAULT_JDK=/usr/local/java/jdk1.2/solaris
+ FILESEP="/"
+ PATHSEP=":"
+ TMP="/tmp"
+ ;;
+
+ Linux )
+ VAR="A different value for Linux"
+ DEFAULT_JDK=/usr/local/java/jdk1.4/linux-i386
+ FILESEP="/"
+ PATHSEP=":"
+ TMP="/tmp"
+ ;;
+
+ Windows_95 | Windows_98 | Windows_NT | Windows_ME )
+ VAR="A different value for Win32"
+ DEFAULT_JDK=/usr/local/java/jdk1.2/win32
+ FILESEP="\\"
+ PATHSEP=";"
+ TMP=`cd "${SystemRoot}/Temp"; echo ${PWD}`
+ ;;
+
+ # catch all other OSs
+ * )
+ echo "Unrecognized system! $OS"
+ fail "Unrecognized system! $OS"
+ ;;
+esac
+
+# Want this test to run standalone as well as in the harness, so do the
+# following to copy the test's directory into the harness's scratch directory
+# and set all appropriate variables:
+
+if [ -z "${TESTJAVA}" ] ; then
+ # TESTJAVA is not set, so the test is running stand-alone.
+ # TESTJAVA holds the path to the root directory of the build of the JDK
+ # to be tested. That is, any java files run explicitly in this shell
+ # should use TESTJAVA in the path to the java interpreter.
+ # So, we'll set this to the JDK spec'd on the command line. If none
+ # is given on the command line, tell the user that and use a cheesy
+ # default.
+ # THIS IS THE JDK BEING TESTED.
+ if [ -n "$1" ] ;
+ then TESTJAVA=$1
+ else echo "no JDK specified on command line so using default!"
+ TESTJAVA=$DEFAULT_JDK
+ fi
+ TESTSRC=.
+ TESTCLASSES=.
+ STANDALONE=1;
+fi
+echo "JDK under test is: $TESTJAVA"
+
+
+############### YOUR TEST CODE HERE!!!!!!! #############
+
+#All files required for the test should be in the same directory with
+# this file. If converting a standalone test to run with the harness,
+# as long as all files are in the same directory and it returns 0 for
+# pass, you should be able to cut and paste it into here and it will
+# run with the test harness.
+
+# This is an example of running something -- test
+# The stuff below catches the exit status of test then passes or fails
+# this shell test as appropriate ( 0 status is considered a pass here )
+
+echo "Create TestApp.jar..."
+
+if [ -f TestApp.jar ] ; then
+ rm -f TestApp.jar
+fi
+
+${TESTJAVA}/bin/jar -cvf TestApp.jar -C ${TESTCLASSES} testapp
+
+if [ $? -ne "0" ] ; then
+ fail "Failed to create TestApp.jar"
+fi
+
+echo "Create Test.jar..."
+if [ -f Test.jar ] ; then
+ rm -f Test.jar
+fi
+
+${TESTJAVA}/bin/jar -cvf Test.jar -C ${TESTCLASSES} test
+
+if [ $? -ne 0 ] ; then
+ fail "Failed to create Test.jar"
+fi
+
+# Prepare temp dir for cahce files
+mkdir ./tmp
+if [ $? -ne 0 ] ; then
+ fail "Unable to create temp directory."
+fi
+
+# Verify that all classoladers are destroyed
+${TESTJAVA}/bin/java -cp Test.jar test.Main
+if [ $? -ne 0 ] ; then
+ fail "Test FAILED: some classloaders weren't destroyed."
+fi
+
+
+# Verify that ImageIO shutdown hook works correcly
+${TESTJAVA}/bin/java -cp Test.jar -DforgetSomeStreams=true test.Main
+if [ $? -ne 0 ] ; then
+ fail "Test FAILED: some classloaders weren't destroyed of shutdown hook failed."
+fi
+
+# sanity check: verify that all cache files were deleted
+cache_files=`ls tmp`
+
+if [ "x${cache_files}" != "x" ] ; then
+ echo "WARNING: some cache files was not deleted: ${cache_files}"
+fi
+
+echo "Test done."
+
+status=$?
+
+if [ $status -eq "0" ] ; then
+ pass ""
+else
+ fail "Test failed due to test plugin was not found."
+fi
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/imageio/stream/StreamCloserLeak/test/Main.java Thu Jan 15 13:55:30 2009 +0300
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2009 Sun Microsystems, Inc. All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+package test;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import javax.imageio.stream.ImageInputStream;
+import sun.awt.AppContext;
+import sun.awt.SunToolkit;
+
+public class Main {
+
+ private static ThreadGroup appsThreadGroup;
+
+ private static WeakHashMap<MyClassLoader, String> refs =
+ new WeakHashMap<MyClassLoader, String>();
+
+ /** Collection to simulate forgrotten streams **/
+ private static HashMap<String, ImageInputStream> strongRefs =
+ new HashMap<String, ImageInputStream>();
+
+ private static ConcurrentLinkedQueue<Throwable> problems =
+ new ConcurrentLinkedQueue<Throwable>();
+
+ private static AppContext mainAppContext = null;
+
+ private static CountDownLatch doneSignal;
+
+ private static final int gcTimeout =
+ Integer.getInteger("gcTimeout", 10).intValue();
+
+ private static boolean forgetSomeStreams =
+ Boolean.getBoolean("forgetSomeStreams");
+
+ public static void main(String[] args) throws IOException {
+ mainAppContext = SunToolkit.createNewAppContext();
+ System.out.println("Current context class loader: " +
+ Thread.currentThread().getContextClassLoader());
+
+ appsThreadGroup = new ThreadGroup("MyAppsThreadGroup");
+
+ File jar = new File("TestApp.jar");
+ if (!jar.exists()) {
+ System.out.println(jar.getAbsolutePath() + " was not found!\n" +
+ "Please install the jar with test application correctly!");
+ throw new RuntimeException("Test failed: no TestApp.jar");
+ }
+
+ URL[] urls = new URL[]{jar.toURL()};
+
+ int numApps = Integer.getInteger("numApps", 20).intValue();
+
+ doneSignal = new CountDownLatch(numApps);
+ int cnt = 0;
+ while (cnt++ < numApps) {
+ launch(urls, "testapp.Main", "launch");
+
+ checkErrors();
+ }
+
+ System.out.println("Wait for apps completion....");
+
+ try {
+ doneSignal.await();
+ } catch (InterruptedException e) {
+ }
+
+ System.out.println("All apps finished.");
+
+ System.gc();
+
+ System.out.flush();
+
+ System.out.println("Enumerate strong refs:");
+ for (String is : strongRefs.keySet()) {
+ System.out.println("-> " + is);
+ }
+
+ System.out.println("=======================");
+
+ // wait few seconds
+ waitAndGC(gcTimeout);
+
+ doneSignal = new CountDownLatch(1);
+
+ Runnable workaround = new Runnable() {
+
+ public void run() {
+ AppContext ctx = null;
+ try {
+ ctx = SunToolkit.createNewAppContext();
+ } catch (Throwable e) {
+ // ignore...
+ } finally {
+ doneSignal.countDown();
+ }
+ }
+ };
+
+ Thread wt = new Thread(appsThreadGroup, workaround, "Workaround");
+ wt.setContextClassLoader(new MyClassLoader(urls, "workaround"));
+ wt.start();
+ wt = null;
+ workaround = null;
+
+ System.out.println("Wait for workaround completion...");
+
+ try {
+ doneSignal.await();
+ } catch (InterruptedException e) {
+ }
+
+ // give a chance to GC
+ waitAndGC(gcTimeout);
+
+ if (!refs.isEmpty()) {
+ System.out.println("Classloaders still alive:");
+
+ for (MyClassLoader l : refs.keySet()) {
+ String val = refs.get(l);
+
+ if (val == null) {
+ throw new RuntimeException("Test FAILED: Invalid classloader name");
+ }
+ System.out.println("->" + val + (strongRefs.get(val) != null ?
+ " (has strong ref)" : ""));
+ if (strongRefs.get(val) == null) {
+ throw new RuntimeException("Test FAILED: exta class loader is detected! ");
+ }
+ }
+ } else {
+ System.out.println("No alive class loaders!!");
+ }
+ System.out.println("Test PASSED.");
+ }
+
+ private static void waitAndGC(int sec) {
+ int cnt = sec;
+ System.out.print("Wait ");
+ while (cnt-- > 0) {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ }
+ // do GC every 3 seconds
+ if (cnt % 3 == 2) {
+ System.gc();
+ System.out.print("+");
+ } else {
+ System.out.print(".");
+ }
+ checkErrors();
+ }
+ System.out.println("");
+ }
+
+ private static void checkErrors() {
+ while (!problems.isEmpty()) {
+ Throwable theProblem = problems.poll();
+ System.out.println("Test FAILED!");
+ do {
+ theProblem.printStackTrace(System.out);
+ theProblem = theProblem.getCause();
+ } while (theProblem != null);
+ throw new RuntimeException("Test FAILED");
+ }
+ }
+ static int counter = 0;
+
+ private static void launch(URL[] urls, final String className,
+ final String methodName)
+ {
+ final String uniqClassName = "testapp/Uniq" + counter;
+ final boolean saveStrongRef = forgetSomeStreams ? (counter % 5 == 4) : false;
+
+ System.out.printf("%s: launch the app\n", uniqClassName);
+ Runnable launchIt = new Runnable() {
+ public void run() {
+ AppContext ctx = SunToolkit.createNewAppContext();
+
+ try {
+ Class appMain =
+ ctx.getContextClassLoader().loadClass(className);
+ Method launch = appMain.getDeclaredMethod(methodName,
+ strongRefs.getClass());
+
+ Constructor c = appMain.getConstructor(String.class,
+ problems.getClass());
+
+ Object o = c.newInstance(uniqClassName, problems);
+
+ if (saveStrongRef) {
+ System.out.printf("%s: force strong ref\n",
+ uniqClassName);
+ launch.invoke(o, strongRefs);
+ } else {
+ HashMap<String, ImageInputStream> empty = null;
+ launch.invoke(o, empty);
+ }
+
+ ctx = null;
+ } catch (Throwable e) {
+ problems.add(e);
+ } finally {
+ doneSignal.countDown();
+ }
+ }
+ };
+
+ MyClassLoader appClassLoader = new MyClassLoader(urls, uniqClassName);
+
+ refs.put(appClassLoader, uniqClassName);
+
+ Thread appThread = new Thread(appsThreadGroup, launchIt,
+ "AppThread" + counter++);
+ appThread.setContextClassLoader(appClassLoader);
+
+ appThread.start();
+ launchIt = null;
+ appThread = null;
+ appClassLoader = null;
+ }
+
+ private static class MyClassLoader extends URLClassLoader {
+
+ private static boolean verbose =
+ Boolean.getBoolean("verboseClassLoading");
+ private String uniqClassName;
+
+ public MyClassLoader(URL[] urls, String uniq) {
+ super(urls);
+
+ uniqClassName = uniq;
+ }
+
+ public Class loadClass(String name) throws ClassNotFoundException {
+ if (verbose) {
+ System.out.printf("%s: load class %s\n", uniqClassName, name);
+ }
+ if (uniqClassName.equals(name)) {
+ return Object.class;
+ }
+ return super.loadClass(name);
+ }
+
+ public String toString() {
+ return "MyClassLoader(" + uniqClassName + ")";
+ }
+ }
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/test/javax/imageio/stream/StreamCloserLeak/testapp/Main.java Thu Jan 15 13:55:30 2009 +0300
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2009 Sun Microsystems, Inc. All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+package testapp;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import javax.imageio.stream.FileCacheImageInputStream;
+import javax.imageio.stream.ImageInputStream;
+
+public class Main {
+
+ public static void main(String[] args) {
+ Main o = new Main("testapp.some.class", null);
+ o.launch(null);
+ }
+
+ private final String uniqClassName;
+ private final ConcurrentLinkedQueue<Throwable> problems;
+
+ public Main(String uniq, ConcurrentLinkedQueue<Throwable> p) {
+ uniqClassName = uniq;
+ problems = p;
+ }
+
+ public void launch(HashMap<String, ImageInputStream> refs) {
+ System.out.printf("%s: current context class loader: %s\n",
+ uniqClassName,
+ Thread.currentThread().getContextClassLoader());
+ try {
+ byte[] data = new byte[1024];
+ ByteArrayInputStream bais = new ByteArrayInputStream(data);
+ MyImageInputStream iis = new MyImageInputStream(bais,
+ uniqClassName,
+ problems);
+ if (refs != null) {
+ System.out.printf("%s: added to strong store\n",
+ uniqClassName);
+ refs.put(uniqClassName, iis);
+ }
+ iis.read();
+ //leave stream open : let's shutdown hook work!
+ } catch (IOException e) {
+ problems.add(e);
+ }
+ }
+
+ private static class MyImageInputStream extends FileCacheImageInputStream {
+ private final String uniqClassName;
+ private ConcurrentLinkedQueue<Throwable> problems;
+ public MyImageInputStream(InputStream is, String uniq,
+ ConcurrentLinkedQueue<Throwable> p) throws IOException
+ {
+ super(is, new File("tmp"));
+ uniqClassName = uniq;
+ problems = p;
+ }
+
+ @Override
+ public void close() throws IOException {
+ Test t = new Test();
+ try {
+ t.doTest(uniqClassName);
+ } catch (Throwable e) {
+ problems.add(e);
+ }
+
+ super.close();
+
+ problems = null;
+ }
+ }
+}
+
+class Test {
+ public void doTest(String uniqClassName) throws ClassNotFoundException {
+ System.out.printf("%s: Current thread: %s\n", uniqClassName,
+ Thread.currentThread());
+
+ ClassLoader thisCL = this.getClass().getClassLoader();
+ Class uniq = thisCL.loadClass(uniqClassName);
+
+ System.out.printf("%s: test is done!\n",uniqClassName);
+ }
+}