类加载器
是什么
类加载的过程中,“通过一个类的全限定名来获取描述此类的二级制字节流”这个动作放到JVM虚拟机外部去实现,由用户程序自己去选择如何加载所需的类;实现此方法的类便是类加载器;
JVM中是如何判断两个Class是否相等的?
类加载器将类元数据装载至JVM。
对任意的一个类来说,都需要这个类本身和其类加载其共同确认在JVM中的唯一性;
比如,在一个程序中,存在类加载器ALoader与BLoader,同时存在唯一一个类,top.bubuzi.App;情况如下
1 | Class<?> ALoadClass ALoader.loadClass('top.bubuzi.App'); |
可以看见,及时是来着同源的类,在不同加载器下,类元信息并不相等;这就实现了加载器隔离;
双亲委派机制
在JAVA中提供了三种类型的系统类加载器
启动类加载器(Bootstrap ClassLoader)
由C++底层实现,属于JVM核心的一部分,用于加载**${JAVA_HOME}/lib**目录下的文件,或者被-Xbootclasspath参数所指定路径中的文件,该加载器只能加载特定名称的文件,如(rt.jar),并非目录下全部文件;该加载器无法被用户直接引用;
扩展类加载器(Extension ClassLoader)
由sun.misc.Launcher.ExtClassLoader实现,它负责加载**${JAVA_HOME}/lib/ext**目录中的文件,或被java.ext.dirs系统变量所指定的路径中的类,开发者可以直接使用扩展类加载器;
JDK1.8后ExtClassLoader被废弃,使用PlatformClassLoader
应用程序类加载器(Application ClassLoader)
也称系统类加载器,由sun.misc.Launcher.AppClassLoader实现。负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器。一般程序默认使用该加载器
应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。加载流程如下图所示:
在JDK1.8版本以上,若代码如下:
1 | public static void main(String[] args) { |
会出现打印:
1 | jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc |
其中并没打印出PlatformClassLoader的父加载器启动类加载器(Bootstrap ClassLoader),因为启动类加载器属于JVM的一部分,是无法被用户直接引用到的;
Java类加载层级称为:双亲委派模型。它的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
为什么?
都知道java.lang.Object是java中所有类的父类,它存放在rt.jar之中,按照双亲委派模型,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。试想,如果没有使用双亲委派模型,由各个类加载器自行去加载,显然,这就存在很大风险,用户完全可以恶意编写一个java.lang.Object类,然后放到ClassPath下,那系统就会出现多个Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
源码分析
以下代码取自 JDK11 中 java.lang.ClassLoader 类
1 | // 由JVM实现的启动类加载器 |
总得来说,类加载过程分为三个阶段:
- 查找对应类是否加载,若加载直接返回类元;
- 若未加载,递归调用父类加载器的加载函数,直到父类为空时,再调用启动类加载器去加载;
- 若上面均未加载到类元,则使用加载器本身的加载函数去加载类元信息;
打破双亲委派模型
核心实现:线程上下文类加载器(Thread Context ClassLoader)
这个类加载器可以通过java.lang.Thread类中的setContextClassLoader方法进行设置,若创建线程时还未设置,它将会从父线程中继承一个类加载器,如果全局均为配置,那么默认就是应用程序加载器(Application ClassLoader);