From many places like Java SE Specifications and "Inside Java Virtual Machine", they all state the notion that when the resolution of a constant pool entry requires loading a type, the virtual machine uses the same class loader that loaded the referencing type to load the referenced type.
However, I find out this rule doesn't always hold:
package com.alaneuler.test;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class LinkageMain {
static class MyClassLoader extends ClassLoader {
private Set<String> selfFirstClasses;
private String name;
public MyClassLoader(String name, ClassLoader parent, String... selfFirstNames) {
super(parent);
this.name = name;
selfFirstClasses = new HashSet<>(Arrays.asList(selfFirstNames));
}
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (selfFirstClasses.contains(name)) {
String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";
try (InputStream is = getClass().getResourceAsStream(filename)) {
byte[] buf = new byte[is.available()];
int len = is.read(buf);
System.out.println(this.name + ": loading " + name);
return defineClass(name, buf, 0, len);
} catch (Exception e) {
e.printStackTrace();
}
}
if (!name.startsWith("java.")) {
System.out.println(this.name + ": super.loadClass(" + name + ")");
}
return super.loadClass(name, resolve);
}
@Override
public String toString() {
return name;
}
}
public static class User {}
public static class Login {
public void login(User u) {
System.out.println("--- login called with user loaded by " + u.getClass().getClassLoader() + " ---");
}
}
public static class Main {
public static void process() {
User u = new User();
new Login().login(u);
}
}
public static void main(String[] args) throws Exception {
MyClassLoader baseCL = new MyClassLoader("Base", LinkageMain.class.getClassLoader(),
"com.alaneuler.test.LinkageMain$User",
"com.alaneuler.test.LinkageMain$Login");
MyClassLoader specialCL = new MyClassLoader("specialCL", baseCL,
"com.alaneuler.test.LinkageMain$User",
"com.alaneuler.test.LinkageMain$Main");
specialCL.loadClass("com.alaneuler.test.LinkageMain$Main").getMethod("process").invoke(null);
}
}
I define a customized ClassLoader which takes in its constructor a list of class names that should be defined by itself. Based on that, baseCL is configured to load class Login and User and specialCl to load User and Main (I know this setup breaks the class-loader delegation model).
Running this example produces:
specialCL: loading com.alaneuler.test.LinkageMain$Main
specialCL: loading com.alaneuler.test.LinkageMain$User
specialCL: super.loadClass(com.alaneuler.test.LinkageMain$Login)
Base: loading com.alaneuler.test.LinkageMain$Login
--- login called with user loaded by specialCL ---
which says login method is called with object u loaded by specialCl not baseCL. This phenomenon is contrary to the notion stated from the beginning, which is really weird and I cannot figure out where goes wrong.
Further, usage of baseCl to load class User always fails with LinkageError: loader constraint violation: loader (instance of com/alaneuler/test/LinkageMain$MyClassLoader) previously initiated loading for a different type with name "com/alaneuler/test/LinkageMain$User".
My question is:
- Shouldn't the invocation of
loginimmediately raise an error? - Why the following usage of
baseCLto loadUserfails withLinkageError?
Any help would be appreciated!
Credit given to Frank Kieviet's blog as the example is constructed from his delving of LinkageError.
Question 1: "Shouldn't the invocation of
loginimmediately raise an error?"No, because the resolution according to Java SE Specification - Resolution is done only when any of the instructions anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, and putstatic is executed.
Main.process()) the resolution is done and succeedsLogin.login()) no resolution is done because you don't invoke any of theUserclass methods or access any of the fields of the class.u.getClass()doesn't count because that resolves to theObject.getClass()methodQuestion 2: "Why the following usage of
baseCLto loadUserfails withLinkageError?"When the JVM loaded the
Loginclass it already had a class namedUseravailable as loaded by thespecialCL. It stored the pair (specialCL, "User") in the loader constraints for theLoginclass.When the JVM now needs to resolve the
Userclass within theLoginclass, it calls theLoginclasses classloader to load theUserclass. This loading however returns anotherUserclass instance (baseCL, "User") then the one that was recorded in the loader contraints (specialCL, "User").For a description of the loading constraints see Java SE Specification - Loading Constraints