类加载机制
类加载器ClassLoader就是加载其他类的类,它负责将字节码文件加载到内存,创建Class对象。与之前介绍的反射、注解、和动态代理一样,在大部分的应用编程中,我们不太需要自己实现ClassLoader。
不过,理解类加载的机制和过程,有助于我们更好的理解之前介绍的内容,更好的理解Java。在反射一节,我们介绍过Class的静态方法Class.forName,理解类加载器有助于我们更好的理解该方法。
ClassLoader一般是系统提供的,不需要自己实现,不过,通过创建自定义的ClassLoader,可以实现一些强大灵活的功能,比如:
- 热部署,在不重启Java程序的情况下,动态替换类的实现,比如Java Web开发中的JSP技术就利用自定义的ClassLoader实现修改JSP代码即生效,OSGI (Open Service Gateway Initiative)框架使用自定义ClassLoader实现动态更新。
- 应用的模块化和相互隔离,不同的ClassLoader可以加载相同的类但互相隔离、互不影响。Web应用服务器如Tomcat利用这一点在一个程序中管理多个Web应用程序,每个Web应用使用自己的ClassLoader,这些Web应用互不干扰。OSGI利用这一点实现了一个动态模块化架构,每个模块有自己的ClassLoader,不同模块可以互不干扰。
- 从不同地方灵活加载,系统默认的ClassLoader一般从本地的.class文件或jar文件中加载字节码文件,通过自定义的ClassLoader,我们可以从共享的Web服务器、数据库、缓存服务器等其他地方加载字节码文件。
理解自定义ClassLoader有助于我们理解这些系统程序和框架,如Tomat, JSP, OSGI,在业务需要的时候,也可以借助自定义ClassLoader实现动态灵活的功能。
类加载的基本机制和过程
运行Java程序,就是执行java这个命令,指定包含main方法的完整类名,以及一个classpath,即类路径。类路径可以有多个,对于直接的class文件,路径是class文件的根目录,对于jar包,路径是jar包的完整名称(包括路径和jar包名)。
Java运行时,会根据类的完全限定名寻找并加载类,寻找的方式基本就是在系统类和指定的类路径中寻找,如果是class文件的根目录,则直接查看是否有对应的子目录及文件,如果是jar文件,则首先在内存中解压文件,然后再查看是否有对应的类。
负责加载类的类就是类加载器,它的输入是完全限定的类名,输出是Class对象。类加载器不是只有一个,一般程序运行时,都会有三个:
- 启动类加载器(Bootstrap ClassLoader):这个加载器是Java虚拟机实现的一部分,不是Java语言实现的,一般是C++实现的,它负责加载Java的基础类,主要是/lib/rt.jar,我们日常用的Java类库比如String, ArrayList等都位于该包内。
- 扩展类加载器(Extension ClassLoader):这个加载器的实现类是sun.misc.Launcher$ExtClassLoader,它负责加载Java的一些扩展类,一般是/lib/ext目录中的jar包。
- 应用程序类加载器(Application ClassLoader):这个加载器的实现类是sun.misc.Launcher$AppClassLoader,它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。
这三个类加载器有一定的关系,可以认为是父子关系,Application ClassLoader的父亲是Extension ClassLoader,Extension的父亲是Bootstrap ClassLoader,注意不是父子继承关系,而是父子委派关系,子ClassLoader有一个变量parent指向父ClassLoader,在子ClassLoader加载类时,一般会首先通过父ClassLoader加载,具体来说,在加载一个类时,基本过程是:
- 判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个ClassLoader加载一次。
- 如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。
- 在父ClassLoader没有加载成功的前提下,自己尝试加载类。
这个过程一般被称为”双亲委派“模型,即优先让父ClassLoader去加载。为什么要先让父ClassLoader去加载呢?这样,可以避免Java类库被覆盖的问题,比如用户程序也定义了一个类java.lang.String,通过双亲委派,java.lang.String只会被Bootstrap ClassLoader加载,避免自定义的String覆盖Java类库的定义。
需要了解的是,”双亲委派”虽然是一般模型,但也有一些例外,比如:
- 自定义的加载顺序:尽管不被建议,自定义的ClassLoader可以不遵从”双亲委派”这个约定,不过,即使不遵从,以”java”开头的类也不能被自定义类加载器加载,这是由Java的安全机制保证的,以避免混乱。
- 网状加载顺序:在OSGI框架中,类加载器之间的关系是一个网,每个OSGI模块有一个类加载器,不同模块之间可能有依赖关系,在一个模块加载一个类时,可能是从自己模块加载,也可能是委派给其他模块的类加载器加载。
- 父加载器委派给子加载器加载:典型的例子有JNDI服务(Java Naming and Directory Interface),它是Java企业级应用中的一项服务,具体我们就不介绍了。
一个程序运行时,会创建一个Application ClassLoader,在程序中用到ClassLoader的地方,如果没有指定,一般用的都是这个ClassLoader,所以,这个ClassLoader也被称为系统类加载器(System ClassLoader)。
理解ClassLoader
类ClassLoader是一个抽象类,Application ClassLoader和Extension ClassLoader的具体实现类分别是sun.misc.Launcher$AppClassLoader和sun.misc.Launcher$ExtClassLoader,Bootstrap ClassLoader不是由Java实现的,没有对应的类。
每个Class对象都有一个方法,可以获取实际加载它的ClassLoader,方法是:
1 | public ClassLoader getClassLoader() |
ClassLoader有一个方法,可以获取它的父ClassLoader:
1 | public final ClassLoader getParent() |
如果ClassLoader是Bootstrap ClassLoader,返回值为null。
比如:
1 | public class ClassLoaderDemo { |
输出为:
1 | sun.misc.Launcher$AppClassLoader |
ClassLoader有一个静态方法,可以获取默认的系统类加载器:
1 | public static ClassLoader getSystemClassLoader() |
ClassLoader中有一个主要方法,用于加载类:
1 | public Class<?> loadClass(String name) throws ClassNotFoundException |
比如:
1 | ClassLoader cl = ClassLoader.getSystemClassLoader(); |
需要说明的是,由于委派机制,Class的getClassLoader()方法返回的不一定是调用loadClass的ClassLoader,比如,上面代码中,java.util.ArrayList实际由BootStrap ClassLoader加载,所以返回值就是null。
ClassLoader vs Class.forName
在反射一节,我们介绍过Class的两个静态方法forName:
1 | public static Class<?> forName(String className) |
第一个方法使用系统类加载器加载,第二个指定ClassLoader,参数initialize表示,加载后,是否执行类的初始化代码(如static语句块),没有指定默认为true。
ClassLoader的loadClass方法与forName方法都可以加载类,它们有什么不同呢?基本是一样的,不过,有一个不同,ClassLoader的loadClass不会执行类的初始化代码,看个例子:
1 | public class CLInitDemo { |
使用ClassLoader加载静态内部类Hello,Hello有一个static语句块,输出”hello”,运行该程序,类被加载了,但没有任何输出,即static语句块没有被执行。如果将loadClass的语句换为:
1 | Class<?> cls = Class.forName(className); |
则static语句块会被执行,屏幕将输出”hello”。
类加载应用 - 可配置的策略
可以通过ClassLoader的loadClass或Class.forName自己加载类,但什么情况需要自己加载类呢?
很多应用使用面向接口的编程,接口具体的实现类可能有很多,适用于不同的场合,具体使用哪个实现类在配置文件中配置,通过更改配置,不用改变代码,就可以改变程序的行为,在设计模式中,这是一种策略模式。
自定义ClassLoader
基本用法
Java类加载机制的强大之处在于,我们可以创建自定义的ClassLoader,自定义ClassLoader是Tomcat实现应用隔离、支持JSP,OSGI实现动态模块化的基础。
怎么自定义呢?一般而言,继承类ClassLoader,重写findClass就可以了。怎么实现findClass呢?使用自己的逻辑寻找class文件字节码的字节形式,找到后,使用如下方法转换为Class对象:
1 | protected final Class<?> defineClass(String name, byte[] b, int off, int len) |
name表示类名,b是存放字节码数据的字节数组,有效数据从off开始,长度为len。
看个例子:
1 | public class MyClassLoader extends ClassLoader { |
MyClassLoader从BASE_DIR下的路径中加载类,它使用了我们在57节介绍的BinaryFileUtils读取文件,转换为byte数组。MyClassLoader没有指定父ClassLoader,默认是系统类加载器,即ClassLoader.getSystemClassLoader()的返回值,不过,ClassLoader有一个可重写的构造方法,可以指定父ClassLoader:
1 | protected ClassLoader(ClassLoader parent) |
用途
MyClassLoader有什么用呢?将BASE_DIR加到classpath中不就行了,确实可以,这里主要是演示基本用法,实际中,可以从Web服务器、数据库或缓存服务器获取bytes数组,这就不是系统类加载器能做到的了。
不过,不把BASE_DIR放到classpath中,而是使用MyClassLoader加载,确实有一个很大的好处,可以创建多个MyClassLoader,对同一个类,每个MyClassLoader都可以加载一次,得到同一个类的不同Class对象,比如:
1 | MyClassLoader cl1 = new MyClassLoader(); |
cl1和cl2是两个不同的ClassLoader,class1和class2对应的类名一样,但它们是不同的对象。
这到底有什么用呢?
- 可以实现隔离,一个复杂的程序,内部可能按模块组织,不同模块可能使用同一个类,但使用的是不同版本,如果使用同一个类加载器,它们是无法共存的,不同模块使用不同的类加载器就可以实现隔离,Tomcat使用它隔离不同的Web应用,OSGI使用它隔离不同模块。
- 可以实现热部署,使用同一个ClassLoader,类只会被加载一次,加载后,即使class文件已经变了,再次加载,得到的也还是原来的Class对象,而使用MyClassLoader,则可以先创建一个新的ClassLoader,再用它加载Class,得到的Class对象就是新的,从而实现动态更新。