108. 你真的知道 Java 类是如何被加载的吗?(二)
三:再谈双亲委派
如果你耐心的看完了上面的源码分析,你一定对 “不同ClassLoader加载的类是互相隔离的” 这句话的理解又上了一个台阶。
我们总结下:每个ClassLoader都有一个 Dictionary 用来保存它所加载的InstanceKlass信息。并且,每个 ClassLoader 通过锁,保证了对于同一个Class,它只会注册一份 InstanceKlass 到自己的 Dictionary 。
正式由于上面这些原因,如果所有的 ClassLoader 都由自己去加载 Class 文件,就会导致对于同一个Class文件,存在多份InstanceKlass,所以即使是同一个Class文件,不同InstanceKlasss 衍生出来的实例类型也是不一样的。
举个栗子,我们自定义一个 ClassLoader,用来打破双亲委派模型:
public class CustomClassloader extends URLClassLoader { public CustomClassloader(URL[] urls) { super(urls); } @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.startsWith("com.wangxiandeng")) { return findClass(name); } return super.loadClass(name, resolve); } }
再尝试加载Studen类,并实例化:
public class Test { public static void main(String[] args) throws Exception { URL url[] = new URL[1]; url[0] = Thread.currentThread().getContextClassLoader().getResource(""); CustomClassloader customClassloader = new CustomClassloader(url); Class clazz = customClassloader.loadClass("com.wangxiandeng.Student"); Student student = (Student) clazz.newInstance(); } }
运行后便会抛出类型强转异常:
Exception in thread "main" java.lang.ClassCastException: com.wangxiandeng.Student cannot be cast to com.wangxiandeng.Student
为什么呢?
因为实例化的Student对象所属的 InstanceKlass 是由CustomClassLoader加载生成的,而我们要强转的类型Student.Class 对应的 InstanceKlass 是由系统默认的ClassLoader生成的,所以本质上它们就是两个毫无关联的InstanceKlass,当然不能强转。
有同学问到:为什么“强转的类型Student.Class 对应的 InstanceKlass 是由系统默认的ClassLoader生成的”?
其实很简单,我们反编译下字节码:
public static void main(java.lang.String[]) throws java.lang.Exception; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=5, args_size=1 0: iconst_1 1: anewarray #2 // class java/net/URL 4: astore_1 5: aload_1 6: iconst_0 7: invokestatic #3 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread; 10: invokevirtual #4 // Method java/lang/Thread.getContextClassLoader:()Ljava/lang/ClassLoader; 13: ldc #5 // String 15: invokevirtual #6 // Method java/lang/ClassLoader.getResource:(Ljava/lang/String;)Ljava/net/URL; 18: aastore 19: new #7 // class com/wangxiandeng/classloader/CustomClassloader 22: dup 23: aload_1 24: invokespecial #8 // Method com/wangxiandeng/classloader/CustomClassloader."<init>":([Ljava/net/URL;)V 27: astore_2 28: aload_2 29: ldc #9 // String com.wangxiandeng.Student 31: invokevirtual #10 // Method com/wangxiandeng/classloader/CustomClassloader.loadClass:(Ljava/lang/String;)Ljava/lang/Class; 34: astore_3 35: aload_3 36: invokevirtual #11 // Method java/lang/Class.newInstance:()Ljava/lang/Object; 39: checkcast #12 // class com/wangxiandeng/Student 42: astore 4 44: return
可以看到在利用加载的Class初始化实例后,调用了 checkcast 进行类型转化,checkcast 后的操作数 #12 即为Student这个类在常量池中的索引:#12 = Class #52 // com/wangxiandeng/Student
下面我们可以看看 checkcast 在HotSpot中的实现。
HotSpot 目前有三种字节码执行引擎,目前采用的是模板解释器,早期的HotSpot采用的是字节码解释器。模板解释器对于指令的执行都是用汇编写的,而字节码解释器采用的C++进行的翻译,为了看起来比较舒服,我们就不看汇编了,直接看字节码解释器就行了。如果你的汇编功底很好,当然也可以直接看模板解释器。
废话不多说,我们来看看字节码解释器对于checkcast的实现,代码在 bytecodeInterpreter.cpp 中
CASE(_checkcast): if (STACK_OBJECT(-1) != NULL) { VERIFY_OOP(STACK_OBJECT(-1)); // 拿到 checkcast 指令后的操作数,本例子中即 Student.Class 在常量池中的索引:#12 u2 index = Bytes::get_Java_u2(pc+1); // 如果常量池还没有解析,先进行解析,即将常量池中的符号引用替换成直接引用, //此时就会触发Student.Class 的加载 if (METHOD->constants()->tag_at(index).is_unresolved_klass()) { CALL_VM(InterpreterRuntime::quicken_io_cc(THREAD), handle_exception); } // 获取上一步系统加载的Student.Class 对应的 InstanceKlass Klass* klassOf = (Klass*) METHOD->constants()->resolved_klass_at(index); // 获取要强转的对象的实际类型,即我们自己手动加载的Student.Class 对应的 InstanceKlass Klass* objKlass = STACK_OBJECT(-1)->klass(); // ebx // 现在就比较简单了,直接看看上面的两个InstanceKlass指针内容是否相同 // 不同的情况下则判断是否存在继承关系 if (objKlass != klassOf && !objKlass->is_subtype_of(klassOf)) { // Decrement counter at checkcast. BI_PROFILE_SUBTYPECHECK_FAILED(objKlass); ResourceMark rm(THREAD); char* message = SharedRuntime::generate_class_cast_message( objKlass, klassOf); VM_JAVA_ERROR(vmSymbols::java_lang_ClassCastException(), message, note_classCheck_trap); } // Profile checkcast with null_seen and receiver. BI_PROFILE_UPDATE_CHECKCAST(/*null_seen=*/false, objKlass); } else { // Profile checkcast with null_seen and receiver. BI_PROFILE_UPDATE_CHECKCAST(/*null_seen=*/true, NULL); }
通过对上面代码的分析,我相信大家已经理解了 “强转的类型Student.Class 对应的 InstanceKlass 是由系统默认的ClassLoader生成的” 这句话了。
双亲委派的好处是尽量保证了同一个Class文件只会生成一个InstanceKlass,但是某些情况,我们就不得不去打破双亲委派了,比如我们想实现Class隔离的时候。
回复下箫陌同学的问题:
// 如果常量池还没有解析,先进行解析,即将常量池中的符号引用替换成直接引用, //此时就会触发Student.Class 的加载 if (METHOD->constants()->tag_at(index).is_unresolved_klass()) { CALL_VM(InterpreterRuntime::quicken_io_cc(THREAD), handle_exception); }
请问,为何这里会重新加载Student.Class?jvm是不是有自己的class加载链路,然后系统循着链路去查找class是否已经被加载?那该怎么把自定义的CustomClassloader 加到这个查询链路中去呢?
第一种方法:设置启动参数 java -Djava.system.class.loader
第二种方法:利用Thread.setContextClassLoder
这里就有点技巧了,看下代码:
public class Test { public static void main(String[] args) throws Exception { URL url[] = new URL[1]; url[0] = Thread.currentThread().getContextClassLoader().getResource(""); final CustomClassloader customClassloader = new CustomClassloader(url); Thread.currentThread().setContextClassLoader(customClassloader); Class clazz = customClassloader.loadClass("com.wangxiandeng.ClassTest"); Object object = clazz.newInstance(); Method method = clazz.getDeclaredMethod("test"); method.invoke(object); } } public class ClassTest { public void test() throws Exception{ Class clazz = Thread.currentThread().getContextClassLoader().loadClass("com.wangxiandeng.Student"); Student student = (Student) clazz.newInstance(); System.out.print(student.getClass().getClassLoader()); } }
要注意的是在设置线程的ClassLoader后,并不是直接调用 new ClassTest().test()。为什么呢?因为直接强引用的话,会在解析Test.Class的常量池时,利用系统默认的ClassLoader加载了ClassTest,从而又触发了ClassTest.Class的解析。为了避免这种情况的发生,这里利用CustomClassLoader去加载ClassTest.Class,再利用反射机制调用test(),此时在解析ClassTest.Class的常量池时,就会利用CustomClassLoader去加载Class常量池项,也就不会发生异常了。
四:总结
写完这篇文章,手也不痒了,甚爽!这篇文章从双亲委派讲到了Class文件的加载,最后又绕回到双亲委派,看似有点绕,其实只有理解了Class的加载机制,才能更好的理解类似双亲委派这样的机制,否则只死记硬背一些空洞的理论,是无法起到由内而外的理解的。