EZLippi-浮生志

Java ClassLoader loader constraints分析

在和Web容器打交道的时候大家有时候会遇到java.lang.LinkageError: loader constraint violation: loader (instance of <bootloader>) previously initiated loading for a different type with name ...这个错误,网上的教程都是说类加载冲突了,把从pom文件里把servlet和el这些依赖的scope改为provided,或者将Tomcat webapp类加载器的delegate属性改为true,让Tomcat的CommonClassLoader来加载,但是始终找不到一篇文章来具体分析这个错误。这篇文章从原理和实践的角度来介绍为什么要这么做。

问题来源

下面有三个很简单的类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class User {
}

public static class LoginService {
static {
System.out.println("LoginService loaded");
}
public static void login(User user) {
}
}

public static class Servlet {
public static void doGet() {
User user = new User();
System.out.println("Logging in with User loaded in " +
user.getClass().getClassLoader());
LoginService.login(user);
}
}

创建自定义的类加载器

在Java里你可以继承ClassLoader类来实现你自己的类加载器,这样子就允许在内存中加载同个类的多个版本(常用在OSGI里)。当一个类加载器被要求加载一个类时,它可以首先要求它的父类来加载,如果父类加载失败它再尝试自己加载,这种称为父类优先类加载器。除此之外,类加载器可以尝试自己先加载,只有自己找不到时才委托给父类,比如我们要从网络流中加载或者本地字节码经过加密了要先解密再加载。关于类加载器的双亲委派模型我就不介绍了,网上有很多教程。

我这里写了一个自定义的类加载器,重写了findClass()和loadClass()方法,这个类加载器只加载指定列表里的类,其他类委托给父类加载器进行加载,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
 /**
* 自己优先加载的类加载器,只加载指定类,其他委托给父加载器
*/

private static class CustomClassLoader extends ClassLoader {
private Set<String> urls;
private String label;

public CustomClassLoader(String name, ClassLoader parent, String... url) {
super(parent);
this.label = name;
this.urls = new HashSet<String>(Arrays.asList(url));
}

@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
if (urls.contains(name)) {
try {
String location = name.replace('.', '/') + ".class";
InputStream inputStream = Demo.class.getClassLoader().getResourceAsStream(location);
byte[] buf = new byte[2000];
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int length;
while ((length = inputStream.read(buf)) != -1) {
outputStream.write(buf, 0 , length);
}
//流的关闭应该放到finally块,这里为了演示简化了
inputStream.close();
System.out.println(label + ": Loading " + name + " in " +
label + " classloader");
byte[] data = outputStream.toByteArray();
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}

throw new ClassNotFoundException(name);
}

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//需要调用findLoaderClass()来检查该类是否已经加载过
if (findLoadedClass(name) != null) {
System.out.println(label + ": already loaded(" + name + ")");
return findLoadedClass(name);
}
if (urls.contains(name)) {
return findClass(name);
} else {
System.out.println(label + ": super.loadclass(" + name + ")");
return super.loadClass(name, resolve);
}
}

创建多个类加载器

接下来我创建了三个不同的类加载器,分别加载不同的类,如下所示:

1
2
3
4
5
6
7
8
9
10
//beanLoader只加载User和LoginService类
CustomClassLoader beanLoader = new CustomClassLoader("BeanLoader ",
Demo.class.getClassLoader(),
"com.ezlippi.Demo$User", "com.ezlippi.Demo$LoginService");
//webLoader只加载Servlet类,父加载器为beanLoader
CustomClassLoader webLoder = new CustomClassLoader("WebLoader",
beanLoader, "com.ezlippi.Demo$Servlet");
//webBeanLoader加载User和Servlet类,父加载器也为beanLoader
CustomClassLoader webBeanLoader = new CustomClassLoader("webBeanLoader", beanLoader,
"com.ezlippi.Demo$User", "com.ezlippi.Demo$Servlet");

类加载过程

当LoginService类被加载,这中间发生了什么事情,我们来看一下:

1
2
3
4
5
6
beanLoader.loadClass("com.ezlippi.Demo$LoginService", true).newInstance();
输出如下所示:
BeanLoader : Loading com.ezlippi.Demo$LoginService in BeanLoader classloader
BeanLoader : super.loadclass(java.lang.Object)
BeanLoader : super.loadclass(java.lang.System)
BeanLoader : super.loadclass(java.io.PrintStream)

当JVM加载LoginService类时,它会检查这个类引用的所有类然后也会加载这些类,java.lang.Object是它的父类,java.lang.System和java.io.PrintStream是在静态块中被引用的,这些类在启动时已经被启动类加载器加载了。当BeanLoader收到加载这些jvm自带的类时,把加载的请求代理给父类加载器(这里是appClassLoader),实际上对于java.和Javax.开头的类我们需要委托给父类加载器加载。

同时我们应该注意到login方法里的User类并没有加载,因为这个User类是在方法内部使用的,这时候还不足以引起这个类的加载。
接下来我们用webBeanLoader来加载Servlet类:

1
2
3
4
5
6
7
8
webBeanLoader.loadClass("com.ezlippi.Demo$Servlet", false).
getMethod("doGet").invoke(null);

webBeanLoader: Loading com.ezlippi.Demo$Servlet in webBeanLoader classloader
webBeanLoader: Loading com.ezlippi.Demo$User in webBeanLoader classloader
Logging in with User loaded in webBeanLoader
webBeanLoader: super.loadclass(com.ezlippi.Demo$LoginService)
BeanLoader : already loaded(com.ezlippi.Demo$LoginService)

这里我省略了JVM自带的类,由于我们调用了servlet的doGet()方法,所以这里可以看到User类和LoginService类也被加载了,这里是委托给了父类beanLoader进行加载。
接下来问题来了:

1
2
3
4
5
6
7
8
//调用getMethods()时LoginService和User类建立了引用关系,所以会去加载User类
beanLoader.loadClass("com.ezlippi.Demo$LoginService", false).getMethods();
BeanLoader : already loaded(com.ezlippi.Demo$LoginService)
BeanLoader : Loading com.ezlippi.Demo$User in BeanLoader classloader
java.lang.LinkageError: loader constraint violation:
loader (instance of com/ezlippi/Demo$CustomClassLoader)previously initiated
loading for a different type with name "com/ezlippi/Demo$User"
... 12 more

这里你会发现抛出了java.lang.LinkageError,我们可以加个虚拟机参数-XX:+TraceLoaderConstraints把约束打印出来,结果如下:

1
2
3
4
5
6
7
BeanLoader  : already loaded(com.ezlippi.Demo$LoginService)
[Adding new constraint for name: com/ezlippi/Demo$User, loader[0]:com/ezlippi/Demo$CustomClassLoader, loader[1]: com/ezlippi/Demo$CustomClassLoader ]
BeanLoader : Loading com.ezlippi.Demo$User in BeanLoader classloader
[Constraint check failed for name com/ezlippi/Demo$User, loader com/ezlippi/Demo$CustomClassLoader: the presented classobject differs from that stored ]
java.lang.LinkageError: loader constraint violation: loader(instance of com/ezlippi/Demo$CustomClassLoader) previously initiated
loading for a different type with name "com/ezlippi/Demo$User"
... 12 more

类加载器beanLoader在加载完LoginService尝试加载User类时,会添加一个约束,loader[0]就是beanLoader,loader[1]是webBeanLoader,当两个类加载器加载的类引用同一个类(这里是User类),并且这两个类之间建立了引用关系时,这个User类必须是同一个,如果不是就会抛出java.lang.LinkageError。

JAVA虚拟机规范里是这么写的:当解析一个包含在中的符号引用(它指向的是类中声明的类型T的字段)时,虚拟机必须产生下列装载约束,TL1=TL2。接下来看另一种场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
webLoder.loadClass("com.ezlippi.Demo$Servlet", false).
getMethod("doGet").invoke(null);
WebLoader: Loading com.ezlippi.Demo$Servlet in WebLoader classloader
WebLoader: super.loadclass(java.lang.Object)
BeanLoader : already loaded(java.lang.Object)
WebLoader: super.loadclass(com.ezlippi.Demo$User)
BeanLoader : Loading com.ezlippi.Demo$User in BeanLoader classloader
[Constraint check failed for name com/ezlippi/Demo$User, loader
com/ezlippi/Demo$CustomClassLoader: the presented class object differs from that stored ]
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at com.ezlippi.Demo.test1(Demo.java:116)
at com.ezlippi.Demo.main(Demo.java:121)
Caused by: java.lang.LinkageError: loader constraint violation:loader(instance of
com/ezlippi/Demo$CustomClassLoader) previously initiated loading for a
different type with name "com/ezlippi/Demo$User"
... 12 more

这里通过webLoader去加载Servlet类,然后调用doGet()方法触发了User类的加载,这里委托给了父类加载器beanLoader去加载,在调用LoginService.login(user)方法时触发了前面这个约束条件,因为加载LoginService的beanLoader也加载了User类,两个类加载器加载同一个类是没问题的,但是当他们建立了引用关系时就会添加一个约束,LoginService期望的是beanLoader加载的User类,而Servlet类传递给他的是webLoader加载的User类,两个不是同一个。

总结一下:无论何时当两个类加载器加载两个不同的类(比如这里的Servlet和LoginService),当这两个不同的类通过某种方式建立了引用关系(比如这里通过User类),虚拟机就会在约束列表上加一个约束,虚拟机在解析符号引用的时候必须检查当前已经能够装载的所有约束。
源码在这里

🐶 您的支持将鼓励我继续创作 🐶