前言

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

类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。每个类加载器都有一个独立的类名称空间,所以每个类唯一性都必须是建立在是否为同一个类加载器的前提下的。

否则,即使是两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

例如:

public class ClassLoaderOneTest {

    public static void main(String[] args) throws Exception{

        ClassLoader oneLoader = new ClassLoader() {

            @Override
public Class<?> loadClass(String name) throws ClassNotFoundException { try { String classFileName = name.substring(name.lastIndexOf(".")+1)+".class"; InputStream inputStream = getClass().getResourceAsStream(classFileName); if(inputStream == null){
return super.loadClass(name);
} byte[] bytes = new byte[inputStream.available()]; inputStream.read(bytes); return defineClass(name,bytes,0,bytes.length);
}catch (IOException e){
throw new ClassNotFoundException(name); }
}
}; Object object = oneLoader.loadClass("com.eurekaclient2.test.jvm3.SonClass").newInstance(); System.out.println(object.getClass()); System.out.println("instanceof result :"+ (object instanceof com.eurekaclient2.test.jvm3.SonClass));
} }

运行结果:

class com.eurekaclient2.test.jvm3.SonClass
instanceof result :false

通过上面的运行结果可以看出,自定义的类加载器加载出来的类创建的对象和com.eurekaclient2.test.jvm3.SonClass在做类型检查时返回了false,这是因为在Java虚拟机中存在的两个SonClass,一个是由虚拟机的应用程序类加载器所加载的,另一个是由自定义的类加载器加载的,虽然来自同一个Class文件,但在Java虚拟机中是两个互相独立的类。

双亲委派模型

Java虚拟机把类加载器分为了两大类:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是虚拟机的一部分。另外一种就是其他类加载器(全部继承自java.lang.ClassLoader)。

从JDK1.2开始至JDK9之前的Java应用绝大多数都会使用到如下3个系统提供的类加载器进行加载。

  • 启动类加载器(Bootstrap Class Loader):这个类加载器复制加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。

    如果需要使用引导类加载器去加载类,直接使用null代替即可。

    如下是ClassLoader.getClassLoader()方法的源码:
/**
* Returns the class loader for the class. Some implementations may use
* null to represent the bootstrap class loader. This method will return
* null in such implementations if this class was loaded by the bootstrap
* class loader.
* /
@CallerSensitive
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}
  • 扩展类加载器(Extension Class Loader):这个类加载器是在类sun.misc.launcher$ExtClassLoader中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。这是Java系统类库的扩展机制,但是在JDK9之后,被模块化能力所替代了。
  • 应用程序类加载器(Application Class Loader):这个类加载器由sun.misc.Launcher$AppClassLoader来实现的。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如没有自定义的类加载器,这个就是默认的类加载器。

在JDK9之前的Java应用都是由这三类加载器互相配合来完成加载的,如果有自定义的类加载器,会先执行自定义的类加载器。各种的类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。

各种类加载器的关系如下图:



双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。

这里的加载器之间的子父关系不是继承,通常使用组合关系来复用父加载器的代码。

双亲委派模型并不是一个强制性约束力的模型而是Java设计者推荐给开发者的一种类加载器实现的最佳实践。

双亲委派模型的工作过程

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶端的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载才会尝试自己去完成加载。

使用双亲委派模型来组织类加载器之间的关系的好处就是能保证Java类型体系中最基础的类的唯一。

例如:类java.lang.Object无论哪一个类加载器加载最终都会委派给启动类加载器,因此能够保证各种类加载器环境中的都是同一个类。这样就能保证我们创建出来的类的拥有最基础的行为。

记得以前在面试的时候有被问到,让自己写一个java.lang.String类然后会被虚拟机加载运行吗?这个问题考察的就是类的加载机制的双亲委派模型。

双亲委派模型的源码其实非常简洁,先检查请求加载的类型是否已经被加载过,若没有则调用父加载器的loadeClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父加载器加载失败,抛出ClassNotFoundException异常,才会调用自己的findClass方法尝试进行加载。

源码如下:

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) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
} if (c == null) {
// 在父类加载器无法加载时
// 在调用本身的findClass方法来进行类加载
long t1 = System.nanoTime();
c = findClass(name); // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

破坏双亲委派模型

上面也提到了,双亲委派模型并不是一个具体强制性约束的模型,虽然在Java的世界大部分的类加载器都遵循这个模型,但也有例外的情况,直到JDK9(模块化)为止,主要出现过3次较大规模“被破坏”的情况。

  • 第一次“被破坏”其实发生在双亲委派模型出现之前,由于双亲委派模型在JDK1.2之后才被引入,但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在了。为了向前兼容,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写类的加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。
  • 第二次“被破坏”是因为自身的一些局限性导致的,双亲委派模型很好的解决了各个类加载器协作时基础类型的一致性问题,即越基础的类由越上层的加载器进行加载。

    但是如果基础的类需要调用下面的用户代码时该怎么办呢?Java设计团队用了一个不太优雅的方案,引入了一个名叫线程上下文的类加载器(Thread Context ClassLodar)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

    像JNDI、JDBC、JCE、JAXB、JBI等都是用的这种类型加载器实现的功能。最常见的tomcat中就用到了线程上下文的类加载器。
  • 第三次“破坏”,是为了实现热部署、模块化。在更新了一部分代码后,不需要停机重启,只需要将类加载器和类都替换掉就可以了。典型的就是OSGi的模块化热部署。

深入理解JVM(③)虚拟机的类加载器(双亲委派模型)的更多相关文章

  1. JVM探究之 —— 类加载器-双亲委派模型

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

  2. 【深入理解JVM】:类加载器与双亲委派模型

    类加载器 加载类的开放性 类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因.在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取定义此类的二进制字 ...

  3. JVM(16)之 双亲委派模型

    开发十年,就只剩下这套架构体系了! >>>   在上一篇博文中,我们知道了如何获得二进制的字节流,并根据获得的字节流去装载一个类.同时也了解到类加载器的存在,每个加载器对应着不同的加 ...

  4. 【深入理解Java虚拟机 】类加载器的命名空间以及类的卸载

    类加载器的命名空间 每个类加载器又有一个命名空间,由其以及其父加载器组成 类加载器的命名空间的作用和影响 每个类加载器又有一个命名空间,由其以及其父加载器组成 在每个类加载器自己的命名空间中不能出现相 ...

  5. 深入理解JVM虚拟机-2垃圾收集器

    这里讨论的收集器基于JDK 1.7 Update 14之后的HotSpot虚拟机. 如果两个收集器之间存在连线,说明可以搭配使用.虚拟机所处的区域,则表示它是属于新生代收集器还是年老代收集器.在这里我 ...

  6. JVM——类加载器的双亲委派模型

    类加载器双亲委派模型,如下图所示: 双亲委派模型的工作过程 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此 ...

  7. JVM类加载过程与双亲委派模型

    类加载过程 类加载过程为JVM将类描述数据从.class文件中加载到内存,并对数据进行解析和初始化,最终形成被JVM直接使用的Java类型.包含: 加载:获取该类的二进制字节流,将字节流代表的静态存储 ...

  8. 深入理解java虚拟机【类加载机制】

    Java虚拟机类加载过程是把Class类文件加载到内存,并对Class文件中的数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程. 在加载阶段,java虚拟机需要完成以下 ...

  9. Java面试题之类加载器有哪些?什么是双亲委派模型

    类加载器有哪些: 1.启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或被-Xbootclasspath参数所指 ...

  10. Java的类加载器都有哪些,每个类加载器都有加载那些类,什么是双亲委派模型,是做什么的?

    类加载器按照层次,从顶层到底层,分为以下三种: (1)启动类加载器(Bootstrap ClassLoader) 这个类加载器负责将存放在JAVA_HOME/lib下的,或者被-Xbootclassp ...

随机推荐

  1. Java实现 蓝桥杯VIP 算法训练 JAM计数法

    题目描述 Jam是个喜欢标新立异的科学怪人.他不使用阿拉伯数字计数,而是使用小 写英文字母计数,他觉得这样做,会使世界更加丰富多彩.在他的计数法中,每个数字的位数都是相同的(使用相同个数的字母),英文 ...

  2. Java实现 LeetCode 207 课程表

    207. 课程表 现在你总共有 n 门课需要选,记为 0 到 n-1. 在选修某些课程之前需要一些先修课程. 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1] ...

  3. Android如何使用Notification进行通知

    有两张图片素材会放在末尾 activity代码,和XML布局 package com.example.myapplication; import androidx.appcompat.app.AppC ...

  4. java实现最大公约数

    编写一函数gcd,求两个正整数的最大公约数. 样例输入: 5 15 样例输出: 5 样例输入: 7 2 样例输出: 1 package adv92; import java.util.Scanner; ...

  5. Java实现第八届蓝桥杯纸牌三角形

    纸牌三角形 题目描述 A,2,3,4,5,6,7,8,9 共9张纸牌排成一个正三角形(A按1计算).要求每个边的和相等. 下图就是一种排法(如有对齐问题,参看p1.png). A 9 6 4 8 3 ...

  6. Tomcat 配置必备的 10 个小技巧

    现在开发Java Web应用,建立和部署Web内容是一件很简单的工作.使用Jakarta Tomcat作为Servlet和JSP容器的人已经遍及全世界.Tomcat具有免费.跨平台等诸多特性,并且更新 ...

  7. python3 主机实时监控系统

    主机实时监控系统(可在局域网访问) 一.思路: 前端: 1.管理员登录(编写一个管理员登录界面) 技术:html+css 2.资源数据显示(用于显示主机资源数据情况) 插件:echarts+jquer ...

  8. [leetcode] 股票问题

    参考文章: [1] 团灭 LeetCode 股票买卖问题 [2] Most consistent ways of dealing with the series of stock problems 其 ...

  9. 基于springcloud gateway + nacos实现灰度发布(reactive版)

    什么是灰度发布? 灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式.在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B ...

  10. Linux命令总结大全,包含所有linux命令

    使用说明:此文档包含所有的Linux命令,只有你想不到的没有你看不到的,此文档共计10万余字,有8400多行,预计阅读时间差不多需要3个小时左右,所以要给大家说一说如何阅读此文档 为了方便大家阅读,我 ...