Java虚拟机类加载器及双亲委派机制

简介: Java虚拟机类加载器及双亲委派机制

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

类加载器

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

image.png启动类加载器:这个加载器不是一个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);       //...}

双亲委派模型

双亲委派模型:当一个类加载器接收到类加载请求时,会先请求其父类加载器加载,依次递归,当父类加载器无法找到该类时(根据类的全限定名称),子类加载器才会尝试去加载。

image.png双亲委派中的父子关系一般不会以继承的方式来实现,而都是使用组合的关系来复用父加载器的代码。

通过编写测试代码,进行debug,可以发现双亲委派过程中不同类加载器之间的组合关系。image.png而这一过程借用一张时序图来查看会更加清晰。 image.png

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中类加载器已经发生了变化。在这里主要简单介绍一下相关模型的变化,具体变化细节就不再这里展开了。image.png在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中更深层次的变化,大家可以进一步研究一下。该系列持续更新中,欢迎关注微信公众号“程序新视界”。

目录
相关文章
|
6天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
22 2
|
11天前
|
Java 编译器
探索Java中的异常处理机制
【10月更文挑战第35天】在Java的世界中,异常是程序运行过程中不可避免的一部分。本文将通过通俗易懂的语言和生动的比喻,带你了解Java中的异常处理机制,包括异常的类型、如何捕获和处理异常,以及如何在代码中有效地利用异常处理来提升程序的健壮性。让我们一起走进Java的异常世界,学习如何优雅地面对和解决问题吧!
|
22天前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
35 5
Java反射机制:解锁代码的无限可能
|
10天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
16天前
|
安全 IDE Java
Java反射Reflect机制详解
Java反射(Reflection)机制是Java语言的重要特性之一,允许程序在运行时动态地获取类的信息,并对类进行操作,如创建实例、调用方法、访问字段等。反射机制极大地提高了Java程序的灵活性和动态性,但也带来了性能和安全方面的挑战。本文将详细介绍Java反射机制的基本概念、常用操作、应用场景以及其优缺点。 ## 基本概念 ### 什么是反射 反射是一种在程序运行时动态获取类的信息,并对类进行操作的机制。通过反射,程序可以在运行时获得类的字段、方法、构造函数等信息,并可以动态调用方法、创建实例和访问字段。 ### 反射的核心类 Java反射机制主要由以下几个类和接口组成,这些类
35 2
|
21天前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
22 3
|
21天前
|
安全 Java UED
深入理解Java中的异常处理机制
【10月更文挑战第25天】在编程世界中,错误和意外是不可避免的。Java作为一种广泛使用的编程语言,其异常处理机制是确保程序健壮性和可靠性的关键。本文通过浅显易懂的语言和实际示例,引导读者了解Java异常处理的基本概念、分类以及如何有效地使用try-catch-finally语句来处理异常情况。我们将从一个简单的例子开始,逐步深入到异常处理的最佳实践,旨在帮助初学者和有经验的开发者更好地掌握这一重要技能。
20 2
|
23天前
|
Java 数据库连接 开发者
Java中的异常处理机制####
本文深入探讨了Java语言中异常处理的核心概念,通过实例解析了try-catch语句的工作原理,并讨论了finally块和throws关键字的使用场景。我们将了解如何在Java程序中有效地管理错误,提高代码的健壮性和可维护性。 ####
|
25天前
|
安全 Java 程序员
深入浅出Java中的异常处理机制
【10月更文挑战第20天】本文将带你一探Java的异常处理世界,通过浅显易懂的语言和生动的比喻,让你在轻松阅读中掌握Java异常处理的核心概念。我们将一起学习如何优雅地处理代码中不可预见的错误,确保程序的健壮性和稳定性。准备好了吗?让我们一起踏上这段旅程吧!
24 6
|
23天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
28 1