108. 你真的知道 Java 类是如何被加载的吗?(二)

简介: 108. 你真的知道 Java 类是如何被加载的吗?(二)

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的加载机制,才能更好的理解类似双亲委派这样的机制,否则只死记硬背一些空洞的理论,是无法起到由内而外的理解的。

目录
相关文章
|
5月前
|
Java 编译器 API
Java 密封类:精细化控制继承关系
Java 密封类:精细化控制继承关系
385 83
|
3月前
|
安全 Java 数据建模
Java记录类:简化数据载体的新选择
Java记录类:简化数据载体的新选择
267 101
|
3月前
|
安全 Java 开发者
Java记录类:简化数据载体的新方式
Java记录类:简化数据载体的新方式
304 100
|
6月前
|
IDE Java 数据挖掘
Java 基础类从入门到精通实操指南
这份指南专注于**Java 17+**的新特性和基础类库的现代化用法,涵盖开发环境配置、数据类型增强(如文本块)、字符串与集合处理进阶、异常改进(如密封类)、IO操作及实战案例。通过具体代码示例,如CSV数据分析工具,帮助开发者掌握高效编程技巧。同时提供性能优化建议和常用第三方库推荐,适合从入门到精通的Java学习者。资源链接:[点此下载](https://pan.quark.cn/s/14fcf913bae6)。
254 36
|
4月前
|
安全 IDE Java
Java记录类型(Record):简化数据载体类
Java记录类型(Record):简化数据载体类
447 143
|
2月前
|
存储 Java 索引
用Java语言实现一个自定义的ArrayList类
自定义MyArrayList类模拟Java ArrayList核心功能,支持泛型、动态扩容(1.5倍)、增删改查及越界检查,底层用Object数组实现,适合学习动态数组原理。
123 4
|
2月前
|
IDE JavaScript Java
在Java 11中,如何处理被弃用的类或接口?
在Java 11中,如何处理被弃用的类或接口?
214 5
|
2月前
|
JSON 网络协议 安全
【Java】(10)进程与线程的关系、Tread类;讲解基本线程安全、网络编程内容;JSON序列化与反序列化
几乎所有的操作系统都支持进程的概念,进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位一般而言,进程包含如下三个特征。独立性动态性并发性。
199 1
|
2月前
|
Java Go 开发工具
【Java】(8)正则表达式的使用与常用类分享
正则表达式定义了字符串的模式。正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别。
255 1
|
2月前
|
存储 Java 程序员
【Java】(6)全方面带你了解Java里的日期与时间内容,介绍 Calendar、GregorianCalendar、Date类
java.util 包提供了 Date 类来封装当前的日期和时间。Date 类提供两个构造函数来实例化 Date 对象。第一个构造函数使用当前日期和时间来初始化对象。Date( )第二个构造函数接收一个参数,该参数是从1970年1月1日起的毫秒数。
205 0