本文地址:https://www.iflym.com/index.php/code/understand-jvm-load-constraint.html
网上进行google或者baidu时,以及在使用tomcat或者其它框架时,经常碰到以下的问题:
1
2
|
ava.lang.LinkageError: loader constraint violation: when resolving field XXXXXX
have different Class objects for that type
|
这种问题在使用jbmp,或者websphere时经常会出现,一般的解决方法就是说有的包重复了,删除一些jar包就可以了。
问题是解决了,但问题产生的根源在哪,我们需要知道这个问题。在阅读了《深入JVM虚拟机》之后,中间有提到一段话,这段话揭示了问题产生的本质在于在一个方法体中同一个类被多个不同的classLoader所加载了,即在声明时的类与引用的类(同一个类)在加载时,却是由不同的classLoader所加载的:
1
2
|
如果引用的类型和被引用的类型并非由同一个初始装载器装载,虚拟机必须确保在字段或者方法描述符中所提及的类型
在不同的命名空间中保持一致。
|
这句话理解起来非常的困难,简单一点理解可以由下面的说明来理解:
为什么会产生上面的输出,我们可以来看一个以下的代码
首先是一个简单的类调用:
类Foo3
1
2
3
4
5
6
|
public
class
Foo3
implements
IFoo{
public
void
hello()
throws
Exception{
Class> clazz = Foo.
class
;
Foo foo2 = Foo4.foo;
}
}
|
在上面的代码中,变量foo2引用了类Foo4的一个静态引用:
1
2
3
|
public
class
Foo4 {
public
static
Foo foo =
new
Foo();
}
|
类Foo是一个非常简单的java类,即普通的java类:
1
|
public
class
Foo
implements
IFoo{}
|
重点在于如何运行这段代码,我们运行一段代码,分别使用两个类加载器来加载同一个类类Foo,运行代码如下:
1
2
3
|
MyClassLoader3 myClassLoader3 =
new
MyClassLoader3(T.
class
.getClassLoader());
IFoo foo3 = (IFoo) (myClassLoader3.loadClass(
"com.m_ylf.study.java.classLoad.Foo3"
).newInstance());
foo3.hello();
|
在上面的代码中,采用自定义的classLoader来定义类Foo3,我们来看具体的定义:
1
2
3
4
5
6
7
8
9
|
public
Class> loadClass(String name)
throws
ClassNotFoundException {
if
(
"Foo"
.equals(name) ) {
//自定义
}
if
(
"Foo3"
.equals(name) ) {
//自定义
}
return
super
.loadClass(name);
}
|
其实就是将类类Foo和类Foo3交由classLoader3即我们自定义加载器来加载,其它的类仍交由super即appClassLoader来加载。现在运行这段代码,即会有一个出错信息,出错信息如下:
1
2
3
4
|
Exception in thread "main" java.lang.LinkageError: loader constraint violation: when resolving field "foo" the class
loader (instance of MyClassLoader3) of the referring class, Foo4, and the class loader (instance of sun/misc
/Launcher$AppClassLoader) for the field's resolved type, /Foo, have different Class objects for that type
at Foo3.hello(Foo3.java:7)
|
错误在第7行,即Foo foo2 = Foo4.foo;这一行出错了。
为什么会出错,我来看来第一行代码:Class> clazz = Foo.class;这段代码,会对Foo类进行加载,采用的加载器为myClassLoader3,即加载Foo3类时所使用的加载器。这句话运行之后,即表示类Foo已经被加载了,且加载器为myClassLoader3。
第二行代码:Foo foo2 = Foo4.foo。这段代码会初始化Foo4,由于myClassLoader3并没有特殊处理Foo4,所以将由父类加载器,即AppClassLoader来加载,在加载过程中,因为调用到了Foo4.foo,所以会加载Foo类。这个加载是在Foo4类初始化时进行加载的。因为在碰到类Foo时,appClassLoader显示其从未加载过foo(先前的foo是由myClassLoader加载的,而不是由appClassLoader加载的),所以又会加载Foo。
这时候,类Foo就会有两个类加载器,一个是由myClassLoader3加载的,另一个是由appClassLoader加载的。如果两个类分开运行,代码是没有问题的。
问题就出在这个赋值语句,或者说是对象引用上。在Foo3内部使用Foo4.foo时,JVM会记录Foo4.foo在foo3内部的类引用和加载器,在这个运行代码中,此加载器为myClassLoader,因为在调用Foo4.foo之前已经加载了Foo。然而,在引用时,它将得到声明Foo4.foo时的Foo类型的加载器,在Foo4.foo中, Foo类型的加载器为appClassLoader。JVM在运行时会对这两个加载器进行验证,JVM规范中要求这两个加载器必须要一致,否则将报类验证错误,即VerifyError的错误,这是为了防止不正常的类冒充正确的类进行类型欺骗。如在类Foo3中的Foo是来自于黑客故意构建的一个类时。
我们再来看关于jbmp的问题,这是由于引用juel.jar时,里面有一个类如ExpressionFactory类,此类在类JspApplicationContext中被声明。在juel.jar中,类ExpressionFactory已经被jspClassLoader加载了,现在要进行赋值语句,即=由jspContext中取得的expressionFactory对象。而JspApplicationContext是由Tomcat的StandardClassLoader类加载的,在类JspApplicationContext中声明的expressionFactory字段自然也是由StandardClassLoader类加载的。现在两个由不同类加载器加载的同一个对象要进行引用操作,自然不能通过JVM的验证了。
总而言之,就是说JVM在引用其它类的字段,或者调用其它类的方法时,将进行类型验证。验证包括,字段的类型验证,方法的返回类型验证,方法参数类型验证等。验证的内容就验证在调用方和被调用方时,同一个类的加载器是否一致。即在调用方时,记录的字段(参数)类型的加载器与被调用方法记录的字段(参数)类型的加载器是否一致。如果不一致,自然就不会被JVM验证通过。