Class Loader 名为“类加载器”,用以加载class 文件到Java 虚拟机中。与普通程序不同,class 文件(Java 程序)并不是本地的可执行程序。当运行class 文件时,首先会运行Java 虚拟机(以下简称JVM),然后再将class 文件加载至JVM,最后JVM 通过内部机制将其执行。负责加载class 文件的这部分程序即被称为"Class Loader"。
默认情况下,一个Java2 JVM通常提供了一个引导类加载器(Bootstrap Class Loader)和两个用户自定义的类装载器:扩展类加载器(Extension Class Loader)和系统/应用类加载器(System Class Loader/Application Class Loader)。
Bootstrap Class Loader 是负责加载JVM 的核心Java 类(如Java.* javax.*等)。Extension Class Loader 负责加载JRE 的扩展目录类。最后,System Class Loader 负责从系统类路径加载类。除了以上三种Class Loader 之外,用户还可以自定义Class Loader,自定义Class Loader是通过继承java.lang.ClassLoader 类来实现的,下面是几种Class Loader 的详细工作内容:
1).Bootstrap Class Loader
主要负责JRE_HOME(JRE 所在目录)/lib 目录下的核心API 或-Xbootclasspath 选项指定的jar包装入工作。
2).Extension Class Loader
主要负责JRE_HOME/lib/ext 目录下的jar 包或-Djava.ext.dirs 选项指定目录下的jar 包装入工作。
3).System/Application Class Loader
主要负责java -classpath 或-Djava.class.path 所指定目录下的class 与jar 包装入工作。
4).User Custom Class Loader(java.lang.ClassLoader的子类)
在程序运行期间, 通过自定义Class Loader 动态加载class 文件, 体现Java 语言动态实时类装入的特性。
四种Class Loader是逐级向上委托的关系,即4-->3-->2-->1 他们的关系如图所示:
这种向上依赖的关系模型被称作:双亲委托模型(或类似叫法,本文以JVM 实现为例),从Java2 (Java1.2版本)开始,Java引入了此种模型。
在此模型下,当一个装载器被请求装载某个类时,它首先委托自己的parent 去装载,若parent 能够装载,则返回这个类所对应的Class 对象,若parent 无法装载,则由parent 的请求者自行去装载。
采用此种模型可以避免重复加载,当上级Class Loader 已经加载了该类的时候,下级Class Loader就没有必要再加载一次。
我们来看一下JDK中java.lang.ClassLoader类加载类部分的源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
从代码中我们可以清晰的看到,在加载类的过程中会先判断改加载器有没有父类,如果有尝试用父类加载,如果加载不成功则自己加载。
考虑到安全因素,如果不使用这种委托模式,那我们就可以随时使用自定义Class Loader 重复加载Java 核心类,并修改相应内容。采用此种模式后,例如String,Integer等核心类将在JVM 启动之后自动被加载,用户将无法再去加载相关内容。从而防止了用户用恶意代码代替JVM 系统可靠代码的安全问题。
注意:此处所指parent 、上级、下级、父、子与Java中的继承不同,是一种逻辑依赖关系。
本文以JVM ClassLoader 实现为例未考虑其他特殊情况。
可以通过以下代码来验证一下这个模型:
public class HelloWorld {
public static void main(String[] args) throws InterruptedException {
ClassLoader loader = HelloWorld.class.getClassLoader();
int i=1;
while (loader != null) {
System.out.println(i+++":"+loader.getClass().getName());
loader = loader.getParent();
}
System.out.println(loader);
}
}
2:sun.misc.Launcher$ExtClassLoader
null
1 表示HelloWorld 的类加载器是AppClassLoader
2 表示AppClassLoader 的类加载器是ExtClassLoder
null 表示ExtClassLoader 的类加载器是BootstrapClassLoader。
还可以做一个试验,那就是将HelloWOrld 打包成hello.jar 并放到JRE_HOME\lib\ext\ 目录下,然后重新运行程序。
null
之所以结果不同是因为在JVM 启动之后ExtClassLoder 已经将JRE_HOME\lib\ext\ 下的所有jar包加载,当我们的程序再运行时无需再一次加载,所以它的类加载器是ExtClassLoder,符合双亲委托模型的限制原则。
如果类装载器查找到一个没有装载的类,它会按照下图的流程来装载和链接这个类:
每个阶段的描述如下:
Verification: 检查读入的结构是否符合Java语言规范以及JVM规范的描述。这是类装载中最复杂的过程,并且花费的时间也是最长的。并且JVM TCK工具的大部分场景的用例也用来测试在装载错误的类的时候是否会出现错误。
Preparation: 分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口的信息。
Resolution: 把这个类的常量池中的所有的符号引用改变成直接引用。
Initialization: 把类中的变量初始化成合适的值。执行静态初始化程序,把静态变量初始化成指定的值。
使用以下命令可以直接显示更详细的加载过程内容:
java -verbose:class HelloWorld
Bootstrap Class Loader并不是用Java 编写的所以之前结果会显示为null,其他三种Class Loader均采用Java 编写。这四种Class Loader 会在下两节分别详细介绍。
命名空间
每个Class Loader都维护了一份自己的命名空间,命名空间由所有以此装载器为创始类装载器的类组成,同一个命名空间内不能出现同名的类。不同命名空间的两个类是不可见的,但只要得到类所对应的Class 对象的reference,还是可以访问另一命名空间的类。
当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name)进行搜索来检测这个类是否已经被加载了。如果两个类的全局限定名是一样的,但是如果命名空间不一样的话,那么它们还是不同的类。不同的命名空间表示class 被不同的类装载器装载。
二进制名称
按照《Java Language Specification》的定义,任何作为String 类型参数传递给ClassLoader 中方法的类名称都必须是一个二进制名称。
"javax.swing.JSpinner$DefaultEditor"
"java.security.KeyStore$Builder$FileBuilder$1"
"java.net.URLClassLoader$3$1"
运行时包(runtime package)
由同一类装载器定义装载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看的定义类装载器是否相同。只有属于同一运行时包的类才能互相访问包可见的类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。
假设用户自己定义了一个类java.lang.Integer2,并用用户自定义的类装载器装载,由于java.lang.Integer2 和核心类库java.lang.* 有着不同的装载器装载,它们属于不同的运行时包,所以java.lang.Integer2 不能访问核心类库java.lang 中类的包可见的成员。
总结:
双亲委托模型加强了Java 的安全与效率,命名空间允许不同空间的类的互相访问,运行时包增加了对包可见成员的保护。
下一节将讲解JVM 预先实现的这三种Class Loader。