文章首发于【Java天堂】,跟随我探索Java进阶之路!
类与类加载器
类是由它的类加载器加载进虚拟机中的,在同一个Java虚拟机中,对于同一个Class文件,如果采用不同的类加载器,得到的是不相等的类,对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
public class ClassLoadTest {
public static void main(String[] args) throws Exception {
ClassLoader myClassLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String className = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(className);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
Object obj = myClassLoader.loadClass("org.example.ClassLoadTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof org.example.ClassLoadTest);
}
}
上面的例子中,我自己创建了一个简单的类加载器myClassLoad,然后我使用这个类加载器加载ClassLoadTest
,最后使用instanceof来检测得到false
class org.example.ClassLoadTest
false
这是因为Java虚拟机中存在两个ClassLoadTest
类,一个是由虚拟机的应用程序类加载器加载的,另外一个是由myClassLoad
加载进来的。虽然他们都来自于同一个Class文件,但在Java虚拟机中是两个独立的类
双亲委派模型
站在Java虚拟机的视角,类加载器分为:
- 启动类加载器(Bootstrap ClassLoader),由C++语言实现,是虚拟机自身的一部分
- 其他所有类的加载器,由Java语言实现,独立存在于Java虚拟机外部
站在开发人员的视角,类加载器分为:
- 启动类加载器(Bootstrap ClassLoader),负责加载存放在 \lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中
- 扩展类加载器(Extension Class Loader),负责加载 \lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库
- 应用程序类加载器(Application Class Loader),负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
如上图所示,除了用户自定义类加载器外,默认是由三种类加载器配合完成类的加载。
上图的关系称为“双亲委派模型”,双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载
双亲委派模型的好外
避免类的重复加载
: 如果每个类加载器都独立加载类,那么相同的类可能会被多次加载,导致内存中存在多个相同类的副本,这显然是不必要的资源浪费。双亲委派模型确保了类的唯一性,一旦某个类加载器的父加载器已经加载了一个类,子加载器就不会再加载同一个类保证核心类库的一致性
: Java的核心类库(如java.lang.String)位于Bootstrap ClassLoader中。如果允许自底向上的加载方式,用户定义的类加载器可能会尝试加载这些核心类,这可能导致安全问题和版本冲突。双亲委派模型确保了这些基础类只由启动类加载器加载,保证了系统类的统一性和安全性隔离应用程序类和系统类
: 不同的应用程序可能定义了相同包和类名的类,但它们应该互不影响。双亲委派模型使得系统类(如java.*包中的类)与应用程序类(用户自定义的类)得以区分,防止应用程序随意覆盖系统类维护Java的类加载生态
: 这种机制使得第三方类库的加载更加有序,开发者可以自定义类加载器来加载特定的类,而不需要担心与系统类发生冲突,因为系统类总是会被先加载
破坏双亲委派模型的场景
双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。在Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况
场景1:发生在双亲委派模型出现之前
由于双亲委派模型在JDK 1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK 1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码
场景2:模型自身的缺陷导致的
JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?
为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器
有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情
虽然上述两个场景破坏了双亲委派模型,但只要有明确的目的和充分的理由,突破旧有原则也算是一种创新
本文由博客一文多发平台 OpenWrite 发布!