阅读 145

JVM 阅读JDK中ClassLoader注释,以及自定义类加载器

前言

  • 学习就应该阅读官方写JavaDoc,JavaDoc是第一手资料,应该是准确无疑的。
  • 本篇以翻译ClassLoader类中文档注释为主,如有不通顺,请自行看英文注释。

英文版

image.png

翻译

  • 类加载器是负责加载类的对象。 ClassLoader 类是一个抽象类。 给定类的二进制名称(binary name),类加载器应该尝试定位或生成(locate or generate)构成类定义的数据。 典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。

批注:

  • 给定类的名称,类加载器会找到定义对应类的数据.
  • binary name: 其实就是字符串
    • "java.lang.String"
    • "javax.swing.JSpinner$DefaultEditor"( $ 后面跟着是内部类的类名)
    • "java.security.KeyStore$Builder$FileBuilder$1"( $1 类中的第一个匿名内部类)
    • "java.net.URLClassLoader$3$1"(URLClassLoader的第3个匿名内部类中第一个匿名内部类)
  • 定位或生成(locate or generate): 说定位是因为类存在磁盘的某个地方等待被加载,说生成是因为有些类是动态生成的,如动态代理。
  • 每个Class对象都包含定义它的ClassLoader的reference(引用)。

批注:在Class类中有成员变量ClassLoader,所以Class类有getClassLoader()方法。

  • 数组类的类对象不是由类加载器创建的,而是根据 Java 运行时的需求自动创建的。 Class.getClassLoader()返回的数组类的类加载器与其元素类型的类加载器相同; 如果元素类型是原始类型,则数组类没有类加载器。

    • 样例说明
    public class ClassLoaderTest3 {
      public static void main(String[] args) {
          String[] strings = new String[2];
          System.out.println(strings.getClass().getClassLoader());//元素类型是String,由BootStrap加载
          System.out.println("---------------------");
          ClassLoaderTest3[] classLoaderTest3s = new ClassLoaderTest3[2];
          System.out.println(classLoaderTest3s.getClass().getClassLoader());
          System.out.println("---------------------");
          int[] a = new int[2];
          System.out.println(a.getClass().getClassLoader());
       }
    }
    复制代码
    • 结果

    image.png

  • 应用程序实现(继承)ClassLoader,用以扩展 Java 虚拟机动态加载类的方式。

批注:自定义类加载器的目的是扩展 Java 虚拟机动态加载类的方式,在默认情况是以双亲委托机制来加载类的,如果你不喜欢双亲委托机制,就自行继承ClassLoader,实现不一样的加载方式。

  • 安全管理器通常可以使用类加载器来指示安全域。

批注:俺看不懂,自行百度。

  • ClassLoader类使用委托模型来搜索类和资源。 ClassLoader 的每个实例都有一个关联的父类加载器。 当请求查找类或资源时, ClassLoader实例会将类或资源的搜索委托给其父类加载器,然后再尝试查找类或资源本身。 虚拟机的内置类加载器,称为“引导类加载器(Bootstrap ClassLoader)”,它本身没有父级,但可以作为ClassLoader实例的父级。
  • 支持并发加载类的类加载器被称为具有并行能力的类加载器,并且需要在类初始化时通过调用ClassLoader.registerAsParallelCapable方法来注册自己,使其可以并行加载类。 请注意, ClassLoader类默认注册为具有并行能力。 但是,虽然它们具有并行能力,它的子类仍然需要注册自己
  • 在委托模型不是严格分层的环境中,类加载器需要具有并行能力否则类加载会导致死锁,因为加载器锁在类加载过程中一直保持着(参见loadClass方法)。
  • 通常,Java 虚拟机以平台相关的方式从本地文件系统加载类。 例如,在 UNIX 系统上,虚拟机从CLASSPATH环境变量定义的目录中加载类。
  • 但是,有些类可能不是来自文件; 它们可能来自其他来源,例如网络,或者它们可以由应用程序构建。 方法 defineClass 将一个字节数组转换为Class 类的一个实例。 可以使用 Class.newInstance 创建这个新定义的类的实例
  • 类加载器创建的对象的方法和构造函数中有引用其他类。 为了确定所引用的类,Java 虚拟机调用最初创建类的类加载器的loadClass方法。

批注:说一个对象里面的方法有使用其它的类,如,method(){return new String("hehe");},引用了String类。

来一些代码来增加一下理解

自定义类加载器

  • 在开始之前先提一下,上面的文档说了:
    • defineClass 将一个字节数组转换为Class 类的一个实例。 可以使用 Class.newInstance 创建这个新定义的类的实例
    • defineClass 参数
    image.png
    • 再说了,需要让类加载器找到需要加载的类,它才能加载,上面提到的binary name就很关键。
  • 这个自定义代码有些问题,先看看
public class UserClassLoader extends ClassLoader {
    private String classLoaderName;
    private final String SUFFIX = ".class";
    private String path;
    
    public void setPath(String path) {
       this.path = path;
    }

    public UserClassLoader(String classLoaderName) {//构造方法
        super();//以系统类加载器为该加载器的父加载器
        this.classLoaderName = classLoaderName;
    }
    //构造方法,可以指定父类加载器的构造方法
    public UserClassLoader(ClassLoader parent, String classLoaderName) {
        super(parent);//显式定义该类加载器的父加载器
        this.classLoaderName = classLoaderName;
    }

    @Override
    public String toString() {
        return "classLoaderName='" + classLoaderName;
    }

    @Override
    protected Class<?> findClass(String className) {//重写
        byte[] data = loaderClassData(className);
        return defineClass(className,data,0,data.length);//字节数组变成Class
    }
    private byte[] loaderClassData(String className) {//将class文件变成字节数组
        InputStream in = null;
        ByteArrayOutputStream outputStream = null;
        byte[] data = null;
        try {
            className = className.replace(".", "/");
            in = new FileInputStream(path + className + SUFFIX);//文件输入流
            outputStream = new ByteArrayOutputStream();//字节输出流
            int ch;
            while ((ch = in.read()) != -1) {
                outputStream.write(ch);
            }
            data = outputStream.toByteArray();//字节数组
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            Optional.ofNullable(in).ifPresent(i -> {
                try {
                    i.close();//关闭流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            Optional.ofNullable(outputStream).ifPresent(i -> {
                try {
                    i.close();//关闭流
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        }
        return data;//返回字节数组。数组内容就是类
    }

    public static void main(String[] args) throws Exception {
        UserClassLoader userClassLoader = new UserClassLoader("userClassLoader");
        UserClassLoader.test(userClassLoader);
    }
    public static void test(ClassLoader classLoader) throws Exception {//测试
       //指定要加载的class文件,不能是当前类,类名是binary name
        Class<?> clazz = classLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");
        Object object = clazz.newInstance();//生成类的实例
        System.out.println(object);//打印
    }
}
复制代码
  • 结果,毫无疑问就是类名

image.png

那么问题来了,上面自定义的类加载器有被使用吗

  • 在上面的代码中,在findClass方法中添加一下打印语句
    @Override
    protected Class<?> findClass(String className) {
        System.out.println("findClass invoke: "+className);
        System.out.println("classLoader name: "+classLoaderName);
        byte[] data = loaderClassData(className);
        return defineClass(className,data,0,data.length);
    }
    复制代码
  • 结果,加载类的时候并没有使用自定义类加载去加载。

image.png

  • 再在test方法中打印一下类是由哪个加载器加载的
        public static void test(ClassLoader classLoader) throws Exception {
          //加载类,不能是当前类
           Class<?> clazz = classLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");
           Object object = clazz.newInstance();
           System.out.println(object);
           System.out.println(object.getClass().getClassLoader());
    复制代码
  • 结果,该类是由系统(App)类加载器加载的

image.png

为什么会出现自定义类加载器没有被使用的情况

  • 可以看到在main方法里,实例化自定义类加载器时,用的是第一个构造方法。
    public UserClassLoader(String classLoaderName) {//构造方法 
        super();//以系统类加载器为该加载器的父加载器 
        this.classLoaderName = classLoaderName;
    }
    复制代码
  • 根据双亲委托机制, 当使用自定义类加载器加载类时,它会委托其父类加载器(AppClassLoader,即系统类加载器)去加载。所以,最终是系统类加载器加载的"com.jvmstudy.classloading.ClassLoaderTest"

更进一步

  • 系统类加载器是加载当前类路径下的类,就说.java文件编译后的.class文件默认输出的路径

image.png

  • 删除默认路径下加载的class文件。

image.png

image.png

  • main方法如下:
    public static void main(String[] args) throws Exception {
          UserClassLoader userClassLoader = new UserClassLoader("userClassLoader");
          userClassLoader.setPath("C:/Users/25852/Desktop/");
          //不能是当前类
          Class<?> clazz = userClassLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");
          Object object = clazz.newInstance();
          System.out.println("class HashCode: "+clazz.hashCode());
          System.out.println(object.getClass().getClassLoader());
      } 
    
    复制代码
  • 结果:

image.png

  • 原因何在
    • 根据双亲委托机制,还是自定义类加载器不会自己先加载类,但是,其父类加载器都无法加载类,根类加载器和扩展类加载器就不用说了肯定加载不了,系统类加载器也加载不了,因为默认路径中的class文件已经被删除了。
    • 最终,都委托了一遍,就只能是自定义类自己加载了,我们已经告诉自定义类加载去桌面加载类了。

重新编译一下,将原本删除的class文件重新生成一下

  • main方法改编如下:
    public static void main(String[] args) throws Exception {
          UserClassLoader userClassLoader = new UserClassLoader("userClassLoader");
          Class<?> clazz = userClassLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类
          Object object = clazz.newInstance();
          System.out.println("class HashCode: "+clazz.hashCode());
          System.out.println(object.getClass().getClassLoader());
          System.out.println("-----------------------");
          UserClassLoader userClassLoader2 = new UserClassLoader("userClassLoader2");
          Class<?> clazz1 = userClassLoader2.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类
          Object object1 = clazz1.newInstance();
          System.out.println("class HashCode: "+clazz.hashCode());
          System.out.println(object1.getClass().getClassLoader());
      }
    复制代码
  • 结果:

image.png

  • 说明同一个类只会被加载一次。

老样子,删掉默认路径下的class,将其移到桌面

  • main方法改编如下:
    public static void main(String[] args) throws Exception {
          UserClassLoader userClassLoader = new UserClassLoader("userClassLoader");
          userClassLoader.setPath("C:/Users/25852/Desktop/");
          Class<?> clazz = userClassLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类
          Object object = clazz.newInstance();
          System.out.println("class HashCode: "+clazz.hashCode());
          System.out.println(object.getClass().getClassLoader());
          System.out.println("-----------------------");
          UserClassLoader userClassLoader1 = new UserClassLoader("userClassLoader1");
          userClassLoader1.setPath("C:/Users/25852/Desktop/");
          Class<?> clazz1 = userClassLoader1.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类
          Object object1 = clazz1.newInstance();
          System.out.println("class HashCode: "+clazz1.hashCode());
          System.out.println(object1.getClass().getClassLoader());
      }
    复制代码
  • 结果:

image.png

  • 前面才说同一个类只会被加载一次,这里怎么就不对了?
    • 这就涉及到类加载器的命名空间问题了。
    • 下亿篇博客再介绍命名空间问题。
文章分类
后端
文章标签