Challenge

Watch a class loader refuse to die

20 minsenior312 yrs

Edge cases. You have to reason, and two valid fixes differ.

What this teaches

  • Each loader defines its own copy of a class, and the two are not the same type
  • A Class object is a hard reference to its loader
  • An instance reaches its class, which reaches its loader — three hops to a leak
  • A WeakReference to the loader is how you prove collection, rather than assuming it

Starter

Starter.javaOpen in playground
import java.io.*;
import java.lang.ref.*;
import java.util.*;

/**
 * CHALLENGE — 20 minutes.
 *
 * Class metadata is released per class loader, all at once or not at all.
 * Four candidate holders are listed below. Some pin the loader and some do
 * not, and the difference is a chain of references you can write out.
 *
 * TASKS
 *   1. Run it. The first probe shows a loader IS collectable when nothing
 *      holds it — that is your control.
 *   2. For each of the four holders, predict pinning or not BEFORE testing,
 *      and write down the reference chain you think exists.
 *   3. Implement testHolder() so each one is actually measured.
 *   4. The string case is the interesting one. Work out why its answer
 *      differs from the Class case even though it came from the same object.
 *   5. In a comment: state the general rule in one sentence.
 */
public class Starter {

    public static class Plugin {
        public static String describe() {
            return "plugin";
        }
    }

    static final String PLUGIN = "Starter$Plugin";

    static final class DeployLoader extends ClassLoader {
        DeployLoader(ClassLoader parent) {
            super(parent);
        }

        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (!name.equals(PLUGIN)) {
                return super.loadClass(name, resolve);
            }
            synchronized (getClassLoadingLock(name)) {
                Class<?> already = findLoadedClass(name);
                if (already != null) {
                    return already;
                }
                try (InputStream in = getParent().getResourceAsStream(PLUGIN + ".class")) {
                    byte[] bytes = in.readAllBytes();
                    return defineClass(name, bytes, 0, bytes.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name, e);
                }
            }
        }
    }

    /** Somewhere for a holder to survive in. */
    static final List<Object> HELD = new ArrayList<>();

    public static void main(String[] args) throws Exception {
        // Control: nothing holds it.
        DeployLoader loader = new DeployLoader(Starter.class.getClassLoader());
        Class<?> plugin = loader.loadClass(PLUGIN);

        System.out.println("two distinct copies of the same class : " + (plugin != Plugin.class));
        System.out.println("same name                             : "
                + plugin.getName().equals(Plugin.class.getName()));

        WeakReference<ClassLoader> control = new WeakReference<>(loader);
        loader = null;
        plugin = null;
        System.out.println("control — nothing held                : "
                + (collected(control) ? "collected" : "PINNED"));

        System.out.println();
        System.out.println("── the four candidates ──");

        // TASK 3: implement testHolder and uncomment these.
        //
        // System.out.println("holding the Class        : " + testHolder("class"));
        // System.out.println("holding an INSTANCE      : " + testHolder("instance"));
        // System.out.println("holding the class NAME   : " + testHolder("name"));
        // System.out.println("holding a Method object  : " + testHolder("method"));
    }

    /**
     * TASK 3: create a loader, load Plugin, put the named thing into HELD,
     * drop every other reference, and report whether the loader was
     * collected.
     *
     *   "class"    -> HELD.add(plugin)
     *   "instance" -> HELD.add(plugin.getDeclaredConstructor().newInstance())
     *   "name"     -> HELD.add(plugin.getName())
     *   "method"   -> HELD.add(plugin.getMethod("describe"))
     */
    static String testHolder(String what) throws Exception {
        throw new UnsupportedOperationException("task 3");
    }

    static boolean collected(WeakReference<?> ref) {
        for (int attempt = 0; attempt < 50 && ref.get() != null; attempt++) {
            System.gc();
            byte[] churn = new byte[1 << 20];
            if (churn.length < 0) System.out.print("");
        }
        return ref.get() == null;
    }
}

Run it locally:

cd exercises/java/jvm/metaspace-vs-permgen/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    Start by confirming the two copies really are distinct types — try assigning an instance of one to a variable of the other via reflection.

  2. Hint 2

    For each of the four holders, ask: what is the chain of references from a GC root to the loader? Write it out before you test it.

  3. Hint 3

    An INSTANCE of the plugin class is enough on its own. Why?

  4. Hint 4

    The string case is the interesting one — work out whether holding plugin.getName() pins anything, and say why the answer differs from holding the Class.

Done when

  • Each of the four holders is tested and classified as pinning or not
  • You explain the reference chain for each one that pins
  • The string case is correctly identified as harmless, with a reason
  • A comment states the general rule in one sentence

Stretch

Make a version that pins the loader through a route not listed: register something from the plugin with a JDK singleton the parent loader owns — a shutdown hook, or a Thread with the plugin class as its context class loader. Then say why this class of bug is so hard to prevent by review.

← Back to What replaced PermGen, and why?