Java8之类的加载

参考文章:

 《JVM 基础 - Java 类加载机制》

《Java的类加载器与双亲委托机制》

《Java ClassLoader 理解》

后文:

《Java8之类加载机制class源码分析》

写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。

目录

一、类的生命周期

        1、概述

        2、 加载

        3、连接

        3.1、验证: 确保被加载的类的正确性

        3.2、准备: 为类的静态变量分配内存,并将其初始化为默认值

         3.3、解析: 把类中的符号引用转换为直接引用

        4、初始化

        4.1、类初始化时机

        4.2、类变量进行初始值方式

       4.3、初始化步骤

        5、使用

        6、卸载

二、类加载器

        1、类加载器的简介

        1.1、概述

        1.2、延迟加载

        1.3、类加载机制

        2、系统类加载器

        2.1、Bootstrap ClassLoader:启动类加载器

        2.2、Extensions ClassLoader:扩展类加载器

        2.3、App ClassLoader:系统类加载器

        2.3、加载器的关系

补充

        1、为什么使用双亲委托机制

        2、由不同的类加载器加载的类会被JVM当成同一个类吗

        3、双亲委派机制的问题与解决


一、类的生命周期

        1、概述

        类的生命周期包括了加载、链接(验证、准备、解析)、初始化、使用、卸载这五个阶段。在这几个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

        2、 加载

        类的加载主要负责查找并加载类的二进制数据。

        

        在加载阶段,虚拟机需要完成3件事:
        (1)通过一个类的全限定名(org/fenixsoft/clazz/TestClass)获取定义此类的二进制字节流(.class文件);
        (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
        (3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口;


        相对于其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

       类加载有三种方式:

        (1)、命令行启动应用时候由JVM初始化加载

        (2)、通过Class.forName()方法动态加载

        (3)、通过ClassLoader.loadClass()方法动态加载

        需要注意的点:

        Class.forName(): 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;

        ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。

        Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。

        加载.class文件的方式包括:

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

        3、连接

        3.1、验证: 确保被加载的类的正确性

        验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:        

        (1)文件格式验证: 验证字节流是否符合Class文件格式的规范;例如: 是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。

        (2)元数据验证: 对字节码描述的信息进行语义分析(注意: 对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如: 这个类是否有父类,除了java.lang.Object之外。

        (3)字节码验证: 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

        (4)符号引用验证: 确保解析动作能正确执行。

        验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

        3.2、准备: 为类的静态变量分配内存,并将其初始化为默认值

        准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

        (1)这里进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

        (2)初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

        假设一个类变量的定义为: public static int value = 3;那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,将value赋值为3的动作将在初始化阶段才会执行。

        (3)如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。假设上面的类变量value被定义为: public static final int value = 3;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。

         3.3、解析: 把类中的符号引用转换为直接引用

           解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、方法类型、方法句柄和调用点限定符7类符号引用进行;

        4、初始化

          初始化阶段才真正开始执行类中定义的Java程序代码(或者说是字节码)。

        4.1、类初始化时机

         只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

        (1)创建类的实例,也就是new的方式

        (2)访问某个类或接口的静态变量,或者对该静态变量赋值

        (3)调用类的静态方法

        (4)反射(如Class.forName("com.pdai.jvm.Test"))

        (5)初始化某个类的子类,则其父类也会被初始化

        (6)Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类

        初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。

        4.2、类变量进行初始值方式

        (1)声明类变量是指定初始值

        (2)使用静态代码块为类变量指定初始值

       4.3、初始化步骤

        (1)假如这个类还没有被加载和连接,则程序先加载并连接该类

        (2)假如该类的直接父类还没有被初始化,则先初始化其直接父类

        (3)假如类中有初始化语句,则系统依次执行这些初始化语句

        

        5、使用

        类访问方法区内的数据结构的接口, 对象是Heap区的数据。

        6、卸载

        Java虚拟机将结束生命周期的几种情况

        (1)执行了System.exit()方法

        (2)程序正常执行结束

        (3)程序在执行过程中遇到了异常或错误而异常终止

        (4)由于操作系统出现错误而导致Java虚拟机进程终止

二、类加载器

        1、类加载器的简介

        1.1、概述

        类加载器ClassLoader ,用来加载Class文件。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

        每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。 

        1.2、延迟加载

        JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

        比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。

        1.3、类加载机制

        (1)全盘负责

        当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入(也被叫做ClassLoader的传递性)

        (2)父类委托

        先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

        (3)缓存机制

        缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效.

     (4)双亲委派机制

        如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。(这里需要注意的是,父加载器不是指当前加载器的父类,这一点我们后续会做详细介绍。)

        2、系统类加载器

        2.1、Bootstrap ClassLoader:启动类加载器

        Bootstrap ClassLoader用来加载JVM运行时所需要的系统类,其使用c++实现。负责将JAVA_HOME/jre/lib目录中的jar包加载到内存中,包括rt.jar、resources.jar和charsets.jar等等。也可以在JVM启动时,指定-Xbootclasspath参数,来改变Bootstrap ClassLoader的加载目录。

        可以通过System.getProperty("sun.boot.class.path")获取其加载的包。

    public static void main(String[] args)  {
        String[] strs = System.getProperty("sun.boot.class.path").split(";");
        for(String str:strs){
            System.out.println(str);
        }
    }

        2.2、Extensions ClassLoader:扩展类加载器

        Extensions ClassLoader(扩展类加载器)具体是由ExtClassLoader类实现的,ExtClassLoader类位于sun.misc.Launcher类中,是其的一个静态内部类。对于Launcher类,可以先看成是Java虚拟机的一个入口。

        负责将JAVA_HOME/jre/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。

        可以通过System.getProperty("java.ext.dirs")获取其加载的包。        

    public static void main(String[] args)  {
        String[] strs = System.getProperty("java.ext.dirs").split(";");
        for(String str:strs){
            System.out.println(str);
        }
    }

        2.3、App ClassLoader:系统类加载器

        具体是由AppClassLoader类实现的,AppClassLoader类也位于sun.misc.Launcher类中。

        主要加载Classpath目录下的的所有jar和Class文件,是程序中的默认类加载器。这里的Classpath是指我们Java工程的bin目录。也可以加载通过-Djava.class.path选项所指定的目录下的jar和Class文件。

        可以通过System.getProperty("java.class.path")获取其加载的包。

        2.3、加载器的关系

        (1)、继承关系

         以上三个类的继承关系图如下:

        

  • ClassLoader是一个抽象类,位于java.lang包下,其中定义了ClassLoader的主要功能。
  • SecureClassLoader继承了抽象类ClassLoader,但SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。
  • ExtClassLoader和AppClassLoader都继承自URLClassLoader,它们都是Launcher 的内部类,Launcher 是Java虚拟机的入口应用,ExtClassLoader和AppClassLoader都是在Launcher中进行初始化的。

       (2)、父加载器关系

        每个 ClassLoader 对象内部都会有一个 parent 属性指向它的父加载器。

class ClassLoader {
  ...
  private final ClassLoader parent;
  ...
}

         这三个ClassLoader之间形成了级联的父子关系如下图

         AppClassLoader的父加载器为ExtClassLoader而ExtClassLoader的父加载器是Bottstrap ClassLoader。需要注意的是图中的 ExtensionClassLoader 的 parent 指针画了虚线,这是因为它的 parent 的值是 null(Bottstrap ClassLoader不是由Java实现的),当 parent 字段是 null 时就表示它的父加载器是「根加载器」。如果某个 Class 对象的 classLoader 属性值是 null,那么就表示这个类也是「根加载器」加载的。

        

        (3)、详解双亲委托机制

  • 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  • 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  • 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  • 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

 这块的详细源码解析可以看这里:《传送门》

补充

        1、为什么使用双亲委托机制

        (1)、避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是先从缓存中直接读取。

        (2)、安全方面的考虑,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这样便会造成安全隐患,采用双亲委托模式会使得系统的String类在Java虚拟机启动时就被加载,也就无法自定义String类来替代系统的String类。

        2、由不同的类加载器加载的类会被JVM当成同一个类吗

        答案是不会,在Java中,我们用包名+类名作为一个类的标识。 但在JVM中,一个类用其包名+类名和一个ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类。

         这样做的好处在于可以解决钻石依赖问题(钻石依赖,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。)。不同版本的软件包使用不同的 ClassLoader 来加载,位于不同 ClassLoader 中名称一样的类实际上是不同的类。

        

public class Main {
    public static void main(String[] args) {
    
        ClassLoaderTest myClassLoader = new ClassLoaderTest("F:\\");
        ClassLoaderTest myClassLoader2 = new ClassLoaderTest("F:\\");
        try {
            Class c = myClassLoader.loadClass("com.example.Hello");
            Class c2 = myClassLoader.loadClass("com.example.Hello");

            Class c3 = myClassLoader2.loadClass("com.example.Hello");

            System.out.println(c.equals(c2)); //true
            System.out.println(c.equals(c3)); //flase
    }
}

        3、双亲委派机制的问题与解决

        通过上面的介绍,我们了解了双亲委派机制的原理与好处,但是这套机制也有一些局限性,那就是父级加载器无法加载子级类加载器路径中的类,基础类无法调用户类。

        举个例子,在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。

   // 1.加载数据访问驱动
        Class.forName("com.mysql.jdbc.Driver");
        //2.连接到数据"库"上去
        Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "");

        在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接获取连接了。

        SPI服务的模式的过程是怎样的:

(1)从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
(2)加载这个类,这里肯定只能用class.forName(“com.mysql.jdbc.Driver”)来加载

        现在问题来了,Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。

        这个时候,ContextClassLoader(上下文类加载器)就来解围了。

public void setContextClassLoader(ClassLoader cl)
public ClassLoader getContextClassLoader()

        我们可以通过在SPI类里面调用getContextClassLoader来获取第三方实现类的类加载器。由第三方实现类通过调用setContextClassLoader来传入自己实现的类加载器, 这样就变相地解决了双亲委派模式遇到的问题

        ​​​​​​​现在我们看下DriverManager是如何使用线程上下文类加载器去加载第三方jar包中的Driver类的。

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    private static void loadInitialDrivers() {
        //省略代码
        //这里就是查找各个sql厂商在自己的jar包中通过spi注册的驱动
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        try{
             while(driversIterator.hasNext()) {
                driversIterator.next();
             }
        } catch(Throwable t) {
                // Do nothing
        }

        //省略代码
    }
}

        ​​​​​​​使用时,我们直接调用DriverManager.getConn()方法自然会触发静态代码块的执行,开始加载驱动。下面我们看ServiceLoader.load()的具体实现:

    public static <S> ServiceLoader<S> load(Class<S> service) {
        //拿到线程上下文类加载器,然后构造了一个ServiceLoader,后续的具体查找过程
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader){
        return new ServiceLoader<>(service, loader);
    }

        ​​​​​​​接下来,DriverManager的loadInitialDrivers()方法中有一句driversIterator.next();,它的具体实现如下:

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //此处的cn就是产商在META-INF/services/java.sql.Driver文件中注册的Driver具体实现类的名称
               //此处的loader就是之前构造ServiceLoader时传进去的线程上下文类加载器
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
         //省略部分代码
        }

        ​​​​​​​现在,我们成功的做到了通过线程上下文类加载器拿到了应用程序类加载器(或者自定义的然后塞到线程上下文中的),同时我们也查找到了厂商在子级的jar包中注册的驱动具体实现类名,这样我们就可以成功的在rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了。
        

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值