一、类加载器的基本概念
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
二、类与类加载器
对于任意一个类,都需要加载它的类加载器和这个类本身来确定这个类在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间。也就是说,如果比较两个类是否是同一个类,除了这比较这两个类本身的全限定名是否相同之外,还要比较这两个类是否是同一个类加载器加载的。即使同一个类文件两次加载到同一个虚拟机中,但如果是由两个不同的类加载器加载的,那这两个类仍然不是同一个类。
这个相等性比较会影响一些方法,比如Class对象的equals()
方法、isAssignableFrom()
方法、isInstance()
方法等,还有instanceof
关键字做对象所属关系判定等。下面的代码演示了不同的类加载器对instanceof
关键字的影响:
package temp;import java.io.IOException;import java.io.InputStream;public class ClassLoaderTest { public static void main(String[] args) throws Exception{ ClassLoader loader=new ClassLoader() { @Override public Class<?> loadClass(String name)throws ClassNotFoundException{ try{ String filename=name.substring(name.lastIndexOf(".")+1)+".class"; InputStream is=getClass().getResourceAsStream(filename); if(is==null){ return super.loadClass(name); } byte[] b=new byte[is.available()]; is.read(b); return defineClass(name,b,0,b.length); }catch(IOException e){ throw new ClassNotFoundException(name); } } }; Object obj=loader.loadClass("temp.ClassLoaderTest").newInstance(); System.out.println(obj.getClass()); System.out.println(obj.getClass().getClassLoader()); System.out.println(ClassLoaderTest.class.getClassLoader()); System.out.println(obj instanceof temp.ClassLoaderTest); } }
运行结果:
TIM截图20180809165411.png
这里构造了一个简单的类加载器,它可以加载与自己在同一个路径下的Class文件。然后使用这个类加载器去加载全限定名是temp.ClassLoaderTest
的类,并实例化了这个类的对象。从第一行输出可以看出,这个对象确实是temp.ClassLoaderTest
类的一个实例,我们打印了一下对象obj
的类加载器和ClassLoaderTest
的类加载器,发现确实是不同的两个不同的类加载器,最后输出表明在做instanceof
检查时出现了false
,这是因为这时虚拟机中有两个temp.ClassLoaderTest
类,一个是系统应用程序类加载器加载的,另一个是自定义的类加载器加载的,这两个类虽然来自同一个Class文件,但是加载它们的类加载器不同,导致类型检查时结果是false
。
三、双亲委派模型
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
启动类加载器:负责将存放在
<JAVA_HOME>\lib
目录中的,或者被-Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。扩展类加载器:这个加载器由
sun.misc.Launcher$ExtClassLoader
实现,负责加载<JAVA_HOME>\lib\ext
目录下的,或者被java.ext.dirs
系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。应用程序类加载器:这个类加载器是由
sun.misc.Launcher$AppClassLoader
实现的。由于这个类加载器是ClassLoader
中的getSystemClassLoader
方法的返回值,所以也叫系统类加载器。它负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
用户的应用程序就是在这三个类加载器的配合下加载的。不过,用户还可以加入自己的类加载器,这些类加载器的关系如下图:
image001.jpg
这种类加载的层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。不过这个父子关系不是通过继承实现的,而是使用组合关系来复用父加载器的代码。
双亲委派模型的工作过程如下:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围内没找到这个类)时,自加载器才会尝试自己加载。
双亲委派模型是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object
类,也就是说在运行的时候,java.lang.Object
这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object
类,而且这些类之间是不兼容的。通过双亲委派模型,对于 Java 核心库的类的加载工作由启动类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。
四、深入探讨双亲委派模型
双亲委派模型虽然保障了Java核心类库的安全问题,但是双亲委派模型也有其缺点,那就是如果基础类又要调用用户的代码,那该怎么办?
比如我们经常使用的JDBC技术,学习过JDBC的应该知道JDBC只是一组接口规范,具体的实现是由数据库厂商实现的,那么JDBC的接口代码存在于核心类库中,是由启动类加载器加载的,但是JDBC的实现代码是由各个厂商提供,是由系统类加载器加载。启动类加载器是无法无法找到 JDBC接口 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的双亲委派模型无法解决这个问题。
看到这里你可能会产生一个疑问,那就是为啥上层的类加载器加载的类无法访问下层类加载器加载的类,但是下层的类加载器加载的类可以访问上层类加载器加载的类,比如:我们写的类Person
由系统类加载器加载,String
类由启动类加载器加载,也就是说Person
类中可以访问到String
,但是String
类中无法访问到Person
(这里是一个不太恰当的例子,因为我们无法具体的测试String
类中是否可以访问到Person
类)。
我在看书的时候就产生了上面的疑问,经过谷歌老师的指导,我终于找到了问题的答案:
首先是两个术语:在前面介绍类加载器的双亲委派模型的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass
来实现的;而启动类的加载过程是通过调用loadClass
来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。
注意:初始类加载器对于一个类来说经常不是一个,比如String类在加载的过程中,先是交给系统类加载器加载,但是系统类加载器代理给了扩展类加载期,扩展类加载器又代理给了引导类加载器,最后由引导类加载器加载完成,那么这个过程中的定义类加载器就是引导类加载器,但是初始类加载器是三个(系统类加载器、扩展类加载器、引导类加载器),因为这三个类加载器都调用了loadClass
方法,而最后的引导类加载器还调用了defineClass
方法。
JVM为每个类加载器维护的一个“表”,这个表记录了所有以此类加载器为“初始类加载器”(而不是定义类加载器,所以一个类可以存在于很多的命名空间中)加载的类的列表。属于同一个列表的类可以互相访问。这就可以解释为什么上层的类加载器加载的类无法访问下层类加载器加载的类,但是下层的类加载器加载的类可以访问上层类加载器加载的类?的疑问了。
在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer
引用了类com.example.Inner
,则由类 com.example.Outer
的定义加载器负责启动类 com.example.Inner
的加载过程。
五、解决双亲委派模型的缺陷——线程上下文类加载器
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread
中的方法getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
通过使用线程上下文类加载器可以实现父类加载器请求子类加载器去完成类加载的动作。
这里并没有讲解为啥线程上下文类加载器可以打破双亲委派模型?为啥可以逆向使用类加载器?,如果想要了解,这里有篇文章可以参考一下:真正理解线程上下文类加载器(多案例分析)
六、另外一种加载类的方法:Class.forName
Class.forName
是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)
和 Class.forName(String className)
。第一种形式的参数 name
表示的是类的全名;initialize
表示是否初始化类;loader
表示加载时使用的类加载器。第二种形式则相当于设置了参数initialize
的值为true
,loader
的值为当前类的类加载器。Class.forName
的一个很常见的用法是在加载数据库驱动的时候。如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用来加载 Apache Derby
数据库的驱动。
七、开发自己的类加载器
一般来说,自己开发的类加载器只需要覆写findClass(String name)
方法即可。java.lang.ClassLoader
类的方法loadClass()
封装了双亲委派模型的实现。该方法会首先调用findLoadedClass()
方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass()
方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()
方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()
方法,而是覆写findClass()
方法。示例如下:
public class FileClassLoader extends ClassLoader { private String rootDir; public FileClassLoader(String rootDir) { this.rootDir = rootDir; } /** * 编写findClass方法的逻辑 * @param name * @return * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 获取类的class文件字节数组 byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { //直接生成class对象 return defineClass(name, classData, 0, classData.length); } } /** * 编写获取class文件并转换为字节码流的逻辑 * @param className * @return */ 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; } /** * 类文件的完全路径 * @param className * @return */ private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } public static void main(String[] args) throws ClassNotFoundException { String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/"; //创建自定义文件类加载器 FileClassLoader loader = new FileClassLoader(rootDir); try { //加载指定的class文件 Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj"); System.out.println(object1.newInstance().toString()); //输出结果:I am DemoObj } catch (Exception e) { e.printStackTrace(); } } }
作者:Jivanmoon
链接:https://www.jianshu.com/p/fed1c9691782