简单理解一下双亲委派机制

  看文章标题就知道是比较难理解的知识点,所以原理先行,让大家先有个大体认识。
双亲委派分成两部分是双亲和委派,委派就是委托的意思,这里的双亲是递进关系,举个简单的例子,这里的双亲不是爹地妈咪,而是爷爷跟你爹。

既然与类加载过程扯到一起了,那一定与类加载有关了。没错双亲委派机制简单说就是一个类需要加载,会先委托给它的父亲(它爹地)加载,父亲会委托它父亲(它爷爷)加载。大概是这样:

类加载过程

我们来看看下面这段代码是如何加载的:

public class CreateObjectTest {public static void main(String[] args) {A a = new A();}
}class A{static {System.out.println("静态成员在构造前面执行");}public A() {System.out.println("构造方法在静态后面执行");}
}

  下图是上述代码的大体流程图,我们先来看下左边红框中的五个步骤:

  1. 程序是运行在内存里的,Java是高级语言不能直接调用内存,所以由C++程序调用内存创建虚拟机
  2. 启动类加载器不是由Java自身创建的,是由C++代码创建的
  3. C++初始化的类是Launcher,一说到类加载,必定提到双亲委派,所以初始化启动类加载器时,会顺带把扩展类加载器与应用程序加载器一起初始化
  4. 学习反射时我们知道一个类在JVM中只能有一个Class实例,三个类加载器只能有一个加载(我们自己写的大都是应用程序加载器加载的),每个类加载器的职责可以查看第一篇文章或百度
  5. 这个加载过程咋往下看

  第5步骤的详细过程如下(反射的文章里有提到):

  1. 加载:**JVM除了固定加载的一些类(比如八大基本类型的字节码对象)外,其他类都是按需加载(**用到了才会加载,也就是new的时候),比如上述代码我们new了一个A对象,那么后面就会去加载这个类
  2. 验证:这个过程主要是来验证字节码文件的正确性(需要补补,应该是前面一些数字是一样的)
  3. 准备:这个过程是隐式初始化,这个时候变量都会被赋予一个初始值(默认值),比如int类型是0等等
  4. 解析:这一步做的是将符号引用替换成直接引用。符号引用大家就可以理解为那些变量名啊,方法名啊都是英文符号组成的,实际这些符号都会指向内存中的某个地址,这个地址就是直接引用
  5. 初始化:int a = 10;这一步的初始化就是显示初始化了,将a的值从第三步的0改成10

扩展: 类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的 引用、对应class实例的引用等信息。
类加载器的引用:这个类到类加载器实例的引用
对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的 对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

进一步理解双亲委派机制

  这里主要以源码展开,看看双亲委派用代码是怎么实现的。

public Launcher() {// 声明一个扩展类加载器Launcher.ExtClassLoader var1;try {// 初始化扩展类加载器,指定null为父加载器(启动类加载器)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);// 沙箱加载的一些类???String var2 = System.getProperty("java.security.manager");if (var2 != null) {SecurityManager var3 = null;if (!"".equals(var2) && !"default".equals(var2)) {try {var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();} catch (IllegalAccessException var5) {} catch (InstantiationException var6) {} catch (ClassNotFoundException var7) {} catch (ClassCastException var8) {}} else {var3 = new SecurityManager();}if (var3 == null) {throw new InternalError("Could not create SecurityManager: " + var2);}System.setSecurityManager(var3);}}

  第一次看这个源码我产生了两个问题:

  1. 这两个加载器如何认爹的呢?
  2. 扩展类加载器与应用程序加载器的初始化顺序可以改变吗?

我们看看源码来解释第一个问题,至于第二个问题请大家看完第一个问题的解答后自己思考。

加载器如何建立与父加载器的关系

首先我观察了Launcher的类结构,它有4个内部类,其中包括扩展类加载器与应用程序加载器,从初始化Launcher的代码中可以看出创建两个类加载器的工作的由具体加载器的方法来实现的。我们来看看两个类加载器的继承体系

(AppClassLoader继承了URLClassLoader,idea不给画线我就自己来)几个类之间的关系是扩展类加载器,应用程序加载器 -> URLClassLoader -> SecureClassLoader-> ClassLoader。

扩展类加载器

  先从扩展类加载器的加载过程说起,从Launcher的构造方法中可以看到扩展类加载器是通过Launcher的静态内部类ExtClassLoader的_getExtClassLoade_r方法创建的_。_我们先来看下这个方法的大概执行流程图(建议看完图自己去点一点,再往下看):

  从图里可以看出,扩展类加载器的构造经过多次调用,最终会调用ClassLoader的构造方法,而该构造方法的第一行就是this.**parent **= parent;这一行代码就是子类加载器与父类建立关系的关键代码。关键在于这个parent参数是多少,往回找我们会发现ExtClassLoader的构造中传递的parent的值是null,也就是说扩展类加载器的父加载器被设置成null。

  设置成null的原因是启动类加载器是由C++创建的,所以拿不到这个对象,故而扩展类加载器的父加载器定义为null。

应用程序加载器

  应用程序加载器的加载过程与扩展类加载器基本一致,从Launcher的构造方法中可以看到扩展类加载器是通过Launcher的静态内部类AppClassLoader的_getAppClassLoader_方法创建的_。_我们先来看下这个方法的大概执行流程图(建议看完图自己去点一点,再往下看):

  流程与扩展类加载器是一致的,最终都是调用ClassLoader的构造方法,需要注意的是AppClassLoader的getAppClassLoader方法是需要传入参数的。这个参数传的是扩展类加载器,根据调用过程,应用程序加载器的父加载器是扩展类加载器。

保姆级源码讲解:类加载如何实现双亲委派

  实现类加载时的双亲委派的主要方法是是ClassLoader中的loadClasss方法,大家可以看下代码及注释,注释应该解释清楚了

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loaded// 检查当前被加载的类是否已加载,是的话直接返回Class<?> c = findLoadedClass(name);// 如果没有加载过,则走双亲委派的流程if (c == null) {long t0 = System.nanoTime();try {// 是否有父类加载器,是则交由父类加载器加载类if (parent != null) {c = parent.loadClass(name, false);} else {// 扩展类执行的oadClass也是这个方法,它的父加载器初始化的时候是null所以最终类会由下方代码加载c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}// 如果扩展类与应用程序加载器都不加载,就交由子类自己加载(走了一圈)if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}}

  我们说双亲委派时,如果已经加载过一次的类就不会再加载而是直接返回,那么程序中是如何判断一个类是否加载了呢?
在类加载之前都会调用ClassLoader#findLoadedClass方法,这个方法如下回去判断类是否加载。这个方法的作用是如果此加载器已被 Java 虚拟机记录为具有该二进制名称的类的启动加载器,则返回具有给定二进制名称的类。否则返回 null。

protected final Class<?> findLoadedClass(String name) {if (!checkName(name))return null;return findLoadedClass0(name);
}private native final Class<?> findLoadedClass0(String name);

  除了主要方法外还有个方法是URLClassLoader#findClass,我翻译了下这个方法的注释,大家可以跟着看下代码:从 URL 搜索路径中查找并加载具有指定名称的类。任何引用 JAR 文件的 URL 都会根据需要加载和打开,直到找到该类。

protected Class<?> findClass(final String name)throws ClassNotFoundException{final Class<?> result;try {result = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {public Class<?> run() throws ClassNotFoundException {String path = name.replace('.', '/').concat(".class");Resource res = ucp.getResource(path, false);if (res != null) {try {return defineClass(name, res);} catch (IOException e) {throw new ClassNotFoundException(name, e);}} else {return null;}}}, acc);} catch (java.security.PrivilegedActionException pae) {throw (ClassNotFoundException) pae.getException();}if (result == null) {throw new ClassNotFoundException(name);}return result;}

自定义类加载器

  自定义类加载器需要我们继承抽象类ClassLoader,重写loadClass方法,当然,其他方法你也可以重写,比如这里的findClass方法。

public class Cat {public void play(String name) {System.out.println(name + "在玩游戏!");}
}
public class MyClassLoader extends ClassLoader {// 加载字节码文件的路径private String loadClassPath;public MyClassLoader(String loadClassPath) {this.loadClassPath = loadClassPath;}/*** 字符串转成byte数组** @param className 字节码名称* @return*/public byte[] stringToByte(String className) throws IOException {String classPath = loadClassPath + "/" + className.replaceAll("\\.", "/") + ".class";FileInputStream inputStream = new FileInputStream(classPath);int lenth = inputStream.available();byte[] bytes = new byte[lenth];inputStream.read(bytes);inputStream.close();return bytes;}@SneakyThrows@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] data = stringToByte(name);return defineClass(name, data, 0, data.length);}// 复制的ClassLoader的方法,删除了双亲委派部分代码@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();long t1 = System.nanoTime();// 直接用当前类加载器加载c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}if (resolve) {resolveClass(c);}return c;}}// 使用自己的类加载器加载Cat类public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {MyClassLoader myClassLoader = new MyClassLoader("E:/电子书");Class<?> clazz = myClassLoader.loadClass("com.evader.evaderjvm.jvm.Cat");Object obj = clazz.newInstance();Method method = clazz.getMethod("play", String.class);method.invoke(obj, "旺财");System.out.println(clazz.getClassLoader().getClass().getName());}
}

  大家经常听到一个名词叫沙箱安全机制,这里我们就演示一下

  从gif图中可以看到我用自定义加载器加载java.lang.Object类(这个字节码文件拿的就是rt.jar包中的),发现加载不了。像这种核心API是不可以随便加载的,如果可以随便加载就会被侵入,后果非常严重,现在对沙箱安全有进一步的理解了吧。

Tomcat:打破双亲委派机制

  Tomcat是web容器,我们的项目通常都是部署在web容器上,但是容器在大多数情况下都是部署多个服务的,并且web容器也有自己依赖的库(Tomcat也是一个应用程序)。如果Tomcat不打破双亲委派机制就产生一些问题:

  1. 多个服务不同版本,但是类只会被加载一次(由于类加载时只认包名与类名)
  2. Tomcat本身也有自己的依赖库,应该与运行在tomcat上的程序所依赖的库相互独立,实际跟第一条差不多

  实际Tomcat也有自己的类加载器,也有自己的委派关系,这里不做说明,下面案例将模拟一个JVM加载两份实例(即模拟Tomcat打破双亲委派)。
  在演示沙箱安全的代码基础下,修改了Cat.class并放到了一个新的目录下。然后用类加载器分别去实例化两个目录下的Cat类。

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {MyClassLoader2 myClassLoader = new MyClassLoader2("E:/电子书");Class<?> clazz = myClassLoader.loadClass("com.evader.evaderjvm.jvm.Cat");Object obj = clazz.newInstance();Method method = clazz.getMethod("play", String.class);method.invoke(obj, "第一只小狗");System.out.println(clazz.getClassLoader().getClass().getName());System.out.println(clazz.getClassLoader());System.out.println("-------------------------------------------------------------");MyClassLoader2 myClassLoader2 = new MyClassLoader2("E:/电子书/test");Class<?> clazz2 = myClassLoader2.loadClass("com.evader.evaderjvm.jvm.Cat");Object obj2 = clazz2.newInstance();Method method2 = clazz2.getMethod("play", String.class);method2.invoke(obj2, "第二只小狗");System.out.println(clazz2.getClassLoader().getClass().getName());System.out.println(clazz2.getClassLoader());
}// 运行结果
第一只小狗在玩游戏!
com.evader.evaderjvm.jvm.MyClassLoader2
com.evader.evaderjvm.jvm.MyClassLoader2@5a07e868
-------------------------------------------------------------
这是修改后编译的文件!!!第二只小狗在玩游戏!
com.evader.evaderjvm.jvm.MyClassLoader2
com.evader.evaderjvm.jvm.MyClassLoader2@3fee733d
public class Cat {public void play(String name) {System.out.println("这是修改后编译的文件!!!"+name + "在玩游戏!");}
}

  这个案例我通过改变字节码文件内容模拟同一个类的不同版本,通过加载两个不同目录下的类来模拟同一个JVM加载两份同一路径下的类。

  问题来了,我们如何来辨别在JVM中存在两份Cat实例呢?

JVM中,判断两个类对象是否是同一个,不光要看类的包名和类名是否都相同之,还需要他们的类
加载器对象也是同一个。从上述的执行结果中可以看出我们的类加载器对象是不一样的。

类加载过程与双亲委派机制相关推荐

  1. java类加载过程(双亲委派机制)

    类加载运行全过程 通过Java命令执行代码的大体流程如下: 其中loadClass的类加载过程有如下几步: 加载 >> 验证 >> 准备 >> 解析 >> ...

  2. JVM 虚拟机类加载过程和双亲委派机制

    Java 编译后的字节码 Class 文件加载到虚拟机后才能运行和使用. 一.类加载过程 包括三个步骤, Loading 加载,Linking 链接,Initializing 初始化:第二步又可以细分 ...

  3. java类加载过程,双亲委派机制

    1.双亲委派机制 Parent Delegation Model 又称为父级委托模型.想要了解它,还需理解类加载机制.类加载器.类加载器的层级关系. 2.类加载机制: 编译器把Java源文件编译成.c ...

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

    转载自 深入JVM系列(三)之类加载.类加载器.双亲委派机制与常见问题 一.概述 定义:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用 ...

  5. java 类加载 双亲委派_Java类加载器和双亲委派机制

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

  6. 【JVM】类加载器:双亲委派机制、沙箱安全机制

    · 双亲委派机制.沙箱安全机制是JVM中类加载器系统的相关术语 · 在这之前,应该先了解JVM类加载器系统的相关概念 一.类加载器基础知识 见下图1,java文件首先会被编译成class文件,clas ...

  7. (二)JVM成神路之剖析Java类加载子系统、双亲委派机制及线程上下文类加载器

    引言 上篇<初识Java虚拟机>文章中曾提及到:我们所编写的Java代码经过编译之后,会生成对应的class字节码文件,而在程序启动时会通过类加载子系统将这些字节码文件先装载进内存,然后再 ...

  8. android双亲委派机制,类加载器及双亲委派机制

    一. 类的加载 java class是由class loader 类加载器加载到JVM内存中的. 看下下面的demo,java中有三种类加载器. 首先,我们自己定义的这个classLoaderDemo ...

  9. JVM入门(位置、体系结构、类加载器、双亲委派机制、沙箱安全机制、Native、PC寄存器、方法区、堆(新生区{伊甸园区、幸存区}、养老区、永久区)、OOM、GC算法、JMM)

    目录 一.JVM的位置 二.JVM的体系结构 三.类加载器 1.类加载器举例 2. JVM中提供了三层的ClassLoader 3. 双亲委派机制(重要) 3.1 工作原理 3.2.优点 四.沙箱安全 ...

最新文章

  1. 离开百度三年多,吴恩达纽交所敲钟,身价再增20亿
  2. Java开发面试题及答案,5年crud“经验
  3. 牛客题霸 NC3 链表中环的入口结点
  4. arcgis python实例_arcgis二次开发_arcgis二次开发python_arcgis二次开发实例
  5. 2019 CCPC - 网络选拔赛 A题^^
  6. hive内部表和外部表的区别_走近大数据之Hive进阶(四、Hive的表连接)
  7. python语言高空坠球_高空坠物打击体验装置制造方法
  8. monty python读音-Monty Python
  9. JSONKit去警告
  10. java与模式观察者模式_谈谈java中的观察者模式
  11. dede产生.php,怎么加快织梦dedeCMS内容生成速度
  12. 数据库多表查询关联查询SQL语句
  13. 一键解决Windows聚焦不更新的方法
  14. Oracle 监控索引使用率脚本分享
  15. 用批量重命名技巧把文件夹下多个文件快速重命名
  16. Excel 添加图片批注
  17. Nagios基本介绍
  18. 怎样开发微信小程序(最初的页面)
  19. STM32CubeIDE 复制工程
  20. python3爬取头条比基尼图片

热门文章

  1. android:打气筒功能View.inflate
  2. 探索Apache Hudi核心概念 (3) - Compaction
  3. Linux为文件夹及其子文件赋权限
  4. Web开发技术应用系统设计报告
  5. 解锁网易云音乐的灰色歌曲、会员歌曲
  6. office 2013 出现Microsoft Office 无法找到此应用程序的许可证。修复尝试失败或者已被取消
  7. 笔记-常见考点-事业环境因素包括(但不限于)
  8. java工程师应届生工资一般多少,附架构师必备技术详解
  9. 京瓷打印机1025默认管理员密码_c8520,京瓷打印一体机C8520管理员账号密码多少,怎么设置......
  10. 04.如何搭建优惠券模板服务