罕见类加载冲突问题:LinkageError

问题描述

假设有C1类和C2类都依赖C0,C1和C2分别用不同的2个类加载器加载,而这两个类加载器都能在自己的类加载路径中加载到C0,这个时候如果在C1中调用C2的某个方法(注:这个方法的签名中依赖了C0)就会出现LinkageError错误。

用例模拟及分析

冲突的依赖类,模拟问题描述中的C0

package loader;

public class ConflictDependence {
}

发生错误的类,模拟问题描述中的C2

package loader;

public class ErrorTester {
    static {
        // 在其它ClassLoader中必需使用一次这个 MyDependence 依赖,
        // 这样在另一个ClassLoader使用的时候就会出错
        System.out.println("ErrorTester MyDependence Location: " +
                ConflictDependence.class.getProtectionDomain().getCodeSource());
    }

    // 不会出错的方法
//    public static void test0() {
//        System.out.println("ErrorTester MyDependence Location: " +
//                ConflictDependence.class.getProtectionDomain().getCodeSource());
//    }

    // 出错方法
//    public static void test(ConflictDependence a) {
//    }

    // 出错方法
    public static ConflictDependence test() {
        return null;
    }
}

被另一个类加载器加载的类,模拟问题描述中的C1

package loader;

public class SubClass {
    private ConflictDependence myDependence;

    public SubClass() {
        System.out.println(ConflictDependence.class.getProtectionDomain().getCodeSource());
        ConflictDependence test = ErrorTester.test();
    }
}

测试类

package loader;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class Test {
    public static void main(String[] args) throws Exception {
        URL[] urls = new URL[]{new File("/home/conquer/Desktop/jse.jar").toURI().toURL()};
        SubFirstLoader newLoader = new SubFirstLoader(urls);
        ErrorTester.test();
        Class b = newLoader.loadClass("loader.SubClass");
        b.newInstance();
    }

    static class SubFirstLoader extends URLClassLoader {
        public SubFirstLoader(URL[] urls) {
            super(urls);
        }
        @Override
        public Class loadClass(String name) throws ClassNotFoundException {
            try {
                // 从自身类路径中加载
                return this.findClass(name);
            } catch (Exception e) {
                // 从父ClassLoader加载
                return super.loadClass(name);
            }
        }
    }
}

模拟说明:
1.将以上的4个类打jar包,名为为jse.jar或其他名字
2.从jar中删除ErrorTester和Test两个class文件
3.修改测试程序Test中的jar路径,运行测试即可看到出错信息

问题分析:
测试中使用了两个类加载器来加载类,使用系统类加载器加载ErrorTester,这个类依赖了ConflictDependence,可以看到静态块里同时也打印出系统类加载器也从自己的加载路径中加载了ConflictDependence,然后使用自定义的类加载器(一个子优先的类加载器)加载了Sub Class,并通过反射构造其实例,构造过程中依赖了ConflictDependence,所以会去尝试加载ConflictDependence,由于自定义的加载器是子优先模式的,会先从自身类加载路径查找并加载,所以可以加载到一个ConflictDependence,注意这个ConflictDependence和系统类加载器加载的并不是同一个,在SubClass中调用了ErrorTester.test()方法,这个方法的签名依赖的是系统类加载器加载的ConflictDependence,而SubClass也有同名的不同实例的ConflictDependence,因此就产生了冲突,这里表现为LinkageError,就是SubClass将自己加载的ConflictDependence链接到ErrorTester.test()方法(这个方法的签名在当前内存中依赖的是系统类加载器加载的ConflictDependence)时产生错误。
继续测试发现,调用test0()方法并无错误,这是因为test0()方法的签名没有依赖到冲突的
ConflictDependence(这里需要注意方法体内的ConflictDependence并不会产生冲突错误,这是因为方法体的字节码不会编译到SubClass的字节码中,类加载器约束检查只会在当前这个SubClass类的方法中检查冲突,其他方法体的冲突是在其他方法体内检查的,简单说就是类冲突检查的单元是在一个方法体内),
另外需要说明的是方法的参数和方法的返回值都属于方法的签名,方法的签名都会编译到调用端的方法体内,所以上面的void test(ConflictDependence a)方法和ConflictDependence test()都会出错。
另外需要注意的是:ErrorTester中使用了静态块一个是可以看出当前实例加载的是哪个ConflictDependence,另一个原因是为了触发一下让系统类加载加载一下ConflictDependence,以促成后面的冲突问题产生,其实如果不在静态块中触发,在test()的方法体中触发也是可以产生此问题的,同理SubClass中的打印也是为了触发当前类的类加载器加载一下ConflictDependence。

关于更多类加载问题,请参考:
ClassLoader问题剖析

你可能感兴趣的:(J2SE)