动态加载的时候一个小细节

昨天在处理一处动态加载逻辑的时候遇到了bug……做个最小repro出来记着。

假设有以下目录结构:
│      Alpha.class
│ Beta.class
│ IAlpha.class

├─path2_not_on_classpath
│ Charlie.class

└─test
TestInterfaceClassLoad.class
TestInterfaceClassLoad.java[/code]
其中IAlpha、Alpha、Beta、Charlie几个.class文件是先前由以下源码编译得到的:
[code="java">public interface IAlpha {
Alpha foo(Beta b);
Charlie bar();
}

class Alpha { }
class Beta { }
class Charlie { }

注意我特意把Charlie.class放到了不同的目录下来再现遇到的问题。

然后写以下代码来测试动态加载:
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class TestInterfaceClassLoad {
public static void main(String[] args) throws Exception {
URLClassLoader loader = new URLClassLoader(new URL[] {
new File("../path1_not_on_classpath").toURI().toURL()
}, Thread.currentThread().getContextClassLoader());
Class<?> clzAlpha = Class.forName("IAlpha", true, loader);
System.out.println(clzAlpha);
}
}

编译,在上述目录结构中的test目录不加任何特别参数运行,一切正常。
在上面测试的最后遍历一下动态加载进来的接口上的所有public方法:
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class TestInterfaceClassLoad {
public static void main(String[] args) throws Exception {
URLClassLoader loader = new URLClassLoader(new URL[] {
new File("../path1_not_on_classpath").toURI().toURL()
}, Thread.currentThread().getContextClassLoader());
Class<?> clzAlpha = Class.forName("IAlpha", true, loader);
System.out.println(clzAlpha);
for (Method m : clzAlpha.getMethods()) System.out.println(m);
}
}

这就挂了:
        at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Unknown Source)
at java.lang.Class.privateGetPublicMethods(Unknown Source)
at java.lang.Class.getMethods(Unknown Source)
at TestInterfaceClassLoad.main(TestInterfaceClassLoad.java:13)
Caused by: java.lang.ClassNotFoundException: Charlie
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClassInternal(Unknown Source)
... 5 more[/code]
再挖一挖可以看到真正出错的stack trace:
[code="">java.lang.Class.getDeclaredMethods0(Native Method)
java.lang.Class.privateGetDeclaredMethods(Unknown Source)
java.lang.Class.privateGetPublicMethods(Unknown Source)
java.lang.Class.getMethods(Unknown Source)
TestInterfaceClassLoad.main(TestInterfaceClassLoad.java:13)


把需要的路径加上,使classloader能找到Charlie.class,问题自然消失:
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class TestInterfaceClassLoad {
public static void main(String[] args) throws Exception {
URLClassLoader loader = new URLClassLoader(new URL[] {
new File("../path1_not_on_classpath").toURI().toURL(),
new File("../path2_not_on_classpath").toURI().toURL()
}, Thread.currentThread().getContextClassLoader());
Class<?> clzAlpha = Class.forName("IAlpha", true, loader);
System.out.println(clzAlpha);
for (Method m : clzAlpha.getMethods()) System.out.println(m);
}
}


其实原本遇到的问题跟这个repro有细微不同,但都是在加载接口时漏了一些依赖的类使得加载失败,抛出NoClassDefFoundError。

根据[url=http://java.sun.com/docs/books/jvms/second_edition/html/Concepts.doc.html#19175]JVM规范第二版[/url],class要进入可用状态需要经过loading、linking、initialization三个阶段,其中linking阶段又包括verification、preparation、resolution三步。

通过传入JVM启动参数-XX:+TraceClassLoading,可以看到第一版测试里与我写的代码直接相关的类的输出是:
[Loaded TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loading IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
interface IAlpha[/code]
很明显,在IAlpha达到"loaded"状态的时候,它依赖的其它class还没进入加载范围。

第二版的相关输出是:
[code="">[Loading TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loaded TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loading IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
interface IAlpha
[Loading java.lang.Class$MethodArray from D:\test\jdk1.6.0_14\fastdebug\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from D:\test\jdk1.6.0_14\fastdebug\jre\lib\rt.jar]
[Loading Beta from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded Beta from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loading Alpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded Alpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]

可以看到IAlpha.foo()的参数与返回值的类型对应的class也被加载进来了。IAlpha.bar()参数列表为空,返回值类型为Charlie,如果正常的话应该在这里看到Charlie也被加载进来才对。然而实际看到的是抛异常的stack trace。

第三版的相关输出是:
[Loaded TestInterfaceClassLoad from file:/D:/test/jdk1.6.0_14/test_classload/test/]
[Loading IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded IAlpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
interface IAlpha
[Loading java.lang.Class$MethodArray from D:\test\jdk1.6.0_14\fastdebug\jre\lib\rt.jar]
[Loaded java.lang.Class$MethodArray from D:\test\jdk1.6.0_14\fastdebug\jre\lib\rt.jar]
[Loading Beta from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded Beta from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loading Alpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loaded Alpha from file:/D:/test/jdk1.6.0_14/test_classload/test/../path1_not_on_classpath/]
[Loading Charlie from file:/D:/test/jdk1.6.0_14/test_classload/test/../path2_not_on_classpath/]
[Loaded Charlie from file:/D:/test/jdk1.6.0_14/test_classload/test/../path2_not_on_classpath/]
public abstract Alpha IAlpha.foo(Beta)
public abstract Charlie IAlpha.bar()[/code]
这里就看到一切正常了,在main()里调用System.out.println()要输出的内容都能看到了。

OK,那到底异常是从哪里抛出来的呢?
测试是在Sun的JDK 1.6系上跑的。下面以这个实现为前提来讨论。

先看点小细节。注意在第二和第三版的输出中都可以看到HotSpot加载了java.lang.Class$MethodArray这个嵌套类。它是哪儿来的呢?在出错的stack trace上可以看到有Class.privateGetPublicMethods()方法,看看它的实现:
[code="java">// Returns an array of "root" methods. These Method objects must NOT
// be propagated to the outside world, but must instead be copied
// via ReflectionFactory.copyMethod.
private Method[] privateGetPublicMethods() {
checkInitted();
Method[] res = null;
if (useCaches) {
clearCachesOnClassRedefinition();
if (publicMethods != null) {
res = (Method[]) publicMethods.get();
}
if (res != null) return res;
}

// No cached value available; compute value recursively.
// Start by fetching public declared methods
MethodArray methods = new MethodArray();
{
Method[] tmp = privateGetDeclaredMethods(true);
methods.addAll(tmp);
}
// Now recur over superclass and direct superinterfaces.
// Go over superinterfaces first so we can more easily filter
// out concrete implementations inherited from superclasses at
// the end.
MethodArray inheritedMethods = new MethodArray();
Class[] interfaces = getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
inheritedMethods.addAll(interfaces[i].privateGetPublicMethods());
}
if (!isInterface()) {
Class c = getSuperclass();
if (c != null) {
MethodArray supers = new MethodArray();
supers.addAll(c.privateGetPublicMethods());
// Filter out concrete implementations of any
// interface methods
for (int i = 0; i < supers.length(); i++) {
Method m = supers.get(i);
if (m != null && !Modifier.isAbstract(m.getModifiers())) {
inheritedMethods.removeByNameAndSignature(m);
}
}
// Insert superclass's inherited methods before
// superinterfaces' to satisfy getMethod's search
// order
supers.addAll(inheritedMethods);
inheritedMethods = supers;
}
}
// Filter out all local methods from inherited ones
for (int i = 0; i < methods.length(); i++) {
Method m = methods.get(i);
inheritedMethods.removeByNameAndSignature(m);
}
methods.addAllIfNotPresent(inheritedMethods);
methods.compactAndTrim();
res = methods.getArray();
if (useCaches) {
publicMethods = new SoftReference(res);
}
return res;
}

这个方法创建了MethodArray的实例,顺带就让HotSpot把该类给加载了进来。

现在来看看上面例子里对抛出异常有直接贡献的方法调用树:
(同名重载相互调用的只写一个,缩进在同一层的说明是被同一个方法调用的。
相关方法上面的文件路径是实际代码在JDK源码中的位置。)
[code=""]j2se/src/share/classes/java/lang/Class.java
java.lang.Class.getMethods()
java.lang.Class.privateGetPublicMethods()
java.lang.Class.privateGetDeclaredMethods()
java.lang.Class.getDeclaredMethods0()

hotspot/src/share/vm/prims/jvm.cpp
JVM_GetClassDeclaredMethods()

hotspot/src/share/vm/runtime/reflection.cpp
Reflection::new_method()
get_parameter_types() <= 参数与返回值类型都包含在内
get_mirror_from_signature()

hotspot/src/share/vm/classfile/systemDictionary.cpp
SystemDictionary::resolve_or_fail()
SystemDictionary::resolve_or_null()
SystemDictionary::resolve_instance_class_or_null()
SystemDictionary::load_instance_class()

hotspot/src/share/vm/runtime/javaCalls.cpp
JavaCalls::call_virtual() <= 对ClassLoader.loadClass()的虚方法调用;
由于URLClassLoader没有覆写loadClass(),
这里实际调用的是ClassLoader的版本

j2se/src/share/classes/java/lang/ClassLoader.java
java.lang.ClassLoader.loadClass()

j2se/src/share/classes/java/net/URLClassLoader.java
java.net.URLClassLoader.findClass() <= 找不到,抛ClassNotFoundException

SystemDictionary::handle_resolution_exception() <= 换成NoClassDefFoundError再抛出来[/code]

看到这个流程,可以发现HotSpot VM中加载类是lazy的,一个class被加载后并不马上解析(resolve)所有依赖项。在调用Class.getMethods()之前,IAlpha.class已经被正确加载并初始化了。等到实际要去获取IAlpha上的方法信息时,方法上依赖的类型才被解析,解析的过程中又会尝试加载依赖的类型。
回头看JVM规范里的一段描述:
[quote="Java Virtual Machine Specification, 2nd, 2.17.3"]The Java programming language allows an implementation flexibility as to when linking activities (and, because of recursion, loading) take place, provided that the semantics of the language are respected, that a class or interface is completely verified and prepared before it is initialized, and that errors detected during linkage are thrown at a point in the program where some action is taken by the program that might require linkage to the class or interface involved in the error.

For example, an implementation may choose to resolve each symbolic reference in a class or interface individually, only when it is used (lazy or late resolution), or to resolve them all at once, for example, while the class is being verified (static resolution). This means that the resolution process may continue, in some implementations, after a class or interface has been initialized.[/quote]
第二段里举个这么明确的例子,自然不是没有道理的……

记完。留着文件名和方法名以后回头再找的时候方便。
话说在寻找相关方法的时候,发现instanceKlass::link_class_impl()有调用初始化vtable(虚方法表)和itable(接口方法表)的逻辑。再看JVM规范里的一段话:
[quote="Java Virtual Machine Specification, 2nd, 2.17.3"]Implementations of the Java virtual machine may precompute additional data structures at preparation time in order to make later operations on a class or interface more efficient. One particularly useful data structure is a "method table" or other data structure that allows any method to be invoked on instances of a class without requiring a search of superclasses at invocation time.[/quote]
不由得一笑 ^_^
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值