guaner
guaner

Reputation: 82

Resolving policy of Java constant pool

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:

  1. Shouldn't the invocation of login immediately raise an error?
  2. Why the following usage of baseCL to load User fails with LinkageError?

Any help would be appreciated!

Credit given to Frank Kieviet's blog as the example is constructed from his delving of LinkageError.

Upvotes: 3

Views: 171

Answers (1)

Thomas Kl&#228;ger
Thomas Kl&#228;ger

Reputation: 21560

Question 1: "Shouldn't the invocation of login immediately 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.

  • on the caller side (in Main.process()) the resolution is done and succeeds
  • on the called side (in Login.login()) no resolution is done because you don't invoke any of the User class methods or access any of the fields of the class.
  • the method call u.getClass() doesn't count because that resolves to the Object.getClass() method

Question 2: "Why the following usage of baseCL to load User fails with LinkageError?"

When the JVM loaded the Login class it already had a class named User available as loaded by the specialCL. It stored the pair (specialCL, "User") in the loader constraints for the Login class.

When the JVM now needs to resolve the User class within the Login class, it calls the Login classes classloader to load the User class. This loading however returns another User class 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

Upvotes: 2

Related Questions