类加载器分类
类加载器主要有以下几种:
类加载器 | 加载范围 |
---|---|
启动类加载器(Bootstrap ClassLoader) | 1. 加载%JAVA_HOME%/lib 下的jar包2. _Xbootclasspath参数指定路径的jar(不常用) 注意:由C++实现,不是ClassLoader的子类,只加载虚拟机认可的类,手动把包放到lib目录下是不管用的 |
扩展类加载器(Extension ClassLoader) | 1. 加载%JAVA_HOME%/lib/ext 下的jar包2. java.ext.dirs系统变量指定的路径的jar |
应用类加载器(App ClassLoader) | 用户classpath指定的类,也就是我们日常开发的类 |
自定义类加载器(User ClassLoader) | 上面的类加载器无法满足需求时,就自定义类加载器 |
确定两个类是否相同
在确定两个类是否相同时,有一个大坑需要我们注意。
每一个类加载器都有各自的类名称空间,例如我们编写的某个类A,正常情况会被App ClassLoader
加载,此时如果我们自定义一个类加载器,也去加载A的class文件,也能加载成功。
如果用instanceof
去判断这两个类是否是同一个类,你将得到一个false的结果,即使这两个类其实是一模一样的。
也就是说,判断两个类是否是同一个类,前提是这两个类必须被同一个类加载进行加载,否则没有意义。
双亲委派机制
从JDK1.2开始,引入了双亲委派机制,这是JAVA设计者推荐给开发者的一种类加载器的实现方式。
双亲委派机制的大致过程是:
- 当一个类加载器要加载类时,不会直接进行加载,而是把这个请求交给它的父类加载器。同样的,父类加载器同样也会将请求往上传递,层层传递直到到达顶层。
- 顶层类加载器会尝试加载这个类,如果成功,则结束,如果失败,则让下层类加载器尝试加载,如果再失败,继续层层往下传递,直到没有成功或者没有下层
整个过程大致如下图所示:
为什么要用这种机制去加载一个类呢?
例如类java.lang.Object
,是在rt.jar包中,使用双亲委派机制,可以确保Object类一定会被最顶层的启动类加载器加载,因为下层即使收到加载Object类的请求,也会把请求往上委托,最终被启动类加载器加载完成。
否则程序中就可能出现多个java.lang.Object
,将一片混乱
破坏双亲委派机制
双亲委派机制并不是一种强制性的模型要求,因此在JDK发展历史上出现了三次较大规模的破坏
第一次破坏
双亲委派机制是在JDK1.2开始出现,其核心代码在ClassLoader.loadClass()
方法中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 查看该类是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 父加载器存在,则让父加载器处理
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 加载类
c = findClass(name);
// this is the defining class loader; record the stats
// 删除部分代码
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
在JDK1.2之前,就已经有自定义类加载器的能力,开发者们继承ClassLoader
类并重写loadClass()
方法来自定义类加载器。
而双亲委派的核心代码都在loadClass()
方法中,一旦被开发者重写,双亲委派机制就被破坏了。
因此在双亲委派机制出现之后,增加了findClass()
方法,建议开发者们在自定义类加载时,选择重写findClass()
而不是loadClass()
,避免对双亲委派进行破坏。
因此,可以说双亲委派历史上第一次较大规模的破坏,发生在双亲委派出现之前
第二次破坏
第二次破坏的出现,是由于双亲委派机制的缺陷导致。
有些情况下,顶层的BootstrapClassLoader
,需要使用到classpath下的类,也就是需要调用AppClassLoader
去加载类,也就破坏了双亲委派模型。
示意图如下:
什么时候会出现这种情况呢?
以JDBC为例,rt.jar
中有一个接口java.sql.Driver
,该接口由BootstrapClassLoader
进行加载。
但SQL的驱动的实现类是由服务提供商提供(如MySQL服务商),而提供商提供的实现类com.mysql.jdbc.Driver
是在classpath下的,由AppClassLoader
进行加载。
Java设计了SPI(Service Provider Interface)机制,约定在jar包的META-INF/services/
目录下的文本文件中,存放了服务实现类的全限定类名,也就是com.mysql.jdbc.Driver
。
如下图:
有了SPI机制之后,BootstrapClassLoader
就能够通过AppClassLoader
加载com.mysql.jdbc.Driver
类,但这种行为破坏了双亲委派模型。
那么BootstrapClassLoader
是如何拿到AppClassLoader
的呢?
其实很简单,Java启动时,会将AppClassLoader
放到当前线程的上下文中,BootstrapClassLoader
就可以直接从线程上下文中拿到了。
代码如下:
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader", e);
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
第三次破坏
第三次破坏是因为开发对“动态性”的追求,比如热部署。简单来说就是希望JVM能够再不重启的前提下,动态更新代码。
OSGi(Open Service Gateway Initiative) 技术是 Java 动态化模块化系统的一系列规范,该技术自定了类加载器,有着自己的类加载机制,它是一种网状结构的类加载方式,破坏了双亲委派机制,可以实现热部署。
鼎鼎大名的Eclipse就是基于OSGi开发的