6788096: ImageIO SreamCloser causes memory leak in FX applets
authorbae
Thu, 15 Jan 2009 13:55:30 +0300
changeset 2377 31e15e69d958
parent 2376 63e13f6d2319
child 2378 256dd41b49ab
6788096: ImageIO SreamCloser causes memory leak in FX applets Reviewed-by: igor, prr
jdk/src/share/classes/com/sun/imageio/stream/StreamCloser.java
jdk/test/javax/imageio/stream/StreamCloserLeak/run_test.sh
jdk/test/javax/imageio/stream/StreamCloserLeak/test/Main.java
jdk/test/javax/imageio/stream/StreamCloserLeak/testapp/Main.java
--- 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);
+    }
+}