美文网首页
类加载机制(1)类加载过程

类加载机制(1)类加载过程

作者: 飞奔的口罩 | 来源:发表于2021-04-16 09:13 被阅读0次

    目录

    <span id="jump1">一、什么是类的加载(类初始化)</span>

    jvm通过加载、连接(验证、准备、解析)、初始化3大步将类加载进内存的过程叫做类的加载或类初始化。

    关键点:

    <span id="jump1_1">1、分清类初始化和对象初始化的不同。

    - 类初始化是指jvm对类使用前初始化过程,就是初始化class本体。而对象初始化是指通过类的本体new 一个具体的对象的过程。
     
    - 所以我们下边总结的初始化开始的时机是对类初始化的时机,而不是对象初始化的时机。
    

    <span id="jump1_2">2、类的初始化过程是在程序运行期间完成。

    - 整个加载、连接、初始化,将类从磁盘加载到内存中的过程,都是在程序运行期间完成的。
    - 正是因为这个特性,才给动态加载类提供了可能。比如动态代理模式。
    

    <span id="jump2">二、类的生命周期</span>

    image.png

    <span id="jump2_1">1、加载</span>

    • 1、加载阶段指的是将类的<font color=red>.class</font>文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在<font color=red>堆区</font>创建一个 java.lang.Class对象(JVM规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在方法区中),用来封装类在方法区内的数据结构。
    • 2、类的加载的最终产品是位于<font color=red>堆区中的 Class对象</font>, Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

    Class对象位置【HotSpot虚拟机】:

    在JDK1.7是在方法区中或者说永久代

    在JDK1.8放在方法区或者说元空间
    JDK1.8移除了永久代,转而使用元空间来实现方法区

    加载阶段简单来说就是:

    <font color=red>.class文件(二进制数据)——>读取到内存——>数据放进方法区——>堆中创建对应Class对象——>并提供访问方法区的接口</font>

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

    加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个 java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。

    加载.calss文件的方式:

    类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。通过使用不同的类加载器,可以从不同来源加载类的二进制数据,二进制数据通常有如下几种来源:

    (1)从本地系统中直接加载

    (2)通过网络下载.class文件

    (3)从zip,jar等归档文件中加载.class文件

    (4)从专用数据库中提取.class文件

    (5)将java源文件动态编译为.class文件

    <span id="jump2_2">2、验证</span>

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

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

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

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

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

    <font id="jump2_3" color=red>3、准备【重要】</font>

    (1)为<font color=red>类变量</font>分配内存

    类变量指:被static修饰的字段。
    

    (2)为类变量设置初始值,此时的初始值是指内存默认值,int=0,string=null。

    - 此时的初始值是指内存默认分配的值,0、0L、null等等。真实用户的默认值是在初始化阶段才被赋值。
    - final修饰的值在这个阶段是不赋值内存默认值的,直接是赋值用户的默认值。
    

    (3)为<font color=red>final static</font>直接赋值

    <span id="jump2_4" >4、解析</span>

    在JVM中类加载过程中,在解析阶段,Java虚拟机会把类的二级制数据中的符号引用替换为直接引用

    1.符号引用(Symbolic References):

    符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在Java中,一个java类将会编译成一个class文件。在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如org.simple.People类引用了org.simple.Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个,当然实际中是由类似于CONSTANT_Class_info的常量来表示的)来表示Language类的地址。各种虚拟机实现的内存布局可能有所不同,但是它们能接受的符号引用都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

    2.直接引用:

    直接引用可以是

    (1)直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)

    (2)相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)

    (3)一个能间接定位到目标的句柄
    直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了。

    <font id="jump2_5" color=red>5、初始化【重要】</font>

    <font id="jump2_5_1">(1)什么是初始化

    这个初始化阶段指的是<font color=red>类的初始化</font>,而不是<font color=red>对象的初始化</font>。所以并不会执行非static修饰的部分。

    • <font color=red>类初始化:</font>是指jvm将类的本体(即模板)从磁盘加载进内存的过程中,将类变量(即static修饰的)初始化的过程。<font color=red>且类初始化在jvm的生命周期内只进行一次</font>,即类初始化完成后,不需要重复初始化,这也是为什么我们的代码变更就需要重启才能生效的原因。

      • 1、初始化static修饰的变量
      • 2、执行静态代码块static{}
    • <font color=red>对象初始化:</font>是值通过类的本体(即模板)实例化成具体对象的过程,即new Object()过程。这个过程会初始化以下内容:

      • 1、非static修饰的成员变量
      • 2、构造代码块{}
      • 3、构造方法
    • 另:<font color=red>对象初始化</font>一定触发<font color=red>类初始化</font>,但是类初始化不一定触发对象初始化。

    <font id="jump2_5_2">(2)初始化的内容

    初始化阶段,会初始化<font color=red>static修饰的变量</font>的值和执行<font color=red>static{}代码块</font>。即将static的类变量赋值成用户所期待的值。

    1、这个阶段会先给static类变量赋值 = 用户的初始化值。
    2、然后执行static{}静态代码块。
    

    举例说明:

    //这种情况就会在初始化阶段,将str1从null赋值为11111
    static String str1 = "11111";
    //这种情况什么都不做,因为用户没有设置初始化值。
    static String str2;
    
    public class Demo01 {
        public static void main(String[] args) {
            System.out.println("main:str1:  "+A.str1 + " str2:  "+A.str2);
        }
    }
    
    class A{
        static String str1 = "11111";
        static String str2;
        static {
            System.out.println("===str1: "+str1+"  str2: "+str2);
            str1 = "22222";
            System.out.println("===str1: "+str1+"  str2: "+str2);
        }
        
        //这些方法和对象成员变量,并不会在类初始化阶段被执行。只有在对象初始化时才被调用
        //1、普通成员变量
        String str3 = "33333";
        //2、构造代码块
        {
            str3 = "44444";
        }
        //3、构造方法
        public A(){
            System.out.println("构造方法"+str3);
        }
    }
    
    执行结果
    ===str1: 11111  str2: null
    ===str1: 22222  str2: null
    main:str1:  22222 str2:  null
    

    <font color=red id="jump2_5_3">(3)类初始化的六大节点:主动引用【必会重点】</font>

    上边我们已经说过,<font color=red>类初始化</font>和<font color=red>对象初始化</font>的包含关系了。

    那么这六种情况我们以颜色区分,红色部分属于都触发,绿色表示不一定,其他为只触发类初始化。

    ① <font color=red>实例化类对象,即new 对象</font>
    ② <font color=red>反射形式实例化(Class.forName("com.wz.Test"))</font>
    ③ <font >调用类的静态方法、访问某个类或接口的静态变量,或者对该静态变量赋值。<font color=red>(除被final修饰并且编译期会直接放入常量池的值。特殊情况:final static str = Math.random();就会触发类初始化) </font>
    ④ <font color=green>初始化某个子类,其父类也会被初始化(是否触发对象初始化,取决于子类是哪种方法初始化。如果子类是new对象,那么父类也是对象初始化)</font>
    ⑤ <font >jvm启动时被标记为启动类的类(javaTest),含有Main主函数的类。</font>
    ⑥当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发初始化。

    <font id="jump2_5_4" color=red>(4)被动引用:不会初始化</font>

    这几种被动引用的情况,并不会触发类初始化。

    ① <font >通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。</font>
    ② <font >通过数组定义来引用类,不会触发此类的初始化。</font>
    ③ <font ><mark>final修饰</mark>的量并且<mark>在编译阶段会存入调用类的常量池中</mark>,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。</font>(特殊情况:final static str = Math.random();就会触发类初始化)

    所以关键不是final,而是编译期就能确定的值,就不需要触发类初始化了

    ① 举例
    class SuperClass {
        static {
            System.out.println("SuperClass init!");
        }
        public static int value = 123;
    }
    
    class SubClass extends SuperClass {
        static {
            System.out.println("SubClass init!");
        }
    }
    
    public class NotInitialization {
        public static void main(String[] args) {
            System.out.println(SubClass.value);
        }
    }
    执行结果
    SuperClass init!
    
    ② 举例
    class SuperClass2 {
        static {
            System.out.println("SuperClass init!");
        }
        public static int value = 123;
    }
    
    public class Test {
        public static void main(String[] args) {
            SuperClass2[] superClasses = new SuperClass2[10];
        }
    }
    执行结果
    
    
    ③ 举例
    class ConstClass {
        static {
            System.out.println("ConstClass init!");
        }
    
        public static final String HELLO_BINGO = "Hello Bingo";
    }
    
    public class Test {
        public static void main(String[] args) {
            System.out.println(ConstClass.HELLO_BINGO);
        }
    }
    执行结果
    
    

    <span id="jump2_6">(6)使用</span>

    当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。

    换句话说就是,用户可以new对象,并使用对象了。

    <span id="jump2_7">(7)卸载</span>

    <span id="jump2_8">(8)结束生命周期</span>

    1、 执行了 System.exit()方法
    2、 程序正常执行结束
    3、 程序在执行过程中遇到了异常或错误而异常终止
    4、 由于操作系统出现错误而导致Java虚拟机进程终止
    

    <font color=red id="jump3">三、接口的加载过程</span>

    接口加载过程与类加载过程稍有不同。

    (1)当一个类在初始化时,要求其父类全部都已经初始化过了,

    <mark style=background-color:yellow>(2)但是一个接口在初始化时,<font color=red>并不要求其父接口全部都完成了初始化</font>,当真正用到父接口的时候才会初始化。

    不太好举例证明,因为interface中就不能写static{}静态代码块

    <font id="jump4">四、总结</span>

    <font id="jump4_1">1、加载、连接、初始化

    1、加载:查找并加载类的二进制数据到java虚拟机中

    (1)从本地系统中直接加载
    (2)通过网络下载.class文件
    (3)从zip,jar等归档文件中加载.class文件
    (4)从专用数据库中提取.class文件
    (5)将java源文件动态编译为.class文件

    2、连接:

    验证:确保被加载的类的正确性
    <font color=red>准备</font>:

    (1) 为类的静态变量分配内存,并将其初始化为默认值(0,0L,null),不是用户设置的默认值。
    (2)如果是被 final 修饰的类变量,则直接会被初始成用户想要的值。

    解析:把类中的符号引用转换为直接引用,就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程

    3、<font color=red>初始化</font>:只有<font color=red>主动使用</font>的六种情况才会触发

    (1)为类的静态变量赋值成用户的默认值。
    (2)执行静态代码块。

    <font id="jump4_2">2、主动引用和被动引用

    <font color=red>主动引用</font>:会触发类初始化

    (1)实例化对象new。同时会触发对象实例化。
    (2)反射实例化对象,Class.forName("com.wz.Test")。同时会触发对象实例化。
    (3)调用类的静态方法,操作类或接口的静态变量(除去被final修饰并且编译期会直接放入常量池的值。特殊情况:final static str = Math.random();就会触发类初始化)
    (4)子类被初始化,父类会先触发类初始化。
    (5)被标记成启动类的类(javaTest)和main函数所在的类。
    (6)jdk 1.7动态语言支持

    <font color=red>被动引用</font>:不会触发类初始化

    (1)通过子类调用父类的static方法、变量。子类并不会初始化
    (2)调用类或接口的<mark>final修饰</mark>并且<mark>编译期能直接放入常量池</mark>的值,不会初始化(特殊情况:final static str = Math.random();就会触发类初始化)
    (3)通过数组初始化等类型定义的引用,<font color=red>都不会触发初始化。

    1. Test[] array = new Test[10];//定义该类的类型数组
    2. Test test;//单纯定义

    <font id="jump4_3">3、类初始化在jvm生命周期内只触发一次

    <font id="jump5">五、来实战一下</span>

    (1)调用子类静态变量:父类会先初始化

    public class Demo {
        public static void main(String[] args) {
            System.out.println("main:  "+Son.strSon);
    
        }
    }
    class Father{
        public static String strFather = "父类static";
        static {
            System.out.println("父类的静态代码块");
        }
    }
    class Son extends Father{
        public static String strSon = "子类static";
        static {
            System.out.println("子类的静态代码块");
        }
    }
    

    执行结果:

    父类的静态代码块
    子类的静态代码块
    main:  子类static
    

    (2)通过子类调用父类的静态变量,并不会触发子类初始化

    public class Demo {
        public static void main(String[] args) {
            System.out.println("main:  "+Son.strFather);
    
        }
    }
    class YeYe{
        public static String strYeye = "爷爷static";
        static {
            System.out.println("爷爷的静态代码块");
        }
    }
    class Father extends YeYe{
        public static String strFather = "父类static";
        static {
            System.out.println("父类的静态代码块");
        }
    }
    class Son extends Father{
        public static String strSon = "子类static";
        static {
            System.out.println("子类的静态代码块");
        }
    }
    

    执行结果:

    爷爷的静态代码块
    父类的静态代码块
    main:  父类static
    

    (3)调用final static类型并且编译期就能放入常量池的值,不会触发初始化

    public class Demo {
        public static void main(String[] args) {
            System.out.println("main:  "+Son.strFather2);
        }
    }
    class YeYe{
        public static String strYeye = "爷爷static";
        static {
            System.out.println("爷爷的静态代码块");
        }
    }
    class Father extends YeYe{
        public static String strFather = "父类static";
        public final static String strFather2 = "父类Final static";
        static {
            System.out.println("父类的静态代码块");
        }
    }
    class Son extends Father{
        public static String strSon = "子类static";
        static {
            System.out.println("子类的静态代码块");
        }
    }
    

    执行结果

    main:  父类Final static
    

    (4)调用final static类型,但编译期不能确定的值,会触发初始化

    
    public class Demo {
        public static void main(String[] args) {
            System.out.println("main:  "+Son.strFather2);
    
        }
    }
    class YeYe{
        public static String strYeye = "爷爷static";
        static {
            System.out.println("爷爷的静态代码块");
        }
    }
    class Father extends YeYe{
        public static String strFather = "父类static";
        public final static String strFather2 = Math.random() + "";
        static {
            System.out.println("父类的静态代码块");
        }
    }
    class Son extends Father{
        public static String strSon = "子类static";
        static {
            System.out.println("子类的静态代码块");
        }
    }
    
    

    执行结果

    爷爷的静态代码块
    父类的静态代码块
    main:  0.19088731957847427
    

    (5)巩固一下,当有main方法时的情况。

    
    
    public class Demo {
        public static void main(String[] args) {
            System.out.println("main:  " + Demo.str2);
        }
    
        public Demo() {
            System.out.println("构造方法str1: " + str1 + "  str2" + str2);
        }
    
        String str0 = "0000";
    
        {
            System.out.println("构造代码块" + str0);
    
            //这么写编译器会报错,因为构造代码块如果引用非静态成员变量时,是按书写顺序执行的。
            //所以str1是引用不到的。
    //        System.out.println("构造代码块" + str0 + str1);
        }
    
        String str1 = "1111";
    
        static String str2 = "2222";
    
        static {
            System.out.println("静态代码块str2: " + str2);
            str2 = "3333";
            System.out.println("静态代码块================");
            System.out.println("静态代码块str2: " + str2);
            //即便是final修饰的参数,优先级也是和static一样的。只是他的值是可能在编译期就能确定。
            //所以这使用str3,str4,也是引用不到的。
    //        System.out.println("静态代码块str3: " + str3);
    //        System.out.println("静态代码块str4: " + str4);
        }
        static final String str3 = Math.random()+"";
        static final String str4 = "44444";
    }
    
    

    执行结果

    静态代码块str2: 2222
    静态代码块================
    静态代码块str2: 3333
    main:  3333
    

    总结:

    1、Demo是Main主函数的类,所以会触发<font color=red>类加载</font>,但不会触发<font color=red>对象初始化</font>。所以不会执行构造代码块和构造函数,也不会给非静态变量str1初始化内存空间。

    2、同优先级的变量和代码块,会按照书写顺序编译。所以str1写在构造代码块下边,那么构造代码块就不能使用str1了,因为构造代码块会在初始化str1之前就执行了。而且如果你去使用的话是会报编译时错误的。

    <font id="zongjie">六、根据上边的最后一道题,总结执行顺序如下:

    • 1、static 修饰的变量、代码块。
    static String str2 = "2222";
    static{
        
    }
    static final String str3 = Math.random()+"";
    static final String str4 = "44444";
    1、执行顺序看书写顺序
    2、即便是被final修饰过,执行优先级也和static没区别,只是他的值可能在编译期就确定了。
    str4在编译期就能确定,但是str3就不能确定。
    
    • 2、普通成员变量、构造代码块。
    String str0 = "0000";
    {
        
    }
    String str1 = "1111";
    执行顺序也是看书写顺序,谁在前先执行谁
    
    • 3、构造函数
    最后执行构造函数内容
    

    特别鸣谢:总结来自"宜春"大神的:【JVM篇二】

    相关文章

      网友评论

          本文标题:类加载机制(1)类加载过程

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