浅谈Java自定义类加载器及JVM自带的类加载器之间的交互关系

 更新时间:2021年02月22日 08:39:33   作者:小萝莉_Lolita  
这篇文章主要介绍了浅谈Java自定义类加载器及JVM自带的类加载器之间的交互关系,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

JVM自带的类加载器:

其关系如下:

其中,类加载器在加载类的时候是使用了所谓的“父委托”机制。其中,除了根类加载器以外,其他的类加载器都有且只有一个父类加载器。

关于父委托机制的说明:

当生成 一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器将成为该类加载器的父类加载器

下面,自定义类加载器。自定义的类加载器必须继承java.lang.ClassLoader类

import java.io.*;
public class MyClassLoader extends ClassLoader {
  private String name;  //类加载器的名字
  private String path;  //加载类的路径
  private final String fileType = ".class"; //class文件的扩展名
  public MyClassLoader(String name){
    super(); //让系统类加载器成为该类加载器的父 类加载器,该句可省略不写
    this.name = name;
  }
  public MyClassLoader(ClassLoader parent,String name){
    super(parent); //显示指定该类加载器的父 类加载器
    this.name = name;
  }
  @Override
  public String toString() {
    return this.name;
  }
  public String getPath() {
    return path;
  }
  public void setPath(String path) {
    this.path = path;
  }
  //实现自定义的类加载器必须重写findClass方法,否则ClassLoader类中的findClass()方法是抛出了异常
  @Override
  public Class findClass(String name)throws ClassNotFoundException{
    byte[] data = this.loadClassData(name);
    return this.defineClass(name,data,0,data.length);
  }
  private byte[] loadClassData(String name){
    InputStream is = null;
    byte[] data = null;
    ByteArrayOutputStream baos = null;
    try {
      this.name = this.name.replace(".","\\"); //com.dream.it---->com\dream\it
      is = new FileInputStream(new File(path + name + fileType));
      int ch;
      while(-1 != (ch = is.read())){
        baos.write(ch);  //将数据写入到字节数组输出流对象中去
      }
      data = baos.toByteArray();
    } catch (Exception e) {
      e.printStackTrace();
    }finally {
      try {
        is.close();
        baos.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    return data;
  }
  public static void main(String[] args) throws Exception {
    MyClassLoader loader1 = new MyClassLoader("loader1");
    loader1.setPath("d:/myapp/serverlib/");
    MyClassLoader loader2 = new MyClassLoader(loader1,"loader2"); //loader1作为loader2的父 类加载器
    loader2.setPath("d:/myapp/clientlib");
    MyClassLoader loader3 = new MyClassLoader(null,"loader3");//父类加载器为null,表明其父类加载器为根类加载器
    loader3.setPath("d:/myapp/otherlib");
    test(loader2);
    test(loader3);
  }
  public static void test(ClassLoader cl) throws Exception {
    Class clazz = cl.loadClass("Sample");
    Object object = clazz.newInstance();
  }
}

附上findClass()方法的JDK说明

protected Class<?> findClass(String name) throws ClassNotFoundException
Finds the class with the specified binary name. 
This method should be overridden by class loader
implementations that follow the delegation model 
for loading classes, and will be invoked by the 
loadClass method after checking the parent class
loader for the requested class. The default 
implementation throws a ClassNotFoundException. 

大致说明一下意思:通过指定的name来查找类。该方法应该被类加载器的实现类重写,从而能够保证在加载类的时候可以遵循委托机制模型。在loadClass()方法(该方法是由JVM调用的)中,检查其父类加载器之后,该方法再被调用去加载请求的类。默认该方法的实现是抛出了一个ClassNotFoundException异常。

其实,所谓的加载类,无非就是读取.class文件到内存中,所以在findClass()方法中,loadClassData()方法用于读取.class文件的数据,并返回一个字节数组。然后利用ClassLoader类的defineClass()方法将字节数组转换为Class对象。

上述自定义的类加载器loader1,loader2,loader3及JVM自带的类加载器之间的关系如下:

对于各个类加载器,系统的类加载器是从环境变量classpath中读取.class文件实现类的加载;loader1是从目录d:/myapp/serverlib/下读取.class文件;loader2是从目录d:/myapp/clientlib/下读取.class文件,loader3是从目录d:/myapp/otherlib/下读取.class文件

执行结果:

此处我们分析一下出现这种执行结果的原因:

当执行loader2.loadClass(“Sample”)时先由它上层的所有父类加载器尝试加载Sample类。

loader1从D:\myapp\serverliv目录下成功加载了Sample类,所以loader1是Sample类的定义类加载器,loader1和loader2是Sample类的初始类加载器。

当执行loader3.loadClass(“Sample”)时,先由它上层的所有父类加载器尝试加载Sample类。

loader3的父加载器为根类加载器,它无法加载Sample类,接着loader3从D:\myapp\otherlib目录下成功加载Sample类,所以loader3是Sample类的定义类加载器及初始类加载器。

在Sample类中主动使用了Dog类(new Dog()),当执行Sample类的构造方法中的new Dog()语句时,JVM需要先加载Dog类,到底用哪个类加载器家在呢?

从上述的打印结果中可以看出,加载Sample类的loader1还加载了Dog类,JVM会用Sample类的定义类加载器去加载Dog类,加载过程中也同样采用了父亲委托机制。

为了验证这一点,可以吧D:\myapp\serverlib目录下Dog.class文件删除,然后在D:\myapp\syslib目录下存放一个Dog.class文件,此时打印结果如下:

Sample:loader1
Dog:sun.misc.Launcher$AppClassLoader@1b84c92
Sample:loader3
Dog:loader3

由此可见,当由loader1加载的Sample类首次主动使用Dog类时,Dog类由系统类加载器加载,如果把D:\myapp\serverlib和D:\myapp\syslib目录下的Dog.class文件都删除,然后在D:\myapp\client目录下存放一个Dog.class文件。

此时文件结构如下图所示:

当Loader1加载Sample类首次主动使用Dog类时,由于loader1及其父类加载器都无法加载Dog类,因此test(loader2)会抛出ClassNotFoundExcption.

这又是因为什么原因呢?

这又牵扯到命名空间的问题。

同一个命名空间内的类时相互可见的。

子加载器的命名空间包含所有父类加载器的命名空间,因此由子加载器加载的类能看见父类加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。由父加载器加载的类不能看见子加载器加载的类。

如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。

对于上述问题,loader1可以加载Sample类,而Dog类只能由loader2加载Dog类,loader1是Loader2的父类加载器,父加载器loader1加载的类Sample不能看见子加载器loader2加载的类Dog,所以会抛出异常。

对于上述实例中的main方法,我们不调用test方法,换成如下代码

Class clazz = loader1.loadClass("Sample");
Object obj = clazz.newInstance();
Sample sample = (Sample)obj;
System.out.println(sample.v1);

MyClassLoader类由系统类加载器加载,而Sample类由loader1类加载器加载,所以MyClassLoader类看不见Sample类。在MyClassLoader类的main方法中使用Sample类,会导致NoClassFoundError错误。

当两个不同命名空间内的类相互不可见时,可采用Java反射机制来访问对象实例的属性和方法。

将上述代码修改:

Class clazz = loader1.loadClass("Sample");
Object obj = clazz.newInstance();
Field field = clazz.getField("v1");
int v1 = field.getInt(obj);
System.out.println(v1);

此时,可以获取到对象中的v1属性值。利用反射机制,我们可以跨越这种命名空间的限制。

补充:

命名空间:

运行时包:

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。如有错误或未考虑完全的地方,望不吝赐教。

相关文章

  • 一篇文章解决Java异常处理

    一篇文章解决Java异常处理

    这篇文章主要给大家介绍了关于如何通过一篇文章解决Java异常处理的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • 运行java的class文件方法详解

    运行java的class文件方法详解

    这篇文章主要详细介绍了运行java的class文件方法的相关资料,需要的朋友可以参考下
    2015-02-02
  • Java BigDecimal使用方法详解

    Java BigDecimal使用方法详解

    Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理
    2022-12-12
  • 批量上传Jar包到Maven私服的工具的方法

    批量上传Jar包到Maven私服的工具的方法

    这篇文章主要介绍了批量上传Jar包到Maven私服的工具的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • Java持久层框架Mybatis入门详细教程

    Java持久层框架Mybatis入门详细教程

    Mybatis持久层框架支持自定义SQL、存储过程以及高级映射,可以通过XML或注解来配置和映射原始类型、接口和Java POJOs为数据库中的记录,接下来通过本文给大家介绍Java持久层框架Mybatis入门详细教程,一起学习下吧
    2021-06-06
  • servlet之session简介_动力节点Java学院整理

    servlet之session简介_动力节点Java学院整理

    这篇文章主要介绍了servlet之session简介,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-07-07
  • 一文详解如何在Java Maven项目中使用JUnit 5进行测试

    一文详解如何在Java Maven项目中使用JUnit 5进行测试

    这篇文章主要介绍了如何在Java Maven项目中使用JUnit 5进行测试的相关资料,JUnit5是一个流行的Java测试框架,它涵盖了JUnit5的概述、环境配置、编写测试用例、运行测试、高级特性和最佳实践,需要的朋友可以参考下
    2025-04-04
  • MyBatis如何使用(一)

    MyBatis如何使用(一)

    这篇文章主要介绍了MyBatis如何使用(一)的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-07-07
  • SpringBoot 2.x整合Log4j2日志的详细步骤

    SpringBoot 2.x整合Log4j2日志的详细步骤

    log4j2优越的性能其原因在于log4j2使用了LMAX,一个无锁的线程间通信库代替了,logback和log4j之前的队列,并发性能大大提升,下面这篇文章主要给大家介绍了关于SpringBoot 2.x整合Log4j2日志的相关资料,需要的朋友可以参考下
    2022-10-10
  • 如何对quartz定时任务设置结束时间

    如何对quartz定时任务设置结束时间

    这篇文章主要介绍了如何对quartz定时任务设置结束时间问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12

最新评论