Java基础——类加载机制及原理

一、什么是类的加载?


       类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。


       类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

加载.class文件的方式
– 从本地系统中直接加载
– 通过网络下载.class文件
– 从zip,jar等归档文件中加载.class文件
– 从专有数据库中提取.class文件
– 将Java源文件动态编译为.class文件

二、类的生命周期



       其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。


1、加载:查找并加载类的二进制数据

加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:

       1)通过一个类的全限定名来获取其定义的二进制字节流。

       2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

       3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

       相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

       加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。


2、验证确保被加载的类的正确性

       验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

       文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

       元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。

       字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

       符号引用验证:确保解析动作能正确执行。

       验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。


3、准备为类的静态变量分配内存,并将其初始化为默认值

       准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

       1)这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

       2)这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

       假设一个类变量的定义为:public static int value = 3;

       那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。


       这里还需要注意如下几点:

  • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
  • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
  • 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
  • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。

       3)如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。

       假设上面的类变量value被定义为: public static final int value = 3;

       编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。回忆上一篇博文中对象被动引用的第2个例子,便是这种情况。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中。


4、解析把类中的符号引用转换为直接引用

       解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。

       直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。


5、初始化

       初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:

       ①声明类变量是指定初始化值

       ②使用静态代码块为类变量指定初始


 JVM初始化步骤

       1)假如这个类还没有被加载和连接,则程序先加载并连接该类

       2)假如该类的直接父类还没有被初始化,则先初始化其直接父类

       3)假如类中有初始化语句,则系统依次执行这些初始化语句


类初始化时机

只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

       – 创建类的实例,也就是new的方式

       – 访问某个类或接口的静态变量,或者对该静态变量赋值

       – 调用类的静态方法

       – 反射(如Class.forName(“com.shengsiyuan.Test”))

       – 初始化某个类的子类,则其父类也会被初始化

       – Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

 

结束生命周期

在如下几种情况下,Java虚拟机将结束生命周期:

       – 执行了System.exit()方法

       – 程序正常执行结束

       – 程序在执行过程中遇到了异常或错误而异常终止

       – 由于操作系统出现错误而导致Java虚拟机进程终止


三、Java虚拟机类加载器


1、JVM三种预定义类型类加载器

我们首先看一下JVM预定义的三种类型类加载器,当一个 JVM启动的时候,Java缺省开始使用如下三种类型类装入器:

       启动(Bootstrap)类加载器引导类装入器是用本地代码实现的类装入器,它负责将%JAVA_HOME%\lib路径下或-Xbootclasspath参数指定路径下的、能被虚拟机识别的类库(仅按照文件名识别,如:rt.jar,名字不符合的类库不会被加载)加载至虚拟机内存中由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

       扩展(Extension)类加载器扩展类加载器是由SunExtClassLoadersun.misc.Launcher$ExtClassLoader实现的。它负责将java.ext.dirs参数(默认值是%JAVA_HOME%\jre\lib\ext,可由VM参数-Djava.ext.dirs指定)指定路径中的所有类库加载到内存中。开发者可以直接使用标准扩展类加载器。

       系统(System)类加载器系统类加载器是由 SunAppClassLoadersun.misc.Launcher$AppClassLoader)实现的,由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,也称为系统类加载器。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

       注意:上述三个JDK提供的类加载器虽然父子类加载器关系,但是没有使用继承,而是使用了组合关系


2、类加载双亲委派机制介绍和分析

从JDK1.2开始,java虚拟机规范推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:

  1. 如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成,每一个层次的加载器都是如此
  2. 因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
  3. 如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。

       关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。


图一 标准扩展类加载器继承层次图


图二系统类加载器继承层次图

       通过图一和图二我们可以看出,类加载器均是继承自java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下java.lang.ClassLoader中几个最重要的方法:

//加载指定名称(包括包名)的二进制类型,供用户调用的接口  
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }  
  
//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用  
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }  
  
//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用  
protected Class<?> findClass(String name) throws ClassNotFoundException { … }  
  
//定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承  
//(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)  
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }
       通过进一步分析标准扩展类加载器( sun.misc.Launcher$ExtClassLoader )和系统类加载器( sun.misc.Launcher$AppClassLoader )的代码以及其公共父类(java.net.URLClassLoader java.security.SecureClassLoader )的代码可以看出,都没有覆写 java.lang.ClassLoader中默认的加载委派规则---loadClass )方法既然这样,我们就可以通过分析java.lang.ClassLoader 中的loadClass String name)方法的代码就可以分析出虚拟机默认采用的双亲委派机制到底是什么模样:
public Class<?> loadClass(String name) throws ClassNotFoundException {  
    return loadClass(name, false);  
}  
  
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 {  
                //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,  
                //通过调用本地方法native findBootstrapClass0(String name)  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能  
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
}  
       通过上面的代码分析,我们可以对JVM 采用的双亲委派类加载机制有了更感性的认识,下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:

图三 类加载器默认委派关系图
       上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:

public class LoaderTest {  
  
    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();  
        }  
    }  
}  
       说明:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器。
代码输出如下:
sun.misc.Launcher$AppClassLoader@6d06d69c  
sun.misc.Launcher$ExtClassLoader@70dea4e  
null  
       通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了 null,就是说标准扩展类加载器本身强制设定父类加载器为null 我们还是借助于代码分析一下。

我们首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:

protected ClassLoader() {  
    SecurityManager security = System.getSecurityManager();  
    if (security != null) {  
        security.checkCreateClassLoader();  
    }  
    //默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器  
    this.parent = getSystemClassLoader();  
    initialized = true;  
}  
  
protected ClassLoader(ClassLoader parent) {  
    SecurityManager security = System.getSecurityManager();  
    if (security != null) {  
        security.checkCreateClassLoader();  
    }  
    //强制设置父类加载器  
    this.parent = parent;  
    initialized = true;  
}  
我们再看一下 ClassLoader 抽象类中 parent 成员的声明:
// The parent class loader for delegation  
private ClassLoader parent;
       声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:

  • 系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)
  • 扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

       现在我们可能会有这样的疑问:扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?

图四 标准扩展类加载器和系统类加载器成员大纲视图
图五  扩展类加载器和系统类加载器公共父类成员大纲视图

       通过图四和图五可以看出,标准扩展类加载器和系统类加载器及其父类(java.net.URLClassLoader和java.security.SecureClassLoader)都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法有关java.lang.ClassLoader中默认的加载委派规则前面已经分析过,如果父加载器为null,则会调用本地方法进行启动类加载尝试。所以,图三中,启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系事实上是仍就成立的。(在后面的用户自定义类加载器部分,还会做更深入的分析)。

       双亲委派模式的类加载机制的优点:java类它的类加载器一起具备了一种带优先级的层次关系,越是基础的类,越是被上层的类加载器进行加载,保证了java程序的稳定运行。


3、类加载双亲委派示例

       以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子。首先在IDE中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:

package classloader.test.bean;  
  
public class TestBean {  
      
    public TestBean() { }  
}  
在现有当前工程中另外建立一测试类(ClassLoaderTest.java)内容如下:

测试一:

package classloader.test.bean;  
  
public class ClassLoaderTest {  
  
    public static void main(String[] args) {  
        try {  
            //查看当前系统类路径中包含的路径条目  
            System.out.println(System.getProperty("java.class.path"));  
            //调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean  
            Class typeLoaded = Class.forName("classloader.test.bean.TestBean");  
            //查看被加载的TestBean类型是被那个类加载器加载的  
            System.out.println(typeLoaded.getClassLoader());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}  
输出结果:

C:\Users\Administrator\Documents\NetBeansProjects\ClassLoaderTest\build\classes  
sun.misc.Launcher$AppClassLoader@73d16e93
说明:当前类路径默认的含有的一个条目就是工程的输出目录。

测试二:

       将当前工程输出目录下的TestBean.class打包进test.jar剪贴<Java_Runtime_Home>/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:

C:\Users\Administrator\Documents\NetBeansProjects\ClassLoaderTest\build\classes  
sun.misc.Launcher$ExtClassLoader@15db9742 

       对比测试一和测试二,我们明显可以验证前面说的双亲委派机制,系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。

测试三:

       将test.jar拷贝一份到<Java_Runtime_Home>/lib下,运行测试代码,输出如下:
C:\Users\Administrator\Documents\NetBeansProjects\ClassLoaderTest\build\classes  
sun.misc.Launcher$ExtClassLoader@15db9742
       测试三和测试二输出结果一致。那就是说,放置到<Java_Runtime_Home>/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。 虚拟机出于安全等因素考虑,不会加载<Java_Runtime_Home>/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除<Java_Runtime_Home>/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。

四、Java程序动态扩展方式

       Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以装载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。

运行时动态扩展java应用程序有如下两个途径:


1、调用java.lang.Class.forName(…)加载类

       这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发哪个类加载器开始加载任务。这里需要说明的是多参数版本的forName(…)方法:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
       这里的initialize参数是很重要的。它表示在加载同时是否完成初始化的工作(说明:单参数版本的forName方法默认是完成初始化的)。 有些场景下需要将initialize设置为true来强制加载同时完成初始化。例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用。这就要求驱动程序类必须被初始化,而不单单被加载。Class.forName的一个很常见的用法就是在加载数据库驱动的时候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()用来加载 Apache Derby 数据库的驱动。


2、用户自定义类加载器

       通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般用户自定义类加载器的工作流程吧(可以结合后面问题解答一起看):

  1. 首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;
  2. 委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;
  3. 调用本类加载器的findClass(…)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(…), loadClass(…)转而抛异常,终止加载过程(注意:这里的异常种类不止一种)。

       说明:这里说的自定义类加载器是指JDK 1.2以后版本的写法,即不覆写改变java.lang.loadClass(…)已有委派逻辑情况下。

整个加载类的过程如下图:


图六 自定义类加载器加载类的过程


五、常见问题


1、由不同的类加载器加载的指定类还是相同的类型吗?

       在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间我们可以用两个自定义类加载器去加载某自定义类型(注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载),然后用获取到的两个Class实例进行java.lang.Object.equals(…)判断,将会得到不相等的结果。这个大家可以写两个自定义的类加载器去加载相同的自定义类型,然后做个判断;同时,可以测试加载java.*类型,然后再对比测试一下测试结果。


2、在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

Class.forName(String name)默认会使用调用类的类加载器来进行类加载。我们直接来分析一下对应的jdk的代码:

//java.lang.Class.java  
publicstatic Class<?> forName(String className) throws ClassNotFoundException {  
    return forName0(className, true, ClassLoader.getCallerClassLoader());  
}  
  
//java.lang.ClassLoader.java  
// Returns the invoker's class loader, or null if none.  
static ClassLoader getCallerClassLoader() {  
    // 获取调用类(caller)的类型  
    Class caller = Reflection.getCallerClass(3);  
    // This can be null if the VM is requesting it  
    if (caller == null) {  
        return null;  
    }  
    // 调用java.lang.Class中本地方法获取加载该调用类(caller)的ClassLoader  
    return caller.getClassLoader0();  
}  
  
//java.lang.Class.java  
//虚拟机本地实现,获取当前类的类加载器,前面介绍的Class的getClassLoader()也使用此方法  
native ClassLoader getClassLoader0();  


3、 在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?
       前面讲过,在不指定父类加载器的情况下,默认采用系统类加载器可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类,对应的无参默认构造函数实现如下:
//摘自java.lang.ClassLoader.java  
protected ClassLoader() {  
    SecurityManager security = System.getSecurityManager();  
    if (security != null) {  
        security.checkCreateClassLoader();  
    }  
    this.parent = getSystemClassLoader();  
    initialized = true;  
}  
我们再来看一下对应的getSystemClassLoader()方法的实现:
private static synchronized void initSystemClassLoader() {  
    //...  
    sun.misc.Launcher l = sun.misc.Launcher.getLauncher();  
    scl = l.getClassLoader();  
    //...  
}  
我们可以写简单的测试代码来测试一下:
System.out.println(sun.misc.Launcher.getLauncher().getClassLoader());  
输出结果:

sun.misc.Launcher$AppClassLoader@73d16e93  
       所以,我们现在可以相信当自定义类加载器没有指定父类加载器的情况下,默认的父类加载器即为系统类加载器。同时,我们可以得出如下结论: 即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

  1. <Java_Runtime_Home>/lib下的类;
  2. < Java_Runtime_Home >/lib/ext下或者由系统变量java.ext.dir指定位置中的类;
  3. 当前工程类路径下或者由系统变量java.class.path指定位置中的类。


4、在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?

       JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:

       即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<Java_Runtime_Home>/lib下的类,但此时就不能够加载<Java_Runtime_Home>/lib/ext目录下的类了。

       说明:问题3和问题4的推断结论是基于用户自定义的类加载器本身延续了java.lang.ClassLoader.loadClass(…)默认委派逻辑,如果用户对这一默认委派逻辑进行了改变,以上推断结论就不一定成立了,详见问题5。


5、编写自定义类加载器时,一般有哪些注意点?

1)一般尽量不要覆写已有的loadClass(...)方法中的委派逻辑

       一般在JDK 1.2之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。举一个例子来验证该问题:

//用户自定义类加载器WrongClassLoader.Java(覆写loadClass逻辑)  
public class WrongClassLoader extends ClassLoader {  
  
    public Class<?> loadClass(String name) throws ClassNotFoundException {  
        return this.findClass(name);  
    }  
  
    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        // 假设此处只是到工程以外的特定目录D:\library下去加载类  
        // 具体实现代码省略  
    }  
}  
       通过前面的分析我们已经知道,这个自定义类加载器WrongClassLoader的默认类加载器是系统类加载器,但是现在问题4种的结论就不成立了。大家可以简单测试一下,现在<Java_Runtime_Home>/lib、< Java_Runtime_Home >/lib/ext和工程类路径上的类都加载不上了。
//问题5测试代码一  
public class WrongClassLoaderTest {  
  
    publicstaticvoid main(String[] args) {  
        try {  
            WrongClassLoader loader = new WrongClassLoader();  
            Class classLoaded = loader.loadClass("beans.Account");  
            System.out.println(classLoaded.getName());  
            System.out.println(classLoaded.getClassLoader());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}  
这里D:"classes"beans"Account.class是物理存在的。输出结果:
java.io.FileNotFoundException: D:"classes"java"lang"Object.class (系统找不到指定的路径。)  
    at java.io.FileInputStream.open(Native Method)  
    at java.io.FileInputStream.<init>(FileInputStream.java:106)  
    at WrongClassLoader.findClass(WrongClassLoader.java:40)  
    at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
    at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)  
    at java.lang.ClassLoader.defineClass1(Native Method)  
    at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
    at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
    at WrongClassLoader.findClass(WrongClassLoader.java:43)  
    at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
    at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27)  
Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object  
    at java.lang.ClassLoader.defineClass1(Native Method)  
    at java.lang.ClassLoader.defineClass(ClassLoader.java:620)  
    at java.lang.ClassLoader.defineClass(ClassLoader.java:400)  
    at WrongClassLoader.findClass(WrongClassLoader.java:43)  
    at WrongClassLoader.loadClass(WrongClassLoader.java:29)  
    at WrongClassLoaderTest.main(WrongClassLoaderTest.java:27) 
       这说明,连要加载的类型的超类型java.lang.Object都加载不到了。这里列举的由于覆写loadClass()引起的逻辑错误明显是比较简单的,实际引起的逻辑错误可能复杂的多。
//问题5测试二  
//用户自定义类加载器WrongClassLoader.Java(不覆写loadClass逻辑)  
public class WrongClassLoader extends ClassLoader {  
  
    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        //假设此处只是到工程以外的特定目录D:\library下去加载类  
        //具体实现代码省略  
    }  
}  
将自定义类加载器代码WrongClassLoader.Java做以上修改后,再运行测试代码,输出结果如下:
beans.Account  
WrongClassLoader@1c78e57  
2)正确设置父类加载器

       通过上面问题4和问题5的分析我们应该已经理解,个人觉得这是自定义用户类加载器时最重要的一点,但常常被忽略或者轻易带过。有了前面JDK代码的分析作为基础,我想现在大家都可以随便举出例子了。

3)保证findClass(String name)方法的逻辑正确性

       事先尽量准确理解待定义的类加载器要完成的加载任务,确保最大程度上能够获取到对应的字节码内容。

6、如何在运行时判断系统类加载器能加载哪些路径下的类?

       一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到。

       二是可以直接通过获取系统属性java.class.path来查看当前类路径上的条目信息 :System.getProperty("java.class.path")。


7、如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

方法一:

import java.net.URL;  
import java.net.URLClassLoader;  
  
public class ClassLoaderTest {  
  
    /** 
     * @param args the command line arguments 
     */  
    public static void main(String[] args) {  
        try {  
            URL[] extURLs = ((URLClassLoader) ClassLoader.getSystemClassLoader().getParent()).getURLs();  
            for (int i = 0; i < extURLs.length; i++) {  
                System.out.println(extURLs[i]);  
            }  
        } catch (Exception e) {  
            //…  
        }  
    }  
}  
输出结果:

file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/access-bridge-64.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/cldrdata.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/dnsns.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jaccess.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/jfxrt.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/localedata.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/nashorn.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunec.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunjce_provider.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunmscapi.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/sunpkcs11.jar  
file:/C:/Program%20Files/Java/jdk1.8.0_05/jre/lib/ext/zipfs.jar 

六、开发自己的类加载器


       在前面介绍类加载器的代理委派模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

       方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。

       类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用

       在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,您还是需要为应用开发出自己的类加载器。比如您的应用通过网络来传输Java类的字节代码,为了保证安全性,这些字节代码经过了加密处理。这个时候您就需要自己的类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出要在Java虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。

1、文件系统类加载器

第一个类加载器用来加载存储在文件系统上的Java字节代码。完整的实现如下所示。

package classloader;  
  
import java.io.ByteArrayOutputStream;  
import java.io.File;  
import java.io.FileInputStream;  
import java.io.IOException;  
import java.io.InputStream;  
  
// 文件系统类加载器  
public class FileSystemClassLoader extends ClassLoader {  
  
    private String rootDir;  
  
    public FileSystemClassLoader(String rootDir) {  
        this.rootDir = rootDir;  
    }  
  
    // 获取类的字节码  
    @Override  
    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        byte[] classData = getClassData(name);  // 获取类的字节数组  
        if (classData == null) {  
            throw new ClassNotFoundException();  
        } else {  
            return defineClass(name, classData, 0, classData.length);  
        }  
    }  
  
    private byte[] getClassData(String className) {  
        // 读取类文件的字节  
        String path = classNameToPath(className);  
        try {  
            InputStream ins = new FileInputStream(path);  
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
            int bufferSize = 4096;  
            byte[] buffer = new byte[bufferSize];  
            int bytesNumRead = 0;  
            // 读取类文件的字节码  
            while ((bytesNumRead = ins.read(buffer)) != -1) {  
                baos.write(buffer, 0, bytesNumRead);  
            }  
            return baos.toByteArray();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  
  
    private String classNameToPath(String className) {  
        // 得到类文件的完全路径  
        return rootDir + File.separatorChar  
                + className.replace('.', File.separatorChar) + ".class";  
    }  
}  
       如上所示,类 FileSystemClassLoader继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。java.lang.ClassLoader类的方法loadClass()封装了前面提到的代理模式的实现。该方法会首先调用findLoadedClass()方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()方法来查找该类。 因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。

       类 FileSystemClassLoader的 findClass()方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成 java.lang.Class类的实例。

加载本地文件系统上的类,示例如下:

package com.example;  
  
public class Sample {  
  
    private Sample instance;  
  
    public void setSample(Object instance) {  
        System.out.println(instance.toString());  
        this.instance = (Sample) instance;  
    }  
} 
package classloader;  
  
import java.lang.reflect.Method;  
  
public class ClassIdentity {  
  
    public static void main(String[] args) {  
        new ClassIdentity().testClassIdentity();  
    }  
  
    public void testClassIdentity() {  
        String classDataRootPath = "C:\\Users\\JackZhou\\Documents\\NetBeansProjects\\classloader\\build\\classes";  
        FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);  
        FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);  
        String className = "com.example.Sample";  
        try {  
            Class<?> class1 = fscl1.loadClass(className);  // 加载Sample类  
            Object obj1 = class1.newInstance();  // 创建对象  
            Class<?> class2 = fscl2.loadClass(className);  
            Object obj2 = class2.newInstance();  
            Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);  
            setSampleMethod.invoke(obj1, obj2);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
} 
输出结果:
com.example.Sample@7852e922


2、网络类加载器
       下面将通过一个网络类加载器来说明如何通过类加载器来实现组件的动态更新。即基本的场景是:Java 字节代码(.class)文件存放在服务器上,客户端通过网络的方式获取字节代码并执行。当有版本更新的时候,只需要替换掉服务器上保存的文件即可。通过类加载器可以比较简单的实现这种需求。

       类 NetworkClassLoader负责通过网络下载Java类字节代码并定义出Java类。它的实现与FileSystemClassLoader类似。

package classloader;  
  
import java.io.ByteArrayOutputStream;  
import java.io.InputStream;  
import java.net.URL;  
  
public class NetworkClassLoader extends ClassLoader {  
  
    private String rootUrl;  
  
    public NetworkClassLoader(String rootUrl) {  
        // 指定URL  
        this.rootUrl = rootUrl;  
    }  
  
    // 获取类的字节码  
    @Override  
    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        byte[] classData = getClassData(name);  
        if (classData == null) {  
            throw new ClassNotFoundException();  
        } else {  
            return defineClass(name, classData, 0, classData.length);  
        }  
    }  
  
    private byte[] getClassData(String className) {  
        // 从网络上读取的类的字节  
        String path = classNameToPath(className);  
        try {  
            URL url = new URL(path);  
            InputStream ins = url.openStream();  
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
            int bufferSize = 4096;  
            byte[] buffer = new byte[bufferSize];  
            int bytesNumRead = 0;  
            // 读取类文件的字节  
            while ((bytesNumRead = ins.read(buffer)) != -1) {  
                baos.write(buffer, 0, bytesNumRead);  
            }  
            return baos.toByteArray();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return null;  
    }  
  
    private String classNameToPath(String className) {  
        // 得到类文件的URL  
        return rootUrl + "/"  
                + className.replace('.', '/') + ".class";  
    }  
}  
       在通过NetworkClassLoader加载了某个版本的类之后,一般有两种做法来使用它。第一种做法是使用Java反射API。另外一种做法是使用接口。 需要注意的是,并不能直接在客户端代码中引用从服务器上下载的类,因为客户端代码的类加载器找不到这些类。使用Java反射API可以直接调用Java类的方法。而使用接口的做法则是把接口的类放在客户端中,从服务器上加载实现此接口的不同版本的类。在客户端通过相同的接口来使用这些实现类。我们使用接口的方式。示例如下:

客户端接口:

package classloader;  
  
public interface Versioned {  
    String getVersion();  
}  
package classloader;  
  
public interface ICalculator extends Versioned {  
    String calculate(String expression);  
} 

网络上的不同版本的类:

package com.example;  
  
import classloader.ICalculator;  
  
public class CalculatorBasic implements ICalculator {  
  
    @Override  
    public String calculate(String expression) {  
        return expression;  
    }  
  
    @Override  
    public String getVersion() {  
        return "1.0";  
    }  
}  
package com.example;  
  
import classloader.ICalculator;  
  
public class CalculatorAdvanced implements ICalculator {  
  
    @Override  
    public String calculate(String expression) {  
        return "Result is " + expression;  
    }  
  
    @Override  
    public String getVersion() {  
        return "2.0";  
    }  
  
} 
在客户端加载网络上的类的过程:
package classloader;  
  
public class CalculatorTest {  
  
    public static void main(String[] args) {  
        String url = "http://localhost:8080/ClassloaderTest/classes";  
        NetworkClassLoader ncl = new NetworkClassLoader(url);  
        String basicClassName = "com.example.CalculatorBasic";  
        String advancedClassName = "com.example.CalculatorAdvanced";  
        try {  
            Class<?> clazz = ncl.loadClass(basicClassName);  // 加载一个版本的类  
            ICalculator calculator = (ICalculator) clazz.newInstance();  // 创建对象  
            System.out.println(calculator.getVersion());  
            clazz = ncl.loadClass(advancedClassName);  // 加载另一个版本的类  
            calculator = (ICalculator) clazz.newInstance();  
            System.out.println(calculator.getVersion());  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
  
} 



已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页