Java LinkageError Loader Constraint 分析

前言

作为 java 程序员, 我们经常遇到该错误, 特别是在传统的 web 容器里, 例如 tomcat, jetty;

例如 stackoverflow 上的经典错误;

StandardClassLoader 尝试加载 ExpressionFactory, 但 ExpressionFactory 已经被 JasperLoader 加载过了;

https://stackoverflow.com/questions/8487048/java-lang-linkageerror-javax-servlet-jsp-jspapplicationcontext-getexpressionfac

通常解决办法就是将 JasperLoader classpath 里的 javax/el/ExpressionFactory 删掉, 或者设置为 provide, 不要让 JasperLoader 去加载这个类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
exception

javax.servlet.ServletException: java.lang.LinkageError: loader constraint violation: when resolving interface method "javax.servlet.jsp.JspApplicationContext.getExpressionFactory()Ljavax/el/ExpressionFactory;
" the class loader (instance of org/apache/jasper/servlet/JasperLoader) of the current class, org/apache/jsp/index_jsp, and the class loader (instance of org/apache/catalina/loader/StandardClassLoader) for resolved class, javax/servlet/jsp/JspApplicationContext,
have different Class objects for the type javax/el/ExpressionFactory used in the signature
org.apache.jasper.servlet.JspServlet.service(JspServlet.java:343)
javax.servlet.http.HttpServlet.service(HttpServlet.java:722)

root cause

java.lang.LinkageError: loader constraint violation: when resolving interface method "javax.servlet.jsp.JspApplicationContext.getExpressionFactory()Ljavax/el/ExpressionFactory;" the class loader (instance of org/apache/jasper/servlet/JasperLoader) of the current class, org/apache/jsp/index_jsp, and the class loader (instance of org/apache/catalina/loader/StandardClassLoader) for resolved class, javax/servlet/jsp/JspApplicationContext, have different Class objects for the type javax/el/ExpressionFactory used in the signature
org.apache.jsp.index_jsp._jspInit(index_jsp.java:31)
org.apache.jasper.runtime.HttpJspBase.init(HttpJspBase.java:49)
org.apache.jasper.servlet.JspServletWrapper.getServlet(JspServletWrapper.java:180)
org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:369)
org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:390)
org.apache.jasper.servlet.JspServlet.service(JspServlet.java:334)
javax.servlet.http.HttpServlet.service(HttpServlet.java:722)

本文重点是如何复现 LinkageError 和分析 LinkageError 出现的原因;

分析

  1. 这是个什么错误?
  2. 为什么会出现这个错误?

GPT 回答: java.lang.LinkageError: loader constraint violation 是 Java 抛出的一个错误,表示类加载器之间存在一个不允许的情况。具体来说,当两个不同的类加载器试图加载相同完全限定名的类,但为这个名字指向了不同版本的类文件时,这种冲突就会发生。

再简单一点说:

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

注意:

  1. 这2个类加载器(org.example.SFClassloader)是 self-first 的, 即违反了双亲委派的类加载器.
  2. 这2个类加载器是父子结构
  3. c1 和 c2 这 2 个目录都包含 C0 这个类.

结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class C0 {
public C0() {
}
}

public class C1 {
public C1() {
}

public C0 get() {
return new C0();
}
}

public class C2 {
C0 c0 = new C0();

public C2() {
}

public C0 get() {
return (new C1()).get();
}
}

执行下面的代码即可复现:

1
2
3
4
5
6
7
8
9
10
11
12
13
URL u = new File("c1/").toURI().toURL();
C1SFClassloader c1SFClassloader = new C1SFClassloader(new URL[]{u}, Thread.currentThread().getContextClassLoader());

URL u2 = new File("c2/").toURI().toURL();
// 父子结构
C2SFClassloader c2SFClassloader = new C2SFClassloader(new URL[]{u2}, c1SFClassloader);
// 加载 c2, c2 cinit 会主动加载 c0
Class<?> aClass = c2SFClassloader.loadClass("org.example.C2");
Object o = aClass.newInstance();
Method get = aClass.getMethod("get");

// 调用 get, c2 会访问 c1, c1 则会再次加载 c0, 触发 error
Object invoke = get.invoke(o);

项目地址: https://github.com/stateIs0/linkageError-test

以下是 hotspot 虚拟机里关于这个错误的相关代码

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

// Constraints on class loaders. The details of the algorithm can be
// found in the OOPSLA'98 paper "Dynamic Class Loading in the Java
// Virtual Machine" by Sheng Liang and Gilad Bracha. The basic idea is
// that the system dictionary needs to maintain a set of contraints that
// must be satisfied by all classes in the dictionary.
// if defining is true, then LinkageError if already in systemDictionary
// if initiating loader, then ok if InstanceKlass matches existing entry

// 对类加载器的约束。算法的详细信息可以在 OOPSLA'98 的论文 "Dynamic Class Loading in the Java
// Virtual Machine" 中找到,该论文由 Sheng Liang 和 Gilad Bracha 编写。基本的思想是
// 系统字典需要维护一个约束集,所有在字典中的类都必须满足这些约束。
// 如果 defining 为 true,则在 systemDictionary 中已存在时会抛出 LinkageError
// 如果是初始化加载器,则当 InstanceKlass 与现有条目匹配时是可以的。

void SystemDictionary::check_constraints(int d_index, unsigned int d_hash,
instanceKlassHandle k,
Handle class_loader, bool defining,
TRAPS) {
const char *linkage_error = NULL;
{
Symbol* name = k->name();
ClassLoaderData *loader_data = class_loader_data(class_loader);

MutexLocker mu(SystemDictionary_lock, THREAD);

Klass* check = find_class(d_index, d_hash, name, loader_data);
if (check != (Klass*)NULL) {
// if different InstanceKlass - duplicate class definition,
// else - ok, class loaded by a different thread in parallel,
// we should only have found it if it was done loading and ok to use
// system dictionary only holds instance classes, placeholders
// also holds array classes

// 如果是不同的 InstanceKlass - 表示类的重复定义,
// 否则 - 没问题,类是由不同的线程并行加载的,
// 我们只应该找到它,如果它已经完成加载并且可以使用。
// 系统字典只存储实例类,
// 占位符还存储数组类。

assert(check->oop_is_instance(), "noninstance in systemdictionary");
if ((defining == true) || (k() != check)) {
linkage_error = "loader (instance of %s): attempted duplicate class "
"definition for name: \"%s\"";
} else {
return;
}
}

#ifdef ASSERT
Symbol* ph_check = find_placeholder(name, loader_data);
assert(ph_check == NULL || ph_check == name, "invalid symbol");
#endif

if (linkage_error == NULL) {
if (constraints()->check_or_update(k, class_loader, name) == false) {
linkage_error = "loader constraint violation: loader (instance of %s)"
" previously initiated loading for a different type with name \"%s\"";
}
}
}

// Throw error now if needed (cannot throw while holding
// SystemDictionary_lock because of rank ordering)

if (linkage_error) {
ResourceMark rm(THREAD);
const char* class_loader_name = loader_name(class_loader());
char* type_name = k->name()->as_C_string();
size_t buflen = strlen(linkage_error) + strlen(class_loader_name) +
strlen(type_name);
char* buf = NEW_RESOURCE_ARRAY_IN_THREAD(THREAD, char, buflen);
jio_snprintf(buf, buflen, linkage_error, class_loader_name, type_name);
THROW_MSG(vmSymbols::java_lang_LinkageError(), buf);
}
}

这段代码在Java虚拟机中确保了类加载约束的满足,并确保不会有重复的类定义。如果违反了这些规则,它会抛出一个LinkageError异常。

注释里提到的论文: https://dl.acm.org/doi/10.1145/286942.286945

maven 模块依赖复现

Maven 依赖

日常情况下,我们经常在 maven 模块里出现该错误;此处,我们使用 maven 模块依赖, 进行复现.

类 依赖

整体 依赖

运行 org.example.Main#main

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
son org.example.HelloImplSon start, 
son org.example.HelloInterface start,
son java.lang.Object start,
parent java.lang.Object start,
son java.lang.Throwable start,
objc[79363]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0_72.jdk/Contents/Home/bin/java (0x10235b4c0) and /Library/Java/JavaVirtualMachines/jdk1.8.0_72.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x10aa884e0). One of the two will be used. Which one is undefined.
java.lang.Object
java.lang.Object
son org.example.HelloInterface end, org.example.SonClassLoader@266474c2
son org.example.HelloImplSon end, org.example.SonClassLoader@266474c2
java.lang.Throwable
java.lang.Throwable
parent java.lang.Throwable start,
son java.lang.Error start,
java.lang.Error
parent java.lang.Error start,
java.lang.Error
son org.example.HelloModel start,
son org.example.HelloModel end, org.example.SonClassLoader@266474c2
son org.example.Delegate start,
org.example.Delegate
parent org.example.Delegate start,
parent java.lang.Object start,
parent org.example.HelloInterface start,
java.lang.Object
parent org.example.Delegate end, org.example.ParentClassLoader@7440e464
java.lang.LinkageError: loader constraint violation: loader (instance of org/example/ParentClassLoader) previously initiated loading for a different type with name "org/example/HelloInterface"
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at org.example.MyClassLoader.loadClass(MyClassLoader.java:76)
at org.example.HelloImplSon.hello(HelloImplSon.java:28)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.example.Main.main(Main.java:18)

这个项目也是使用 Self first 模式, 和上面的 c0, c1, c2 类似;
项目地址: https://github.com/stateIs0/linkageError-test

总结

要复现 LinkageError 核心条件:

  1. 两个 CL 是 self-first 模式
  2. 两个类加载器是父子类加载器(相对于平行类加载器)
  3. 父子 CL classpath都有同名类, 且有交叉访问(例如 C2 里访问 C1, C2 和 C1 都有 C0).

参考


Java LinkageError Loader Constraint 分析
http://thinkinjava.cn/2023/08/21/2023/linkageerror/
作者
莫那·鲁道
发布于
2023年8月21日
许可协议