所谓的类加载器(Class Loader)就是加载Java类到Java虚拟机中的,前面《面试官,不要再问我“Java虚拟机类加载机制”了》中已经介绍了具体加载class文件的机制。本篇文章咱们重点介绍加载器和双亲委派机制。java
在JVM中有三类ClassLoader构成:启动类(或根类)加载器(Bootstrap ClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader)。不一样的类加载器负责不一样区域的类的加载。mysql
启动类加载器:这个加载器不是一个Java类,而是由底层的c++实现,负责将存放在JAVA_HOME下lib目录中的类库,好比rt.jar。所以,启动类加载器不属于Java类库,没法被Java程序直接引用,用户在编写自定义类加载器时,若是须要把加载请求委派给引导类加载器,那直接使用null代替便可。c++
扩展类加载器:由sun.misc.Launcher$ExtClassLoader实现,负责加载JAVA_HOME下libext目录下的,或者被java.ext.dirs系统变量所指定的路径中的全部类库,开发者能够直接使用扩展类加载器。面试
应用类加载器:由sun.misc.Launcher$AppClassLoader实现的。因为这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,因此也叫系统类加载器。它负责加载用户类路径上所指定的类库,能够被直接使用。若是未自定义类加载器,默认为该类加载器。sql
能够经过这种方式打印加载路径及相关jar:数据库
System.out.println("boot:" + System.getProperty("sun.boot.class.path"));
System.out.println("ext:" + System.getProperty("java.ext.dirs"));
System.out.println("app:" + System.getProperty("java.class.path"));复制代码
在打印的日志中,能够看到详细的路径以及路径下面都包含了哪些类库。因为打印内容较多,这里就不展现了。数组
除启动类加载器外,扩展类加载器和应用类加载器都是经过类sun.misc.Launcher进行初始化,而Launcher类则由根类加载器进行加载。相关代码以下:安全
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//初始化扩展类加载器,构造函数没有入参,没法获取启动类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
//初始化应用类加载器,入参为扩展类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 设置上下文类加载器
Thread.currentThread().setContextClassLoader(this.loader);
//...
}复制代码
双亲委派模型:当一个类加载器接收到类加载请求时,会先请求其父类加载器加载,依次递归,当父类加载器没法找到该类时(根据类的全限定名称),子类加载器才会尝试去加载。微信
双亲委派中的父子关系通常不会以继承的方式来实现,而都是使用组合的关系来复用父加载器的代码。并发
经过编写测试代码,进行debug,能够发现双亲委派过程当中不一样类加载器之间的组合关系。
而这一过程借用一张时序图来查看会更加清晰。
ClassLoader类是一个抽象类,但却没有包含任何抽象方法。继承ClassLoader类并重写findClass方法即可实现自定义类加载器。但若是破坏上面所述的双亲委派模型来实现自定义类加载器,则须要继承ClassLoader类并重写loadClass方法和findClass方法。
ClassLoader类的部分源码以下:
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
//进行类加载操做时首先要加锁,避免并发加载
synchronized (getClassLoadingLock(name)) {
//首先判断指定类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//若是当前类没有被加载且父类加载器不为null,则请求父类加载器进行加载操做
c = parent.loadClass(name, false);
} else {
//若是当前类没有被加载且父类加载器为null,则请求根类加载器进行加载操做
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
//若是父类加载器加载失败,则由当前类加载器进行加载,
c = findClass(name);
//进行一些统计操做
// ...
}
}
//初始化该类
if (resolve) {
resolveClass(c);
}
return c;
}
}复制代码
上面代码中也提现了不一样类加载器之间的层级及组合关系。
双亲委派模型是为了保证Java核心库的类型安全。全部Java应用都至少须要引用java.lang.Object类,在运行时这个类须要被加载到Java虚拟机中。若是该加载过程由自定义类加载器来完成,可能就会存在多个版本的java.lang.Object类,并且这些类之间是不兼容的。
经过双亲委派模型,对于Java核心库的类的加载工做由启动类加载器来统一完成,保证了Java应用所使用的都是同一个版本的Java核心库的类,是互相兼容的。
子类加载器都保留了父类加载器的引用。但若是父类加载器加载的类须要访问子类加载器加载的类该如何处理?最经典的场景就是JDBC的加载。
JDBC是Java制定的一套访问数据库的标准接口,它包含在Java基础类库中,由根类加载器加载。而各个数据库厂商的实现类库是做为第三方依赖引入使用的,这部分实现类库是由应用类加载器进行加载的。
获取Mysql链接的代码:
//加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//链接数据库
Connection conn = DriverManager.getConnection(url, user, password);复制代码
DriverManager由启动类加载器加载,它使用到的数据库驱动(com.mysql.jdbc.Driver)是由应用类加载器加载的,这就是典型的由父类加载器加载的类须要访问由子类加载器加载的类。
这一过程的实现,看DriverManager类的源码:
//创建数据库链接底层方法
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//获取调用者的类加载器
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
//由启动类加载器加载的类,该值为null,使用上下文类加载器
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
//...
for(DriverInfo aDriver : registeredDrivers) {
//使用上下文类加载器去加载驱动
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
//加载成功,则进行链接
Connection con = aDriver.driver.connect(url, info);
//...
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}
//...
}
}复制代码
在上面的代码中留意改行代码:
callerCL = Thread.currentThread().getContextClassLoader();复制代码
这行代码从当前线程中获取ContextClassLoader,而ContextClassLoader在哪里设置呢?就是在上面的Launcher源码中设置的:
// 设置上下文类加载器
Thread.currentThread().setContextClassLoader(this.loader);复制代码
这样一来,所谓的上下文类加载器本质上就是应用类加载器。所以,上下文类加载器只是为了解决类的逆向访问提出来的一个概念,并非一个全新的类加载器,本质上是应用类加载器。
自定义类加载器只须要继承java.lang.ClassLoader类,而后重写findClass(String name)方法便可,在方法中指明如何获取类的字节码流。
若是要破坏双亲委派规范的话,还需重写loadClass方法(双亲委派的具体逻辑实现)。但不建议这么作。
public class ClassLoaderTest extends ClassLoader {
private String classPath;
public ClassLoaderTest(String classPath) {
this.classPath = classPath;
}
/**
* 编写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 is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
// 读取类文件的字节码
while ((num = is.read(buffer)) != -1) {
stream.write(buffer, 0, num);
}
return stream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 类文件的彻底路径
*
* @param className
* @return
*/
private String classNameToPath(String className) {
return classPath + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
public static void main(String[] args) {
String classPath = "/Users/zzs/my/article/projects/java-stream/src/main/java/";
ClassLoaderTest loader = new ClassLoaderTest(classPath);
try {
//加载指定的class文件
Class<?> object1 = loader.loadClass("com.secbro2.classload.SubClass");
System.out.println(object1.newInstance().toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}复制代码
打印结果:
SuperClass static init
SubClass static init
com.secbro2.classload.SubClass@5451c3a8复制代码
关于SuperClass和SubClass在上篇文章《面试官,不要再问我“Java虚拟机类加载机制”了》已经贴过代码,这里就再也不贴出了。
经过上面的代码能够看出,主要重写了findClass获取class的路径便实现了自定义的类加载器。
那么,什么场景会用到自定义类加载器呢?当JDK提供的类加载器实现没法知足咱们的需求时,才须要本身实现类加载器。好比,OSGi、代码热部署等领域。
以上类加载器模型为Java8之前版本,在Java9中类加载器已经发生了变化。在这里主要简单介绍一下相关模型的变化,具体变化细节就再也不这里展开了。
java9中目录的改变。
Java9中类加载器的改变。
在java9中,应用程序类加载器能够委托给平台类加载器以及启动类加载器;平台类加载器能够委托给启动类加载器和应用程序类加载器。
在java9中,启动类加载器是由类库和代码在虚拟机中实现的。为了向后兼容,在程序中仍然由null表示。例如,Object.class.getClassLoader()仍然返回null。可是,并非全部的JavaSE平台和JDK模块都由启动类加载器加载。
举几个例子,启动类加载器加载的模块是java.base,java.logging,java.prefs和java.desktop。其余JavaSE平台和JDK模块由平台类加载器和应用程序类加载器加载。
java9中再也不支持用于指定引导类路径,-Xbootclasspath和-Xbootclasspath/p选项以及系统属性sun.boot.class.path。-Xbootclasspath/a选项仍然受支持,其值存储在jdk.boot.class.path.append的系统属性中。
java9再也不支持扩展机制。可是,它将扩展类加载器保留在名为平台类加载器的新名称下。ClassLoader类包含一个名为getPlatformClassLoader()的静态方法,该方法返回对平台类加载器的引用。
本篇文章主要基于java8介绍了Java虚拟机类加载器及双亲委派机制,和Java8中的一些变化。其中,java9中更深层次的变化,你们能够进一步研究一下。该系列持续更新中,欢迎关注微信公众号“程序新视界”。
原文连接:《Java虚拟机类加载器及双亲委派机制》
《面试官》系列文章: