虚拟机设计团队把类加载阶段中的 “通过一个类的全限定名来获取描述此类的二进制字节流” 这个动作放到虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块称为 “类加载器”。
一. 类与类加载器
类加载器虽然只用于实现类的加载动作,但是在Java程序中起到的作用远远不限于类加载阶段。对于任何一个类,都需要由加载它的类加载器本身和这个类本身来确立其在Java虚拟机中的唯一性。比较两个类是否 相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。相等是指,代表类的Class
对象的equals()
、isAssignableFrom()
、isInstance()
方法的返回结果,以及instanceof
关键字做对象所属关系判定。
二. 双亲委派模型
从java虚拟机的角度来讲,只存在以下两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader),这个类加载器用C++实现,是虚拟机自身的一部分。
- 所有其他类的加载器,这些类由Java实现,独立于虚拟机外部,并且全都继承自抽象类
java.lang.ClassLoader
。
还可以细分为以下三种:
- 启动类加载器(Bootstrap ClassLoader) 此类加载器负责将存放在
目录中的,或者被\lib -Xbootclasspath
参数所指定的路径中的,并且是 虚拟机识别 的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null代替即可。 - 扩展类加载器(Extension ClassLoader) 这个类加载器是由
ExtClassLoader(sun.misc.Launcher$ExtClassLoader)
实现的。它负责将
或者被/lib/ext java.ext.dir
系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 - 应用程序类加载器(Application ClassLoader) 这个类加载器是由
AppClassLoader(sun.misc.Launcher$AppClassLoader)
实现的。由于这个类加载器是ClassLoader
中的getSystemClassLoader()
方法的返回值,因此一般称为 系统类加载器。它负责加载用户类路径(ClassPath)
上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中 默认的类加载器 。
类在加载器之间存在层次关系,也成为类加载器的 双亲委派模型(Parents Delgation Model),如下图所示。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般通过 组合(Composition) 关系来实现,而不是通过继承(Inheritance)的关系实现。
工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归,因此 所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。
优点
使用双亲委派模型来组织类加载器之间的关系,使得Java类随着它的类加载器一起具备了一种 带有优先级的层次关系。例如类java.lang.Object
,它存放再rt.jar
中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。所以,双亲委派很好地解决了各个类加载器的 基础类的统一 问题(越基础的类由越上层的加载器进行加载)。
同时,也可以避免核心API被篡改。
缺陷
在双亲委派模型中,委派是单向的,子类加载器会委派给父类加载器,但是父类加载器无法委托给子类加载器,所以启动类加载器无法加载拓展类加载器加载的类,拓展类加载器也无法加载应用程序类加载器加载的类。
实现
实现双亲委派的代码都集中在java.lang.ClassLoader
的loadClass()
方法之中。如下代码所示:
protected synchronized Class> loadClass(String name,boolean resolve)throws ClassNotFoundException{
//检查请求的类是否加载过了
Class c = findLoadedClass(name);
if(c == null){
try{
if(parent != null){
c = parent.loadClass(name,false);
}else{
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
//如果抛出异常,说明父加载器无法完成加载请求
}
if(c == null){
//在父类无法加载的时候
//再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
步骤为:
首先,检查请求的类是否加载过了,如果加载过了,就不需要再加载,直接返回。
如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用
parent.loadClass(name, false);
)。或者是调用bootstrap类加载器来加载。如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。
三. 初始类加载器和定义类加载器
在类加载的双亲委派模型下,类加载器首先会代理给它的父加载器来尝试加载某个类,所以真正完成类的加载工作的类加载器和启动这个类加载过程的类加载器有可能不是同一个。真正完成类的加载工作是通过调用defineClass
来实现的;而启动类的加载过程是通过调用loadClass
来实现的。前者称为一个类的定义加载器(defining loader)
,后者称为初始加载器(initiating loader)
。
JVM文档是这样说明的, 假设类或接口C
的创建是因为类或接口D
触发的:
If D was defined by the bootstrap class loader, then the bootstrap class loader initiates loading of C (§5.3.1).
If D was defined by a user-defined class loader, then that same user-defined class loader initiates loading of C (§5.3.2).
也就是,如果D
被启动类加载器定义,那么启动类加载器就是C
的初始加载器。如果D
被其他加载器定义,那么同一个加载器初始化C
的加载,也就是C
的初始加载器。
在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器(defining loader)。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。
四、实现双亲委派的自定义类加载器
如前方法loadClass
所示,该方法实现了双亲委派的逻辑,如果双亲类加载器没有找到对应的类,最后会使用findClass
来加载类。所以实现满足双亲委派的自定义类加载器只需要继承ClassLoader
类,并重写findClass
方法就行。
如下所示:
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
// 从文件系统中读取
FileInputStream fis = new FileInputStream(classPath + "/" + name
+ ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadByte(name);
// 将一个字节数组转换成 Class实例。
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
MyClass
类如下:
public class MyClass {
public void print() {
System.out.println("MyClass, ClassLoader:" + getClass().getClassLoader().getClass().getName());
}
}
执行入口:
public static void main(String[] args) throws Exception {
MyClassLoader loader = new MyClassLoader("C:\\work\\leo\\leotest\\myclass\\target\\classes");
Class> aClass = loader.loadClass("MyClass");
Object o = aClass.newInstance();
Method printMethod = aClass.getDeclaredMethod("print", null);
printMethod.invoke(o);
}
结果:
MyClass, ClassLoader:MyClassLoader