设为首页 加入收藏

TOP

不是单例的单例——巧用ClassLoader(四)
2023-07-25 21:25:11 】 浏览:56
Tags:单例的 单例 巧用 ClassLoader
System.out.println(obj.getClass().getClassLoader()); System.out.println(com.ppphuang.demo.classloader.ClassLoaderTest.class.getClassLoader()); System.out.println(obj instanceof ClassLoaderTest); } } //输出如下: //com.ppphuang.demo.classloader.ClassLoaderTest$1@7a07c5b4 //sun.misc.Launcher$AppClassLoader@18b4aac2 //false

上述代码自定义了一个类加载器 myClassLoader,用 myClassLoader 加载的 ClassLoaderTest 类实例化出的对象与 AppClassLoader 加载的 ClassLoaderTest 类做 instanceof 运算,最终输出的接口是 false。由此可以判断出不同加载器加载同一个类,这两个类也是不相同的。

因为不同类加载器的加载的类是不同的,所以我们可以在一个 JVM 里通过自定义类加载器来将一个单例类实例化两次。

ClassLoader 传递性

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?

虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法写在哪个类,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全权负责,它就是 AppClassLoader。

ClassLoaderTest classLoaderTest = new ClassLoaderTest();
System.out.println(classLoaderTest.getClass().getClassLoader());
//sun.misc.Launcher$AppClassLoader@18b4aac2

如果我们使用一个自定义类加载器加载一个类,那么这个类里依赖的类也会由这个类加载来负责加载:

Object obj = myClassLoader.loadClass("com.ppphuang.demo.classloader.ClassLoaderTest").newInstance();

因为类加载器的传递性,依赖类的加载器也会使用当前类的加载器,当我们利用自定义类加载器来将一个单例类实例化两次的时候,能保证两个单例对象是完全隔离。

双亲委派模型

当一个类加载器需要加载一个类时,自己并不会立即去加载,而是首先委派给父类加载器去加载,父类加载器加载不了再给父类的父类去加载,一层一层向上委托,直到顶层加载器(BootstrapClassLoader),如果父类加载器无法加载那么类加器才会自己去加载。

findLoadedClass

当一个类被父加载器加载了,子加载器再次加载这个类的时候,还需要向父加载器委托吗?

我们先把问题细化一下:

  1. AClassLoader 的父加载器为 BClassLoader,BClassLoader 的父加载器为 CClassLoader,当 AClassLoader 调用 loadClass() 加载类,并最终由 CClassLoader 加载的类,到底算谁加载的?

  2. 后续 AClassLoader 再加载相同类时,是否能直接从 AClassLoader 的 findLoadedClass0() 中找到该类并返回,还是说再走一次双亲委派最终从 CClassLoader 的 findLoadedClass0() 中找到该类并返回?

JVM 里有一个数据结构叫做 SystemDictonary,这个结构主要就是用来检索我们常说的类信息,其实也就是 private native final Class<?> findLoadedClass0(String name) 方法的逻辑。

这些类信息对应的结构是 klass,对 SystemDictonary 的理解,可以理解为一个哈希表,key 是类加载器对象 + 类的名字,value是指向 klass 的地址。当我们任意一个类加载器去正常加载类的时候,就会到这个 SystemDictonary 中去查找,看是否有这么一个 klass 可以返回,如果有就返回它,否则就会去创建一个新的并放到结构里。

这里面还涉及两个小概念,初始类加载器、定义类加载器。

上述类加载问题中,AClassLoader 加载类的时候会委托给 BClassLoader 来加载,BClassLoader 加载类的时候会委托给 CClassLoader 来加载,当 AClassLoader 调用 loadClass() 加载类,并最终由 CClassLoader 加载,那么我们称 CClassLoader 为该类的定义类加载器,AClassLoader 和 BClassLoader 为该类的初始类加载器。在这个过程中,AClassLoader、BClassLoader 和 CClassLoader 都会在 SystemDictonary 生成记录。那么后续 C 的子加载器(AClassLoader 和 BClassLoader)加载相同类时,就能在自己 findLoadedClass0() 中找到该类,不必再向上委托。

双亲委派的目的

  1. 防止重复加载类。在 JVM 中,要唯一确定一个对象,是由类加载器和全类名两者共同确定的,考虑到各层级的类加载器之间仍然由重叠的类资源加载区域,通过向上抛的方式可以避免一个类被多个不同的类加载器加载,从而形成重复加载。

  2. 防止系统 API 被篡改。例如读者定义了一个名为 java.lang.Integer 的类,而该类在核心库中也存在,借用双亲委派的机制,我们就能有效防止该自定义的同名类被加载,从而保护了平台的安全性。

JDK 1.2 之后引入双亲委派的方式来实现类加载器的层次调用,以尽可能保证 JDK 的系统 API 不会被用户定义的类加载器所破坏,但一些使用场景会打破这个惯例来实现必要的功能。

破坏双亲委派模型

Thread Context ClassLoader

在介绍破坏双亲委派模型之前,我们先了解一下 Thread Context ClassLoader(线程上下文类加载器)。

JVM 中经常需要调用由其他厂商实现并部署在应用程序的 ClassPath 下的 JNDI 服务提供者接口 (Servicepovider iotertace, SPD) 的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?
为了解决这个困境,Java 的设计团队只好引入了一个不太优雅的设计:线程上下文类加裁器 ( Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用

首页 上一页 1 2 3 4 下一页 尾页 4/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Springboot通过谷歌Kaptcha 组件.. 下一篇SpringBoot 使用 Sa-Token 完成注..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目