一、类加载器
1、在Java中,有两种类型的类加载器,分别是JVM自带的类加载器和用户自定义的类加载器。
2、JVM自带的类加载器有三种,如下:
- 根(Bootstrap)类加载器:该加载器没有父加载器,它负责加载虚拟机的核心类库,如java.lang.*等。它从系统属性sun.boot.class.path所指定的目录中加载类库,它的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类,因为它是用C++写的,无法在代码中访问该类。
- 扩展(Extension)类加载器:它的父加载器为根类加载器,它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的jar文件放在该目录下,也会自动由扩展类加载器加载。它是纯Java类,是java.lang.ClassLoader类的子类。
- 系统(System)类加载器:也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量classpath或系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。它是纯Java类,也是java.lang.ClassLoader类的子类。
如下代码所示,由于类的加载是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。而这个Class对象会包含一个负责加载该类的类加载器的引用,所以通过Class对象的getClassLoader方法就可以得到加载该类的类加载器。以下代码的运行结果是首先输出“null”,然后输出“sun.misc.Launcher$AppClassLoader@18fe7c3”,因为根类加载器它负责加载虚拟机的核心类库,所以代码“Class<?> clazz = Class.forName("java.lang.String")”会由根类加载器进行加载,而根类加载器是无法在代码中访问到的,所以“clazz.getClassLoader()”返回null代表根类加载器。
package com.jgao.classloader.test05;
class User {
}
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> clazz = Class.forName("java.lang.String");
System.out.println(clazz.getClassLoader());
clazz = Class.forName("com.jgao.classloader.test05.User");
System.out.println(clazz.getClassLoader());
}
}
3、用户自定义类加载器
除了以上JVM自带的加载器外,用户还可以定制自己的类加载器,所有用户自定义的类加载器需要继承java.lang.ClassLoader类。
二、父亲委托机制
1、父子加载器
类加载器用来把类加载到JVM中。从JDK 1.2开始,类的加载过程采用父亲委托机制,该机制能更好地保证Java平台的安全性。在委托机制中,各个加载器按照父子关系形成了树形结构,除了JVM自带的根类加载器以外,其余的类加载器都有且只有一个父类加载器,如下图:
需要注意的是加载器之间的父子关系实际上指的是加载器对象之间的包装关系,而不是类之间的继承关系,所以一对父子加载器可能是同一个加载器类的两个实例(在子加载器对象中包装了一个父加载器对象),也可能不是。如下图所示,这是java.lang.ClassLoader类的两个构造方法;当生成一个自定义的类加载器实例时,如果使用的是ClassLoader类的空参数的构造方法时(也就是没有指定它的父加载器),那么会使用 getSystemClassLoader() 方法返回的系统类加载器作为该类加载器的父加载器,所以在介绍系统类加载器时说它是用户自定义的类加载器的默认父加载器。而使用另外一个构造方法就可以指定父加载器。
2、父亲委托机制加载流程
如下图所示,当Java程序请求加载器loader2加载User类时,加载器load2首先从自己的命名空间中查找User类是否已经被加载了,如果已经加载,则直接返回代表User类的Class对象的引用。否则,loader2首先请求父加载器loader1代为加载,loader1再继续请求它的父加载器(系统类加载器)代为加载,系统类加载器再请求扩展类加载器代为加载,扩展类加载器再请求根类加载器代为加载。若根类加载器和扩展类加载器都不能加载,则系统类加载器尝试加载,若能加载成功,则将User所对应的Class对象的引用返回给loader1,loader1再将引用返回给loader2,从而成功将User类加载进虚拟机。若系统类加载器不能加载User类,则loader1尝试加载User类,若loader1也不能成功加载,则loader2尝试加载。若所有的父加载器及loader2本身都不能加载,则抛出ClassNotFoundException异常。
在这整个过程中,如果有一个类加载器能够成功加载User类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象的引用的类加载器(包括定义类加载器)都被称为初始类加载器。比如loader1实际加载了User类,则loader1为User类的定义类加载器,loader2和loader1为User类的初始类加载器。
3、父亲委托机制的优点
父亲委托机制的优点就是能够提高软件系统的安全性,因为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。比如java.lang.Object类总是由根类加载器加载,其他任何用户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。
三、其他
1、命名空间
每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字相同的两个类。比如有两个类加载器A和B,A类加载器已经加载了C这个类,那么B这个类加载器还能加载C这个类吗?这要分两种情况,如下:
- A和B加载器不是父子关系:这时B这个类加载器还是可以加载C这个类
- A和B加载器是父子关系:这时B这个类加载器就不可以加载C这个类,因为已经由A加载器加载过了,就不会再次进行加载
2、运行时包
由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看定义类加载器是否相同,只有属于同一运行时包的类才能互相访问包可见(即默认的访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。比如用户自定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库java.lang.*是由不同的类加载器加载,它们属于不同的运行时包,所以java.lang.Spy不能访问核心类库java.lang包中的包可见成员。