注:本文为阅读《深入理解Java虚拟机 JVM高级特性与最佳实战》第2版(周志明)、《疯狂Java讲义》第3版(李刚)两本书后的学习笔记, 特此说明。
Java虚拟机中类加载的全过程包括 加载、验证、准备、解析和初始化这5个阶段。
其中,在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
类加载器
实现“通过一个类的全限定名来获取描述此类的二进制字节流”的这个动作的代码模块称为“类加载器”。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构:
启动类加载器(Bootstrap ClassLoader):
也称为引导类加载器、根类加载器、原始类加载器。这个类加载器比较特殊,是使用C++语言实现的,是虚拟机自身的一部分,且并不是java.lang.ClassLoader的子类。用来加载Java的核心类库。
扩展类加载器(Extension ClassLoader):
负责加载JRE的扩展目录中JAR包的类。
应用程序类加载器(Application ClassLoader):
也称为系统类加载器,这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
双亲委派模型
图7-2中展示的类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
实现双亲委派的代码都集中在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){
//如果父类加载器抛出ClassNotFoundException
//说明父类加载器无法完成加载请求
}
if(c==null){
//在父类加载器无法加载的时候
//再调用本身的findClass方法来进行类加载
c=findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
双亲委派模型的好处:
Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。双亲委派模型对于保证Java程序的稳定运作很重要。
创建并使用自定义的类加载器
JVM中除根类加载器之外的所有类加载器都是ClassLoader的子类的实例,开发者可以通过继承ClassLoader,并重写该ClassLoader所包含的方法来实现自定义的类加载器。
ClassLoader类有如下两个关键方法:
- loadClass(String name, boolean resolve):该方法为ClassLoader的入口点,根据指定的名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。
- findClass(String name):根据指定的名称来查找类。
如果需要实现自定义的ClassLoader,则可以通过重写上面的两个方法来实现,通常推荐重写findClass(String name)方法,可以避免覆盖默认类加载器的双亲委派模型和缓冲机制。
ClassLoader里还有一个核心方法:Class defineClass(String name, byte[] b, int off, int len),该方法负责将已经读入字节数组byte[] b中的字节码文件内容转换为Class对象。该方法管理JVM的许多复杂的实现,它负责将字节码分析成运行时数据结构,并校验有效性。不过,该方法是final的,程序员无需重写该方法。