美文网首页JVM学习记
JVM(三)类加载器

JVM(三)类加载器

作者: r09er | 来源:发表于2020-03-20 16:46 被阅读0次

    类的加载

    类的加载是指将类的.class文件中二进制数据读入到内存中,然后将其放在运行时数据区的方法区内,然后在内存中创建爱你一个java.lang.Class对象

    规范并没有说明Class对象应该存放在哪,HotSpot虚拟机将其放在方法区中,用来封装类在方法区内的数据结构

    加载.class文件的方式

    • 从本地系统中直接加载
    • 从网络下载.calss文件
    • 从zip,jar等归档文件中加载
    • 从专有数据库中提取
    • 将Java源文件动态编译为.class文件

    servlet技术

    类加载器

    类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用双亲委托机制,这种机制能保证Java平台的安全性.

    从源码文档中翻译应该称为父类委托模式

    类加载器并不需要等到某个类被首次主动使用时再加载它

    • JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError)
    • 如果一个类一直没有被程序主动使用,那么累加载器就不会报告错误

    JVM中的类加载器

    根加载器(Bootstrap),

    根加载器没有父加载器,主要负责虚拟机的核心类库,如java.lang.*等,java.lang.Object是由根类加载器加载的,根类加载器的实现依赖于底层操作系统,属于虚拟机实现第一部分,它并没有继承java.lang.ClassLoader类.
    启动类加载器是特定于平台的机器指令,它负责开启整个加载过程
    启动类加载器还会负责加载JRE正常运行所需的基本组件.其中包括java.util,java.lang包中的类

    扩展类加载器(Extension)

    扩展类加载器的父加载器是根加载器,从java.ext.dirs系统属性指定的目录中加载类库,或者再jre\lib\ext子目录下加载类库,如果把用户创建的JAR文件放在这个目录下,会自动由扩展类加载器加载,扩展类加载器是纯Java类,是ClassLoader的子类

    注意一点的是,拓展类加载器加载的是jar包内的class文件

    系统(应用)类加载器(System/Application)

    系统类加载器的父加载器为扩展类加载器,从环境变量classpath或者系统属性java.class.path所制定的目录加载类,它是用户自定义的类加载器的默认父加载器,系统类加载器是纯Java类,是ClassLoader的子类

    用户自定义的类加载器

    除了虚拟机自带的加载器外,用户可以定制自己的类加载器.Java提供了抽象类ClassLoader.所有用户自定义的加载器都应该继承ClassLoader

    AppClassLoader和ExtClassLoader都是Java类,所以需要类加载器进行加载,而这两个类的类加载器就是bootstrapClassLoader

    可以通过修改
    System.getProperty(java.system.class.loader)对默认的SystemClassLoader进行修改

    类加载器的层级关系

    父亲委托机制

    在父亲委托机制中,各个加载器按照父子关系形成树形结构,除了根加载器之外,其余的类加载器有且只有一个父加载器.

    父亲委托机制

    简单描述,就是一个类加载器要加载一个类,并不是由自身进行直接加载,而是通过向上寻找父加载器,直到没有父加载器的类加载器,然后再从上至下尝试加载,直至找到一个可以正确加载的类加载器,一般情况下,系统类加载器就能加载普通的类.

    并不是所有的类加载器都必须遵守双亲委托的机制,具体实现可以根据需要进行改造

    代码示例,查看类的加载器

    public class Test08 {
    
        public static void main(String[] args) {
            try {
                Class<?> clzz = Class.forName("java.lang.String");
                //如果返回null,证明是由BootStrap加载器进行加载的
                System.out.println(clzz.getClassLoader());
    
    
                Class<?> customClass = Class.forName("com.r09er.jvm.classloader.Custom");
                System.out.println(customClass.getClassLoader());
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    
    class Custom{
    
    }
    

    输出

    null
    sun.misc.Launcher$AppClassLoader@18b4aac2
    

    String的类加载器为null,证明String是由Bootstrap类加载器加载,因为根加载器是由C++实现.所以会返回null.

    Custom的类加载器是Launcher$AppClassLoader,这个类是不开源的.但是是默认的系统(应用)类加载器.

    classLoader和初始化的时机

    通过ClassLoader手动加载类,观察是否会触发类的初始化

    public class Test12 {
    
        public static void main(String[] args) throws Exception {
            ClassLoader loader  = ClassLoader.getSystemClassLoader();
            Class<?> aClass = loader.loadClass("com.r09er.jvm.classloader.TestClassLoader");
    
            System.out.println(aClass);
    
            System.out.println("-------");
    
            aClass = Class.forName("com.r09er.jvm.classloader.TestClassLoader");
    
            System.out.println(aClass);
    
        }
    }
    class TestClassLoader{
        static {
            System.out.println("Test classloader");
        }
    }
    

    输出

    class com.r09er.jvm.classloader.TestClassLoader
    -------
    Test classloader
    class com.r09er.jvm.classloader.TestClassLoader
    

    结论

    明显可以看出,classLoader.load方法加载类,类并不会初始化,说明不是对类的主动使用,调用了Class.ForName才进行初始化

    不同的类加载器与加载动作分析

    打印类加载器,由于根加载器由C++编写,所以就会返回null

    public static void main(String[] args) {
            ClassLoader loader = ClassLoader.getSystemClassLoader();
            System.out.println(loader);
            //向上遍历父classLoader
            while (null != loader) {
                loader = loader.getParent();
                System.out.println(loader);
            }
        }
    

    输出结果

    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@610455d6
    null
    
    

    获取ClassLoader的途径

    • 通过类对象获取ClassLoader,clazz.getClassLoader()
    • 通过线程获取上限文ClassLoader,Thread.currentThread().getContextLoader()
    • 获得系统(应用)ClassLoader,ClassLoader.getSystemClassLoader()
    • 获得调用者的ClassLoader,DriverManager.getClassLoader()

    ClassLoader源码分析

    JavaDoc描述

    类加载器是负责加载的对象,classLoader是抽象类.赋予类一个二进制名称,一个类加载器应当尝试定位生成数据,这些数据构成类的定义.一种典型的策略是将二进制名称转换为文件名,然后从文件系统中读取该名称的字节码文件

    每一个对象都包含定义该的classLoader引用(reference)

    数组对应的class对象并不是由类加载器创建的,而是由java虚拟机在需要时自动创建的.对于一个数组的类加载器,与这个数组元素的类加载器一致.如果数组是原生类型,那这个数组将没有classLoader

    String[],则这个数组的类加载器是String的类加载器,使用的是Bootstrap类加载器
    int[] ,这种基本类型的数组,是没有类加载器的.

    应用实现classLoader的目的是为了拓展JVM动态加载类

    ClassLoader使用了委托模型去寻找类的资源.ClassLoader的每一个实例都有会一个关联的父ClassLoader,当需要寻找一个类的资源时,ClassLoader实例就会委托给父ClassLoader.虚拟机内建的ClassLoader称为BootstrapClassLoader,BootstrapClassLoader本身是没有父ClassLoader的,但是可以作为其他ClassLoader的父加载器

    支持并发加载的类加载器称为并行类加载器,这种类加载器要求在类初始化期间通过ClassLoader.registerAsParallelCapable将自身注册上.默认情况下就是并行的,而子类需要需要并行,则需要调用该方法

    在委托机制并不是严格层次化的环境下,classLoader需要并行处理,否则类在加载过程中会导致死锁,因为类加载过程中是持有锁的

    通常情况下,JVM会从本地的文件系统中加载类,这种加载与平台相关.例如在UNIX系统中,jvm会从环境变量中CLASSPATH定义的目录中加载类.

    然而有些类并不是文件,例如网络,或者由应用构建出来(动态代理),这种情况下,defineClass方法会将字节数组转换为Class实例,可以通过Class.newInstance创建类真正的对象
    由类加载器创建的对象的构造方法和方法,可能会引用其他的类,所以JVM会调用loadClass方法加载其他引用的类

    二进制名称BinaryNames,作为ClassLoader中方法的String参数提供的任何类名称,都必须是Java语言规范所定义的二进制名称。
    例如

    • "java.lang.String",全限定类名
    • "javax.swing.JSpinner$DefaultEditor",内部类
    • "java.security.KeyStoreBuilderFileBuilder$1",匿名内部类
    • "java.net.URLClassLoader31"

    自定义类加载器

    步骤

    • 1.继承CLassLoader
    • 2.重写loadClass方法
    • 3.在loadClass方法中实现加载class字节码的方法,返回byte[]
    • 4.调用super.defineClass(byte[])方法将Class对象返回给loadClass方法

    源码示例

    public class Test16 extends ClassLoader {
    
        private String classLoaderName;
    
        private String path;
    
        private final String fileExtension = ".class";
    
    
        public Test16(String classLoaderName) {
            //将systemClassLoader作为当前加载器的父加载器
            super();
            this.classLoaderName = classLoaderName;
        }
    
        public Test16(ClassLoader parent, String classLoaderName) {
            //将自定义的ClassLoader作为当前加载器的父加载器
            super(parent);
            this.classLoaderName = classLoaderName;
        }
    
    
        public void setPath(String path) {
            this.path = path;
        }
    
        public static void main(String[] args) throws Exception {
            Test16 loader1 = new Test16("loader1");
            //设置绝对路径,加载工程根目录下的com.r09er.jvm.classloader.Test01.class
            loader1.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
            Class<?> aClass = loader1.loadClass("com.r09er.jvm.classloader.Test01");
            //打印加载的类
            System.out.println("loader1 load class" + aClass.hashCode());
            Object instance = aClass.newInstance();
            System.out.println("instance1: " + instance);
    
    
            Test16 loader2 = new Test16("loader2");
            //设置绝对路径,加载工程根目录下的Test01.class
            loader2.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
            Class<?> aClass2 = loader2.loadClass("com.r09er.jvm.classloader.Test01");
            System.out.println("loader2 load class" + aClass2.hashCode());
            Object instance2 = aClass2.newInstance();
            System.out.println("instance2 : " + instance2);
    
            //todo ****
            //1.重新编译工程,确保默认的classPath目录下有Test01.class的字节码文件,然后运行main方法,观察输出
            //2.删除默认classpath目录下的Test01.class,运行main方法,观察输出
    
        }
    
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            System.out.println("invoke findClass");
            System.out.println("class loader name : " + this.classLoaderName);
            byte[] bytes = this.loadClassData(name);
            return super.defineClass(name, bytes, 0, bytes.length);
        }
    
        private byte[] loadClassData(String binaryName) {
            byte[] data = null;
    
            binaryName = binaryName.replace(".", "/");
    
            try (
                    InputStream ins = new FileInputStream(new File(this.path + binaryName + this.fileExtension));
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    
            ) {
                int ch;
                while (-1 != (ch = ins.read())) {
                    baos.write(ch);
                }
                data = baos.toByteArray();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return data;
        }
    
    }
    
    

    执行两次main方法后,会发现类加载器真正生效的逻辑,因为默认的父加载器其实是系统加载器(AppClassLoader),所以如果默认的classPath存在字节码文件,则会由AppClassLoader正确加载类,如果classPath中没有,则会向下使用自定义的类加载器加载类

    如果构造函数传入两个不一样的ClassLoaderName,会发现两个class对象并不一致,是由于命名空间NameSpace的原因,因为两个类加载器定义的名称是不一样的,如果改成相同的名称,则两个class对象一致

    重写的是findClass方法,在调用时候,使用的是classLoader的loadClass方法,这个方法内部会调用findClass

    还有一个重点,如果将class字节码文件放在根目录,则会抛出NoClassDefFoundError异常,因为binaryName不符合规范.

    自定义类加载器加载类流程图

    类加载器重要方法详解

    findClass

    实现自己的类加载器,最重要就是实现findClass,通过传入binaryName,将二进制名称加载成一个Class对象

    defineClass

    在实现findClass后,需要通过defineClass方法,将二进制数据交给defineClass方法转换成一个Class实例,
    defineClass内部会做一些保护和检验工作.

    双亲委派机制解析

    通过loadClass方法加载类,会有如下默认加载顺序

    • 1.调用findLoadedClass方法检查class是否被加载
    • 2.调用父加载器的loadClass方法,如果父加载器为null,则会调用JVM内建的类加载器.
    • 3.调用findClass方法找到类

    在默认的loadClass方法中,类加载是同步

    双亲委派机制优点

    • 1.可以确保Java核心类库的类型安全,如果这个加载过程由Java应用自己的类加载器完成,很可能会在JVM中存在多个版本的同一个类(包名,类名一致),

    命名空间发挥的作用

    • 2.可以确保Java核心类库提供的类不会被自定义的类替代

    因为优先加载的是类库中的class,会忽略掉自定义的类

    • 3.不同的类加载器可以为相同名称(binaryName)的类创建额外的命名空间,相同名称的类可以并存在JVM中.

    类的卸载

    当类被加载,连接,初始化之后,它的生命周期就开始了.当代表类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在元空间内的数据也会被卸载,从而结束类的生命周期.

    一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期

    由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载.

    用户自定义的类加载器所加载的类是可以被卸载的

    类加载器加载的类路径

    BootstrapClassLoader加载的路径

    • System.getProperty("sun.boot.class.path")

    ExtClassLoader

    • System.getProperty("java.ext.dirs")

    AppClassLoader

    • System.getProperty("java.class.path")

    三个路径和JDK版本,操作系统都有关系

    如果将编译好的class字节码文件放到根加载器的加载路径上,可以成功由BootstrapClassLoader加载

    类加载器命名空间

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

    即子加载器能访问父加载器加载的类,而父加载器不能访问子加载器加载的类.(类似于继承的概念)

    • 在同一个命名空间中,不会出现类的完整名字相同的两个类

    一个Java类是由该类的全限定名称+用于加载该类的定义类加载器(defining loader)共同决定.

    ClassLoader.getSystemClassLoader源码

    返回用于委托的系统类加载器.是自定义类加载器的父加载器,通常情况下类会被系统类加载器加载.
    该方法在程序运很早的时间就会被创建,并且会将系统类加载器设为调用线程的上下文类加载器(context class loader)

    Launcher构造主要逻辑

    1.初始化ExtClassLoader
    2.初始化AppClassLoader,将初始化好的ExtClassLoader设置为AppClassLoader的父加载器
    3.将AppClassLoader设置为当前线程的上下文类加载器

    SystemClassLoaderAction逻辑

    1.判断System.getProperty("java.system.class.loader")是否有设置系统类加载器
    2.如果为空,直接返回AppClassLoader
    3.如果不为空,通过反射创建classLoader,其中必须提供一个函数签名为ClassLoader的构造
    4.将反射创建的自定义类加载器设置为上限为加载器.
    5.返回创建好的类加载器

    Class.ForName(name,initialize,classloader)解析

    • name,需要构造的类全限定名称(binaryName)

    不能用于原生类型或者void类型
    如果表示的是数组,则会加载数组中的元素class对象,但是不进行初始化

    • initialize,类是否需要初始化
    • classloader,加载此类的类加载器

    线程上下文加载器(ContextClassLoader)实现与分析

    CurrentClassLoader(当前类加载器)

    • 每一个类都会尝试使用自己的ClassLoader去加载当前类引用的其他类

    如果ClassA引用了ClassY,那么ClassA的类加载器会去加载ClassY,前提是ClassY未被加载

    线程类加载器从JDK1.2开始引入,Thread类中的getContextClassLoadersetContextClassLoader分别用来获取和设置上下文加载器.如果没有手动进行设置,那么线程会继承其父线程的上下文加载器.
    java应用运行时的初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的类可以通过这个类加载器加载类与资源

    由JDBC引出的问题

    回顾一下JDBC操作

    Class.forName("com.mysql.driver.Driver");
    Connection conn = Driver.connect();
    Statement stae = conn.getStatement();
    

    Driver,Connection,Statement都是由JDK提供的标准,而实现是由具体的DB厂商提供.
    根据类加载的机制,JDK的rt包会被BootstrapClassLoader加载,而自定义的类会被AppClassLoader加载,同时因为命名空间的原因,父加载器是无法访问子加载器加载的类的.所以父加载器会导致这个问题.

    上下文加载器就是为了解决这种问题所存在的

    父ClassLaoder可以使用当前线程Thread.currentThread().getContextClassLoader()加载的类,
    这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader无法访问对方加载的class问题.

    即改变了父亲委托模型

    线程上下文加载器一般使用

    使用步骤(获取 - 使用 - 还原)

    1. Thread.currentThread().getContextClassLoader()
    2. Thread.currentThread().setContextClassLoader(targetClassLoader)
      doSomentthing();
      3.Thread.currentThread().setContextClassLoader(originClassLoader);

    ContextClassLoader的作用就是破坏Java的类加载委托机制

    ServiceLoader

    ServiceLoader是一个简单的服务提供者加载设施

    加载基于JDK规范接口实现的具体实现类
    实现类需要提供无参构造,用于反射构造出示例对象

    服务提供者将配置文件放到资源目录的META-INF/services目录下,告诉JDK在此目录的文件内配置了需要加载的类,其中文件名称是需要加载的接口全限定名称,文件内容是一个或多个实现的类全限定名称.

    总结

    在双亲委托模型下,类加载时由下至上的.但是对于SPI机制来说,有些接口是由Java核心库提供的,根据类加载的机制,JDK的rt包会被BootstrapClassLoader加载,而自定义的类会被AppClassLoader加载.这样传统的双亲委托模型就不能满足SPI的情况,就可以通过线程上下文加载器来实现对于接口实现类的加载.

    相关文章

      网友评论

        本文标题:JVM(三)类加载器

        本文链接:https://www.haomeiwen.com/subject/dcojyhtx.html