• 深入Java类加载全流程,值得你收藏


    先测试一番,全对的就走人

    //题目一
    class Parent1{
        public static String parent1 = "hello parent1";
        static { System.out.println("Parent1 静态代码块"); }
    }
    class Children1 extends Parent1{
        public static String children1 = "hello children1";
        static {System.out.println("Children1 静态代码块");}
    }
    //----------------------------------------------------------------
    //题目二
    class GrandParent2{
        static { System.out.println("GrandParent2静态代码块"); }
    }
    class Parent2 extends GrandParent2{
        public static String parent2="hello parent2";
        static{ System.out.println("Parent2 静态代码块");}
    }
    class Children2 extends Parent2{
        public static String children2 ="hello children2";
        static{ System.out.println("Children2 静态代码块");}
    }
    //----------------------------------------------------------------
    //题目三
    class GrandParent3{
        static { System.out.println("GrandParent3静态代码块"); }
    }
    class Parent3 extends GrandParent3{
        public final static String parent3="hello parent3";
        static{ System.out.println("Parent3 静态代码块");}
    }
    class Children3 extends Parent3{
        public static String children3 ="hello children3";
        static{ System.out.println("Children3 静态代码块");}
    }
    //测试
    public class ClassLoaderTest {
        public static void main(String[] args) {
            //测试一的输出
            System.out.println(Children1.children1);
            System.out.println("-------------------------------");
            //测试二的输出
            System.out.println(Children2.parent2);
            System.out.println("--------------------------------");
            //测试三的输出
            System.out.println(Children3.parent3);
        }
        //你认为输出什么呢
    }
    
    答案如下

    Parent1 静态代码块
    Children1 静态代码块
    hello children1


    GrandParent2静态代码块
    Parent2 静态代码块
    hello parent2


    hello parent3

    如果看清到这里,你的回答和结果一致,那么你真的懂了,可以转载给他人了,如果出乎你的意料,请认真看完。

    什么是类加载(或者初始化)

    Java源代码经过编译之后转换成class文件,在系统运行期间当需要某个类的时候,如果内存中还没该class文件,那么JVM需要对这个类的class文件进行加载,连接,初始化,JVM通常会连续完成这三步,这个过程叫做类的加载或者初始化, 类从磁盘加载到内存必须经历这三个阶段的。

    重点是:类的加载都是在程序运行期间完成的,这提供了无限可能,意味着你可以在某个阶段对类的字节码进行修改,JVM也确实提供了这样的功能。

    类的加载并不是对象的创建,类的加载是在为对象创建前做一些信息准备。

    类的生命周期

    我们明白了什么是类的加载,那么从类的加载到最后类的卸载成为类在JVM中的声明周期,这个生命周期总共包含了七个阶段:我画一张图,如下,我们逐个分析一下类的生命周期的每一步。

    这是类的生命周期的,但它不总是按照这个固定的流程进行的,我们先知道这个就行,后面再说。

    加载

    类的加载指的是把class文件从磁盘读入内存中,将其放入元数据区域并且创建一个Class对象,放入堆中,Class对象是类加载的最终产品,Class对象并不是new出来的对象。

    元数据区域存储的信息

    1. 这个类型的完整有效名
    2. 这个类型的直接父类完整有效名
    3. 这个类型的修饰符(public final abstract等)
    4. 这个类型的直接接口的列表

    Class对象中包含的如下信息,这也是我们能够通过Class对象获取类的很多信息的原因

    1. 类的方法代码,方法名,字段等
    2. 类的返回值
    3. 类的访问权限

    加载class文件有很多种方式,可以从磁盘上读取,可以从网络上读取,可以从zip等归档文件中读取,可以从数据库中读取

    验证

    验证的目的是验证class文件的正确性,是否能够被当前JVM虚拟机执行,主要包含了一些部分验证,验证非常重要,但不是必须的(正常情况下都是正确的)
    文件格式验证:比如JDK8加载的是JDK6下编译的class文件,这肯定不行。
    元数据验证:确保字节码描述信息符合Java语言规范的要求,你理解为校验外壳,比如类中是否实现了接口的所有方法。
    字节码验证:确定程序语义执行是合法的,校验内在,校验方法体,防止字节码执行过程中危害JVM虚拟机。
    符合引用验证:其对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,比如:符号引用中的类、字段、方法的访问性是否可被当前类访问,通过全限定名,是否能找到对应的类。

    准备(重点)

    验证完成之后,JVM就开始为类变量(静态变量) 分配内存,设置初始化值, 记住两点

    1. 不会为成员变量分配内存的。
    2. 初始化值是指JVM默认的指,不是程序中指定的值。

    看如下代码,你就明白了:

    //类变量,初始化值是 null, 不是123
    public static String s1 = "123"
    //成员变量
    public String s2 = "456"
    

    但有一个特殊,如果一个类变量是final修饰的常量,那么在准备阶段就会被赋值为程序中指定的值,如下代码,初始值是123

    //初始值是123,不是null
    public static final String s1 = "123"
    

    为什么会这样呢?两行代码的区别在于final,final在Java中代表着不可变,不能赋值了之后重新赋值,所以一开始就必须赋值为用户想要的默认值,而不是Java语言的默认值。而不是final修时的变量有可能在之后发生变化,所以就先赋值为Java语言的默认值。

    解析

    解析阶段主要是将常量池中的符号引用转换为直接引用,解析动作主要包含类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用。

    符号引用包括什么呢?

    1. 类和方法的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符,

    直接引用是是什么呢?一个指向目标的指针地址或者句柄。
    举个例子如下:

    // 123 是一个符号引用,123所对应的内存中的地址是一个直接引用。
    public static final String s1 = "123"
    

    常量池是什么呢?,常量池包含好多种,字符串常量池,class常量池,运行时常量池,这里指的是class常量池。我们写的每一个Java类被编译后,就会形成一份class文件,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用,每个class文件都有一个class常量池。

    比如解析阶段,找不到某个字段就抛出NoSuchFieldError,同理NoSuchMethodError

    初始化(重点)

    初始化阶段用户定义的Java代码才会真正开始执行,一般来说当首次主动使用某个类的时候就会对该类初始化,初始化某个类时也会初始化这个类的父类,这里的首次主动使用,大家要理解清楚了,第二次使用时不会初始化的。类的初始化其实就是执行类构造器的过程,这个不是我们代码定义的构造方法。

    下面列举了JVM初始化类的时机:

    1. 创建对象时(比如:new Person())
    2. 访问类变量时
    3. 调用类的静态方法时
    4. 反射加载某个类是(Class.forName("....."))
    5. Java虚拟机启动时被标明为启动类的类(单测时),Main方法的类。

    初始化时类变量会被赋予真正的值,也就是开发人员在代码中定义的值,也会执行静态代码块。

    JVM初始化类的步骤:

    1. 若该类还没有被加载和连接,则程序先加载并连接该类
    2. 若该类的父类还没有初始化,则先初始化该类的夫类
    3. 若该类中有静态代码块,则系统依次执行这些代码块

    上面提到了首次主动使用时初始化类,那么就有被动使用,被动使用是什么意思呢?比如说通过子类引用父类的静态字段,那么子类会初始化吗?答案是不会的,所以下面测试的子类的静态代码块是不会执行的。

    class Parent4{
        public final static String parent4="hello parent4";
    }
    
    class Children4 extends Parent4{
        static{ System.out.println("Children4 静态代码块");}
    }
    public class ClassLoaderTest {
        public static void main(String[] args) {
            //测试四的输出
            System.out.println(Children4.parent4);
        }
    }
    

    再说一个点解析时有提到常量池的概念,在经过初始化后,类就被加载到内存中去了,这个时候jvm就会将class常量池中的内容存放到运行时常量池中,运行时常量池也是每个类都有一个。在解析阶段,会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与字符串常量池中是一致的

    上面还有一个关键字一般来说,那么不一般呢?类加载器并不需要等到某个类被首次主动使用时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它.

    使用

    使用就比较简单了,JVM初始化完成后,就开始按照顺寻执行用户代码了。

    卸载

    类卸载有个前提,就是class的引用是空的,要么程序中手动置为空,要么进程退出时JVM销毁class对象,然后JVM退出。只要class引用不存在,那么这个类就可以回收了。

    你自己可以试验一下,写一个classload类加载器,写一个Test测试类,实际测试一下,我的测试代码如下:

    public class ClassTest {
        public static void main(String[] args){
            ClassLoaderMy classLoader = new ClassLoaderMy();
            classLoader.setRoot("D:\github\java_common\target\classes\");
            Class clazz = classLoader.findClass("jvm.Test类中有一个静态代码块。");
            Object obj = clazz.newInstance();
            System.out.println("1:"+clazz.hashCode());
            obj=null;
            System.out.println("2:"+clazz.hashCode());
            classLoader = null;
            System.out.println("3:"+clazz.hashCode());
            clazz = null;
    
            System.out.println("此时 obj classloader clazz 都为空了");
    
            classLoader = new ClassLoaderMy();
            classLoader.setRoot("D:\github\java_common\target\classes\");
            clazz = classLoader.findClass("jvm.Test");
            System.out.println("4:"+clazz.hashCode());
            obj = clazz.newInstance();
        }
        //打印结果如下,看之前你猜一猜。Test类中有一个静态代码块。
    }
    

    初始化了
    1:1775282465
    2:1775282465
    3:1775282465
    此时 obj classloader clazz 都为空了
    4:1267032364
    初始化了

    最终结果你会发现,前三个hashcode的值是一样的,第四个的值发生了变化,说明class文件被卸载了后重新加载生成了新的class对象,否则,同一个对象的hashcode是不会发生变化的,而且Test类的静态代码块执行了两遍,完整代码地址如下:

    https://github.com/sunpengwei1992/java_common/tree/master/src/jvm
    

    我画了一张图,方便大家更好的理解,如下,当左边的三个变量都指向为null时,最右边的元数据区域的代表Class对象的Test二进制数据就会被卸载,当下次使用时就会被重新加载,初始化等。

    但是,注意了 由JVM自带的类加载器加载的类,在JVM生命周期中,始终不会被卸载,
    JVM自带的类加载器包括根类加载器,扩展类加载器,系统类加载器,这些回头单聊。

    解密测试题目

    接下来我们聊一聊一开始的测试题,其实看到这里,想必大家都明白了吧,还是说一说。

    第一个不用讲了,都会。

    第二题:子类Children2,父类Parent2, 祖父类GrandParent2,我们通过Chidlren2打印父类Parent2的静态变量,类加载时,发现有父类存在,逐层往上加载,那么Parent2和GrandParent2都会被加载,所以Parent2和GrandParent2的静态代码块都会被执行,而Children2就不会被加载了,因为不符合首次主动使用的条件。

    第三题:同样的道理,只是Parent3和GrandParent3的静态代码块为什么没执行呢,因为Parent3的静态变量是final类型的,在准备阶段就已经完成了,不需要再逐层往上加载了.

    提一下接口的加载

    当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,当真正用到父接口的时候才会加载该接口,如下代码,执行main方法,Parent5接口是不会被加载的,parent5变量也是不会被初始化的。

    interface Parent5{
        public final static  String parent5 = "hello parent5";
    }
    interface Children5 extends Parent5{
        public final static String children5 = "hello children5";
    }
    public static void main(String[] args) {
        System.out.println(Children5.children5);
    }
    

    表格整理一下流程

  • 相关阅读:
    vi命令大全
    理解proc文件系统
    读目录
    取得系统资源信息
    qtempinc
    我实现的一个正则表达式代码
    oracle内置函数大全
    STL算法
    unix基础教程
    两日期间的天数
  • 原文地址:https://www.cnblogs.com/sy270321/p/12258421.html
Copyright © 2020-2023  润新知