热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

java类加载器ClassLoader详解

类加载器是用于加载类的对象,ClassLoader是一个抽象类。如果我们给定了一个类的二进制名称,类加载器应尝试去定位或生成构成定义类的数据。一种典型的策略是将给定的二进制名称转换为文件名,然后去文件系统中读取这个文件名所对应的class。

然而,有些类并不是来自于文件;它们是从其它来源得到的,比如网络,或者是由应用本身构建【动态代理】。定义类(defineClass )方法会将字节数组转换为Class的实例,这个新定义类的实例可以由Class.newInstance创建。

由类加载器创建的对象的方法和构造方法可能引用其它的类,为了确定被引用的类,Java虚拟机会调用最初创建类的类加载器的loadClass方法。

二进制名称:以字符串参数的形式向CalssLoader提供的任意一个类名,必须是一个二进制的名称,包含以下四种情况

  • "java.lang.String" 正常类
  • "javax.swing.JSpinner$DefaultEditor" 内部类
  • "java.security.KeyStore\(Builder\)FileBuilder$1" KeyStore的内部类Builder的内部类FileBuilder的第一个匿名内部类
  • "java.net.URLClassLoader$3$1" URLClassLoader类的第三个匿名内部类的第一个匿名内部类

代码一:

public class Test12 {
    public static void main(String[] args) {
        String[] strings = new String[6];
        System.out.println(strings.getClass().getClassLoader());
        // 运行结果:null

        Test12[] test12s = new Test12[1];
        System.out.println(test12s.getClass().getClassLoader());
        // 运行结果:sun.misc.Launcher$AppClassLoader@18b4aac2

        int[] ints = new int[2];
        System.out.println(ints.getClass().getClassLoader());
        // 运行结果:null
    }
}

loadClass方法

loadClass的源码如下, loadClass方法加载拥有指定的二进制名称的Class,默认按照如下顺序寻找类:

a)调用findLoadedClass(String)检查这个类是否被加载

b)调用父类加载器的loadClass方法,如果父类加载器为null,就会调用启动类加载器

c)调用findClass(String)方法寻找

使用上述步骤如果类被找到且resolve为true,就会去调用resolveClass(Class)方法

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 {
                  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 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;
  }
}

findClass方法

findClass的源码如下,findClass寻找拥有指定二进制名称的类,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass方法调用,默认返回ClassNotFoundException异常。

protected Class findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

defineClass方法

defineClass的源码如下,defineClass方法将一个字节数组转换为Class的实例。

protected final Class defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectiOnDomain= preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

自定义类加载器

/**
 * 继承了ClassLoader,这是一个自定义的类加载器
 * @author 夜的那种黑丶
 */
public class ClassLoaderTest extends ClassLoader {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
       Class clazz = loader.loadClass("classloader.Test01");
        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println(object.getClass().getClassLoader());
    }
    //------------------------------以上为测试代码---------------------------------

    /**
     * 类加载器名称,标识作用
     */
    private String classLoaderName;

    /**
     * 从磁盘读物字节码文件的扩展名
     */
    private String fileExtension = ".class";

    /**
     * 创建一个类加载器对象,将系统类加载器当做该类加载器的父加载器
     * @param classLoaderName 类加载器名称
     */
    private ClassLoaderTest(String classLoaderName) {
        // 将系统类加载器当做该类加载器的父加载器
        super();
        this.classLoaderName = classLoaderName;
    }

    /**
     * 创建一个类加载器对象,显示指定该类加载器的父加载器
     * 前提是需要有一个类加载器作为父加载器
     * @param parent 父加载器
     * @param classLoaderName 类加载器名称
     */
    private ClassLoaderTest(ClassLoader parent, String classLoaderName) {
        // 显示指定该类加载器的父加载器
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    /**
     * 寻找拥有指定二进制名称的类,重写ClassLoader类的同名方法,需要自定义加载器遵循双亲委托机制
     * 该方法会在检查完父类加载器之后被loadClass方法调用
     * 默认返回ClassNotFoundException异常
     * @param className 类名
     * @return Class的实例
     * @throws ClassNotFoundException 如果类不能被找到,抛出此异常
     */
    @Override
    protected Class findClass(String className) throws ClassNotFoundException {
        byte[] data = this.loadClassData(className);
        /*
         * 通过defineClass方法将字节数组转换为Class
         * defineClass:将一个字节数组转换为Class的实例,在使用这个Class之前必须要被解析
         */
        return this.defineClass(className, data, 0 , data.length);
    }

    /**
     * io操作,根据类名找到对应文件,返回class文件的二进制信息
     * @param className 类名
     * @return class文件的二进制信息
     * @throws ClassNotFoundException 如果类不能被找到,抛出此异常
     */
    private byte[] loadClassData(String className) throws ClassNotFoundException {
        InputStream inputStream = null;
        byte[] data;
        ByteArrayOutputStream byteArrayOutputStream = null;

        try {
            this.classLoaderName = this.classLoaderName.replace(".", "/");
            inputStream = new FileInputStream(new File(className + this.fileExtension));
            byteArrayOutputStream = new ByteArrayOutputStream();

            int ch;
            while (-1 != (ch = inputStream.read())) {
                byteArrayOutputStream.write(ch);
            }

            data = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            throw new ClassNotFoundException();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (byteArrayOutputStream != null) {
                    byteArrayOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }
}

以上是一段自定义类加载器的代码,我们执行这段代码

classloader.Test01@7f31245a
sun.misc.Launcher$AppClassLoader@18b4aac2

可以看见,这段代码中进行类加载的类加载器还是系统类加载器(AppClassLoader)。这是因为jvm的双亲委托机制造成的,private ClassLoaderTest(String classLoaderName)将系统类加载器当做我们自定义类加载器的父加载器,jvm的双亲委托机制使自定义类加载器委托系统类加载器完成加载。

改造以下代码,添加一个path属性用来指定类加载位置:

public class ClassLoaderTest extends ClassLoader {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
        loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
        Class clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz);

        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println(object.getClass().getClassLoader());
    }
    //------------------------------以上为测试代码---------------------------------

    /**
     * 从指定路径加载
     */
    private String path;

    ......
    
    /**
     * io操作,根据类名找到对应文件,返回class文件的二进制信息
     * @param className 类名
     * @return class文件的二进制信息
     * @throws ClassNotFoundException 如果类不能被找到,抛出此异常
     */
    private byte[] loadClassData(String className) throws ClassNotFoundException {
        InputStream inputStream = null;
        byte[] data;
        ByteArrayOutputStream byteArrayOutputStream = null;

        className = className.replace(".", "/");

        try {
            this.classLoaderName = this.classLoaderName.replace(".", "/");
            inputStream = new FileInputStream(new File(this.path + className + this.fileExtension));
            byteArrayOutputStream = new ByteArrayOutputStream();

            int ch;
            while (-1 != (ch = inputStream.read())) {
                byteArrayOutputStream.write(ch);
            }

            data = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            throw new ClassNotFoundException();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (byteArrayOutputStream != null) {
                    byteArrayOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    public void setPath(String path) {
        this.path = path;
    }
}

运行一下

class:class classloader.Test01
classloader.Test01@7f31245a
sun.misc.Launcher$AppClassLoader@18b4aac2

修改一下测试代码,并删除工程下的Test01.class文件

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
   loader.setPath("/home/fanxuan/桌面/");
    Class clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz);

    Object object = clazz.newInstance();
    System.out.println(object);
    System.out.println(object.getClass().getClassLoader());
}

运行一下

class:class classloader.Test01
classloader.Test01@135fbaa4
classloader.ClassLoaderTest@7f31245a

分析

改造后的两块代码,第一块代码中加载类的是系统类加载器AppClassLoader,第二块代码中加载类的是自定义类加载器ClassLoaderTest。是因为ClassLoaderTest会委托他的父加载器AppClassLoader加载class,第一块代码的path直接是工程下,AppClassLoader可以加载到,而第二块代码的path在桌面目录下,所以AppClassLoader无法加载到,然后ClassLoaderTest自身尝试加载并成功加载到。如果第二块代码工程目录下的Test01.class文件没有被删除,那么依然是AppClassLoader加载。

再来测试一块代码

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
    loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
    Class clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz.hashCode());

    Object object = clazz.newInstance();
    System.out.println(object.getClass().getClassLoader());

    ClassLoaderTest loader2 = new ClassLoaderTest("loader");
    loader2.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
    Class clazz2 = loader2.loadClass("classloader.Test01");
    System.out.println("class:" + clazz2.hashCode());

    Object object2 = clazz2.newInstance();
    System.out.println(object2.getClass().getClassLoader());
}

结果显而易见,类由系统类加载器加载,并且clazz和clazz2是相同的。

class:2133927002
sun.misc.Launcher$AppClassLoader@18b4aac2
class:2133927002
sun.misc.Launcher$AppClassLoader@18b4aac2

再改造一下

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
    loader.setPath("/home/fanxuan/桌面/");
    Class clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz.hashCode());

    Object object = clazz.newInstance();
    System.out.println(object.getClass().getClassLoader());

    ClassLoaderTest loader2 = new ClassLoaderTest("loader2");
    loader2.setPath("/home/fanxuan/桌面/");
    Class clazz2 = loader2.loadClass("classloader.Test01");
    System.out.println("class:" + clazz2.hashCode());

    Object object2 = clazz2.newInstance();
    System.out.println(object2.getClass().getClassLoader());
}

运行结果

class:325040804
classloader.ClassLoaderTest@7f31245a
class:621009875
classloader.ClassLoaderTest@45ee12a7

ClassLoaderTest是显而易见,但是clazz和clazz2是不同的,这是因为类加载器的命名空间的原因。

我们可以通过设置父类加载器来让loader和loader2处于同一命名空间

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
    loader.setPath("/home/fanxuan/桌面/");
    Class clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz.hashCode());

    Object object = clazz.newInstance();
    System.out.println(object.getClass().getClassLoader());

    ClassLoaderTest loader2 = new ClassLoaderTest(loader, "loader2");
    loader2.setPath("/home/fanxuan/桌面/");
    Class clazz2 = loader2.loadClass("classloader.Test01");
    System.out.println("class:" + clazz2.hashCode());

    Object object2 = clazz2.newInstance();
    System.out.println(object2.getClass().getClassLoader());
}

运行结果

class:325040804
classloader.ClassLoaderTest@7f31245a
class:325040804
classloader.ClassLoaderTest@7f31245a

扩展:命名空间

1. 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成

2. 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类

3. 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

,大量的免费Java入门教程,欢迎在线学习!

以上就是java类加载器ClassLoader详解的详细内容,更多请关注其它相关文章!


推荐阅读
  • 在使用 MUI 框架进行应用开发时,开发者常常会遇到 mui.init() 和 mui.plusReady() 这两个方法。本文将详细解释它们的区别及其在不同开发环境下的应用。 ... [详细]
  • 精选15款免费扁平化界面设计资源
    高质量的网页和移动元素是任何Web或移动项目的基础。优秀的UI设计组件不仅能够提升用户体验,还能显著缩短设计师的工作时间,帮助他们快速创建出引人注目的作品。本文将分享一系列免费的扁平化设计素材。 ... [详细]
  • 深入理解ExtJS:从入门到精通
    本文详细介绍了ExtJS的功能及其在大型企业前端开发中的应用。通过实例和详细的文件结构解析,帮助初学者快速掌握ExtJS的核心概念,并提供实用技巧和最佳实践。 ... [详细]
  • 本文汇集了一系列具有强烈设计感的网站模板,特别是来自知名平台WrapBootstrap的响应式网站模板。这些模板不仅美观,而且功能强大,适合各种类型的网站建设需求。 ... [详细]
  • Spring Boot 中静态资源映射详解
    本文深入探讨了 Spring Boot 如何简化 Web 应用中的静态资源管理,包括默认的静态资源映射规则、WebJars 的使用以及静态首页的处理方法。通过本文,您将了解如何高效地管理和引用静态资源。 ... [详细]
  • 本文详细探讨了Java中的ClassLoader类加载器的工作原理,包括其如何将class文件加载至JVM中,以及JVM启动时的动态加载策略。文章还介绍了JVM内置的三种类加载器及其工作方式,并解释了类加载器的继承关系和双亲委托机制。 ... [详细]
  • 本文探讨了2019年前端技术的发展趋势,包括工具化、配置化和泛前端化等方面,并提供了详细的学习路线和职业规划建议。 ... [详细]
  • Asp.net MVC 中 Bundle 配置详解:合并与压缩 JS 和 CSS 文件
    本文深入探讨了 Asp.net MVC 中如何利用 Bundle 功能来合并和压缩 JavaScript 和 CSS 文件,提供了详细的配置步骤和示例代码,适合开发人员参考学习。 ... [详细]
  • 本文深入探讨了CART(分类与回归树)的基本原理及其在随机森林中的应用。重点介绍了CART的分裂准则、防止过拟合的方法、处理样本不平衡的策略以及其在回归问题中的应用。此外,还详细解释了随机森林的构建过程、样本均衡处理、OOB估计及特征重要性的计算。 ... [详细]
  • 本文将详细介绍如何在Bootstrap 5中使用五种不同的表单控件样式,包括输入框、选择器和文本区域等元素。 ... [详细]
  • window下kafka的安装以及测试
    目录一、安装JDK(需要安装依赖javaJDK)二、安装Kafka三、测试参考在Windows系统上安装消息队列kafka一、安装JDKÿ ... [详细]
  • Kafka Topic 数据管理与清理策略
    本文探讨了在生产环境中如何有效管理和定期清理Kafka Topic中的数据。介绍了基于时间、日志大小和日志起始偏移量三种清除方式,并重点讲解了基于时间的清除策略及其配置方法。 ... [详细]
  • 在使用 Spring Cloud Config 作为配置中心时,若在配置文件中指定了请求路径但未能生效,本文将探讨其原因及解决方案。 ... [详细]
  • 修复了Datepicker组件中,当字段值默认为null时,输入验证器直到选择日期后才被调用的问题。 ... [详细]
  • 前端常用的布局类型——前端布局
    1.Static静态布局固定宽高:2.Liquid流式布局宽高用百分比,按屏幕分辨率调整,布局不发生变化3.Adaptive自适应 ... [详细]
author-avatar
曾军78930
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有