所谓的类加载器(Class Loader)就是加载Java类到Java虚拟机中的,前面《面试官,不要再问我“Java虚拟机类加载机制”了》中已经介绍了具体加载class文件的机制。本篇文章我们重点介绍加载器和双亲委派机制。

类加载器

在JVM中有三类ClassLoader构成:启动类(或根类)加载器(Bootstrap ClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader)。不同的类加载器负责不同区域的类的加载。

启动类加载器:这个加载器不是一个Java类,而是由底层的c++实现,负责将存放在JAVA_HOME下lib目录中的类库,比如rt.jar。因此,启动类加载器不属于Java类库,无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。

扩展类加载器:由sun.misc.Launcher$ExtClassLoader实现,负责加载JAVA_HOME下lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用类加载器:由sun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫系统类加载器。它负责加载用户类路径上所指定的类库,可以被直接使用。如果未自定义类加载器,默认为该类加载器。

可以通过这种方式打印加载路径及相关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#loadClass源码

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、代码热部署等领域。

Java9类加载器修改

以上类加载器模型为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虚拟机类加载器及双亲委派机制

《面试官》系列文章:

程序新视界:精彩和成长都不容错过
![程序新视界-微信公众号](https://img2018.cnblogs.com/blog/1742867/201910/1742867-20191013111755842-2090947098.png)

Java虚拟机类加载器及双亲委派机制的更多相关文章

  1. Java 基础 类加载器和双亲委派机制 学习笔记

    转自博客:https://blog.csdn.net/weixin_38118016/article/details/79579657 文章不是我写的,但是感觉写的挺通俗易懂的,然后防止以后丢失,就转 ...

  2. Java类加载器和双亲委派机制

    前言 之前详细介绍了Java类的整个加载过程(类加载机制详解).虽然,篇幅较长,但是也不要被内容吓到了,其实每个阶段都可以用一句话来概括. 1)加载:查找并加载类的二进制字节流数据. 2)验证:保证被 ...

  3. 深入JVM系列(三)之类加载、类加载器、双亲委派机制与常见问题

    一.概述   定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型.类加载和连接的过程都是在运行期间完成的. 二. 类的 ...

  4. Java自定义类加载器与双亲委派模型

    其实,双亲委派模型并不复杂.自定义类加载器也不难!随便从网上搜一下就能搜出一大把结果,然后copy一下就能用.但是,如果每次想自定义类加载器就必须搜一遍别人的文章,然后复制,这样显然不行.可是自定义类 ...

  5. 【Java_基础】java类加载过程与双亲委派机制

    1.类的加载.连接和初始化 当程序使用某个类时,如果该类还未被加载到内存中,则系统会通过加载.连接.初始化三个步骤来对类进行初始化.如果没有意外,jvm将会连续完成这三个步骤,有时也把这三个步骤统称为 ...

  6. 深入理解java虚拟机(九)类加载器以及双亲委派模型

    虚拟机把类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到虚拟机外部去实现,以便让程序自己决定如何去获取所需要的类.实现这个动作的代码模块称为“类加载器”. 类与类加载器 任 ...

  7. JVM类加载机制详解(二)类加载器与双亲委派模型

    在上一篇JVM类加载机制详解(一)JVM类加载过程中说到,类加载机制的第一个阶段加载做的工作有: 1.通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件).而获取的方式,可 ...

  8. 深入理解JVM(③)虚拟机的类加载器(双亲委派模型)

    前言 先解释一下什么是类加载器,通过一个类的全限定名来获取描述该类的二进制字节流,在虚拟机中实现这个动作的代码被称为"类加载器(Class Loader)". 类与类加载器 类加载 ...

  9. 类文件的结构、JVM 的类加载过程、类加载机制、类加载器、双亲委派模型

    一.类文件的结构 我们都知道,各种不同平台的虚拟机,都支持 "字节码 Byte Code" 这种程序存储格式,这构成了 Java 平台无关性的基石.甚至现在平台无关性也开始演变出 ...

随机推荐

  1. [c++] 面试题之犄角旮旯 第壹章

    记录C/C++语言相关的问题. 算法可视化:https://visualgo.net/en <data structure and algorithm in c++> By Adam 有免 ...

  2. Java面试-interrupt

    我们都知道,Java中停止一个线程不能用stop,因为stop会瞬间强行停止一个线程,且该线程持有的锁并不能释放.大家多习惯于用interrupt,那么使用它又有什么需要注意的呢? interrupt ...

  3. linux mint 17编译android 2.3.1错误记录

    有转载这里的也有添加的. ################# Fix 1 ########################## Error: frameworks/base/include/utils ...

  4. 无暇代码(js的整洁之道)

    如果你关注代码本身和代码的编写方式,而不是只关心它是否能工作,那么你写代码是有一定的水准.专业开发人员将为未来的自己和“其他人”编写代码,而不仅仅只编写当前能工作就行的代码.在此基础上,简洁代码可以定 ...

  5. 【Unity与Android】02-在Unity导出的Android工程中接入Google Admob广告

    我在上一篇文章 [Unity与Android]01-Unity与Android交互通信的简易实现) 中介绍了Unity与Android通讯的基本方法. 这一篇开始进入应用阶段,这次要介绍的是如何在An ...

  6. 虚拟机中设置 CentOS 静态 IP

    作为开发人员在工作和学习中通过虚拟机使用 Linux 的情况肯定会非常多,但是 Linux 自带的终端使用体验较差,所以绝大多数使用者都会在宿主机上使用第三方 SSH工具(例如 Xshell)进行连接 ...

  7. SpringBootSecurity学习(16)前后端分离版之 OAuth2.0 加密配置

    示例代码的改进 前面使用spring cloud security和spring cloud oauth2写了一个第三方授权的例子,例子非常的简单,主要目的是用来熟悉OAuth2.0 申请授权的整个流 ...

  8. Centos6 日常使用小结

    网络配置目录 1./etc/sysconfig/network-script/ifcfg-eth0 2.netstat -rn Kernel IP routing table Destination ...

  9. 2019.10.15 CSP初赛知识点整理

    初赛需要的知识点整理如下: (1)计算机的硬件组成与基本常识 (2)单位/进制的转换 (3)进制/逻辑运算相关 (4)概率与期望 (5)排序的各种性质 (6)简单数据结构的使用(栈.队列.链表等) ( ...

  10. 安装Elasticsearch可视化插件

    背景 项目中使用Elasticsearch , 最开始14年使用的时候需要es自己安装插件才能通过web页面查看数据情况,目前新版本的ES安装插件很费劲,通过搜索发现目前谷歌浏览器就有这个插件,这里简 ...