ExerciseChallenge
Challenge
Watch a class loader refuse to die
20 minsenior3–12 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 StarterHints
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.
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.
Hint 3
An INSTANCE of the plugin class is enough on its own. Why?
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.