最近在学习Java虚拟机的基础知识,其中类加载器是非常重要一部分,也是JVM启动后做的第一件事就是加载类。
双亲委派
本文主要整理了类加载器用途 ,类加载过程 和类加载器实现 三部分的内容
说明:文中的源码都是JDK1.8版本的
一 类加载器概述
java类的加载是由虚拟机来完成的,虚拟机把描述类的Class文件加载到内存,并对数据进行校验,解析和初始化,最终形成能被java虚拟机直接使用的java类型,这就是虚拟机的类加载机制.JVM中用来完成上述功能的具体实现就是类加载器.类加载器读取.class字节码文件将其转换成java.lang.Class类的一个实例.每个实例用来表示一个java类.通过该实例的newInstance()方法可以创建出一个该类的对象.
二 类加载过程
2.1 类加载过程
JVM的类加载过程一共有三个步骤:装载(Load),链接(Link)和初始化(Initialize)三个步骤。过程如下图所示:
装载:查找并加载类的二进制数据,也就是Class文件。在内存中生成代表此类的java.lang.Class对象,作为该类访问入口
链接:把类的二进制数据合并到JRE中
- 验证:确保被加载类的正确性;避免万一有高手自己写一个class文件,让JVM加载并运行,用于恶意用途
- 准备:正式为类变量分配内存并设置变量的初始值.(仅包含类变量,不包含实例变量)
- 解析:虚拟机将常量池中的符号引用替换为直接引用,解析动作主要针对类或接口,字段,类方法,方法类型等等
初始化:该阶段,才真正意义上的开始执行类中定义的java程序代码.该阶段会执行类构造器,对类的静态变量,静态代码块执行初始化操作
2.2 类的生命周期
一个类的生命周期如下图所示:
三 类加载器分类
在JVM当中预定义了三种类型的类加载器:启动类加载器,扩展类加载器,系统类加载器。每个加载器其实就是个类的对象。
另外还有线程上下文类加载器和用户自定义类加载器。
3.1 启动类加载器
启动类加载器(BootstrapClassLoader)引导类装入器是用本地代码实现的类装入器,它负责将 /lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
启动类加载器是最低层的加载器,它是由C++编写的,因为加载器是一个类,也需要通过类加载器加载,启动类加载器就能完成这个功能。
3.2 扩展类加载器
扩展类加载器(ExtensionClassLoader)是由ExtClassLoader类实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
3.3 系统类加载器
系统类加载器(SystemClassLoader)是由AppClassLoader类实现。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。
四 双亲委派机制
4.1 定义
JVM加载类时默认采用双亲委派 机制。通俗的讲就是,当某个类加载器收到加载类的请求时,首先会将加载任务任务委托给加载器的父类,然后父类再委托给父类的父类,以此类推。如果父类成功可以成功加载,就返回成功,如果父类加载器无法完成任务时,才会自己加载。
下图展示了双亲委派机制的运行逻辑。
4.2 实现
ExtClassLoader和AppClassLoader都是继承于java.lang.ClassLoader类,ClassLoader类中有个loadClass方法来实现双亲委派,ExtClassLoader和AppClassLoader都没有重写这个方法(JDK1.8)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先获得当前类的类加载器,返回加载器或null
Class<?> c = findLoadedClass(name);
if (c == null) {
// 如果当前类没有被加载,就委托给父类加载
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
// 如果父类加载器不存在,就检查是否有启动类加载器加载的类
// 通过调用本地方法native findBootstrapClass0(String name)
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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
下面我们来看看ExtClassLoader和AppClassLoader的父类加载器是谁。
public static void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent());
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
输出结果:
sun.misc.Launcher$AppClassLoader@6d06d69c
sun.misc.Launcher$ExtClassLoader@70dea4e
null
说明AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是空,再结合上面loadClass()方法的源码,当父加载器为null时调用本地方法native findBootstrapClass0(String name) ,也就是调用BootstrapClassLoader加载器
五 类的动态扩展方式
Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。
运行时动态扩展java应用程序有如下两个途径:
5.1 调用java.lang.Class.forName( )
Class类的forName(xxx)方法手动的加载xxx类,并返回xxx这个类对应的Class类的对象。
5.2 用户自定义类加载器
通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。
一般用户自定义类加载器的工作流程:
①首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;
②委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;
③调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转而抛异常,终止加载过程(注意:这里的异常种类不止一种)。
整个加载类的过程如下图:
六 常见问题说明
1 由不同的类加载器加载的指定类还是相同的类型吗?
在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。
所以说,不同加载器加载的指定类不是相同的类型
2 对于自定义类加载器,如果没有设定父加载器,那父加载器是谁
在不指定父类加载器的情况下,默认采用系统类加载器(SystemClassLoader)。