深入理解Java类加载器

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

当JVM启动的时候,Java开始使用如下三种类型的类加载器:

  1. 启动(Bootstrap)类加载器:启动类加载器是用本地代码实现的类加载器,它负责将JAVA_HOME/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。具体可由启动类加载器加载到的路径可通过System.getProperty(“sun.boot.class.path”)查看。
  2. 扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器,具体可由扩展类加载器加载到的路径可通过System.getProperty(“java.ext.dirs”)查看。
  3. 系统(System)类加载器:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径)下的类库加载到内存中。开发者可以直接使用系统类加载器,具体可由系统类加载器加载到的路径可通过System.getProperty(“java.class.path”)查看。

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

JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归 (本质上就是loadClass函数的递归调用),因此所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。事实上,大多数情况下,越基础的类由越上层的加载器进行加载,因为这些基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API(当然,也存在基础类回调用户用户代码的情形,即破坏双亲委派模型的情形)。 关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。
在这里插入图片描述

上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器

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

2.扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null(null 本身就代表着引导类加载器)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

事实上,这就是启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系。


三、Java 程序动态扩展方式

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

1、反射 (调用java.lang.Class.forName(…)加载类)
2、用户自定义类加载器

调用java.lang.Class.forName(…)返回的是调用者的类加载器。显然,调用者的类加载器不一定是系统类加载器,比如我们使用了自定义类加载器。

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

public class TestBean {

public static void main(String[] args) throws Exception {
    // 一个简单的类加载器,逆向双亲委派机制
    // 可以加载与自己在同一路径下的Class文件
	ClassLoader myClassLoader = 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 (Exception e) {
				throw new ClassNotFoundException(name);
			}
		}
	};

	Object obj = myClassLoader.loadClass("classloader.test.bean.TestBean")
			.newInstance();
	System.out.println(obj.getClass());
	System.out.println(obj instanceof classloader.test.bean.TestBean);
}

}/* Output:
class classloader.test.bean.TestBean
false
*///:~

我们发现,obj 确实是类classloader.test.bean.TestBean实例化出来的对象,但当这个对象与类classloader.test.bean.TestBean做所属类型检查时却返回了false。这是因为虚拟机中存在了两个TestBean类,一个是由系统类加载器加载的,另一个则是由我们自定义的类加载器加载的,虽然它们来自同一个Class文件,但依然是两个独立的类,因此做所属类型检查时返回false。


四. 常见问题分析
  1. 问:在编写自定义类加载器时,如果没有设定父加载器,那么父加载器是谁?
    答:在不指定父类加载器的情况下,默认采用系统类加载器。可能有人觉得不明白,现在我们来看一下JDK对应的代码实现。众所周知,我们编写自定义的类加载器直接或者间接继承自java.lang.ClassLoader抽象类。

即使用户自定义类加载器不指定父类加载器,那么,同样可以加载如下三个地方的类:

  • <Java_Runtime_Home>/lib下的类;
  • <Java_Runtime_Home>/lib/ext下或者由系统变量java.ext.dir指定位置中的类;
  • 当前工程类路径下或者由系统变量java.class.path指定位置中的类。
  1. 问:在编写自定义类加载器时,如果将父类加载器强制设置为null,那么会有什么影响?如果自定义的类加载器不能加载指定类,就肯定会加载失败吗?
    答:JVM规范中规定如果用户自定义的类加载器将父类加载器强制设置为null,那么会自动将启动类加载器设置为当前用户自定义类加载器的父类加载器(这个问题前面已经分析过了)。同时,我们可以得出如下结论:即使用户自定义类加载器不指定父类加载器,那么,同样可以加载到<JAVA_HOME>/lib下的类,但此时就不能够加载<JAVA_HOME>/lib/ext目录下的类了。

  1. 在JVM规范和JDK文档中(1.2或者以后版本中),都没有建议用户覆写loadClass(…)方法,相比而言,明确提示开发者在开发自定义的类加载器时覆写findClass(…)逻辑。

五. 开发自己的类加载器

在前面介绍类加载器的代理委派模型的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类,这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用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虚拟机中运行的类来。下面将通过两个具体的实例来说明类加载器的开发。
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类的实例。加载本地文件系统上的类。


六、线程上下文类加载器

Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。JDBC SPI mysql的实现如下所示。
在这里插入图片描述
SPI机制带来的问题

Java 提供了很多服务SPI,允许第三方为这些接口提供实现。这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现则是由各供应商来完成。终端只需要将所需的实现作为 Java 应用所依赖的 jar 包包含进类路径(CLASSPATH)就可以了。问题在于SPI接口中的代码经常需要加载具体的实现类:SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;而SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的(因为它只加载 Java 的核心库),按照双亲委派模型,启动类加载器无法委派系统类加载器去加载类。也就是说,类加载器的双亲委派模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。Java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过此类加载器来加载类和资源。

JDBC中的DriverManager加载Driver的步骤顺序依次是:

  1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用线程上下文类加载器加载;
  2. 通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载。

Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader(启动类加载器只能加载java核心类库)。这时候只能使用线程上下文类加载器了:把自己加载不了的类加载到线程上下文类加载器中(通过Thread.currentThread()获取),而线程上下文类加载器默认是使用系统类加载器AppClassLoader。

ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

到这儿差不多把SPI机制解释清楚了。直白一点说就是:我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载。但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的线程上下文类加载器里,后续你想怎么操作就是你的事了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值