JAVA-类加载机制(2)-类加载的过程

类的加载过程

一,加载

定义:获取.class文件的字节流,转为方法区的数据结构,在内存中创建Class对象作为该类的数据访问入口

内容:【1】,根据类的全限定名获取二进制字节流

     【2】,根据字节流代表的静态存储结构转化为方法区的运行时数据结构

   【3】,在内存中生成该类的java.lang.Class对象,作为方法区该类的数据访问入口(注:内存指的不一定是堆, 如HotSpot虚拟机是存放在方法区)

从.class文件获取字节流方式:

   【1】,zip包读取 如:jar,ear,war等格式文件

   【2】,网络获取 如:Applet

   【3】,运行时计算生成 如:动态代理,java.lang.reflect.Proxy中使用ProxyGenerator.generateProxyClass为特定接口生成形式为"*$Proxy"的代理类的二进制字节流

   【4】,其他文件 如:jsp文件生成对应的Class类

   【5】,数据库 如:中间件服务器SAP Netweaver

实现方式:

  类加载有三种方式:

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

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

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

  扩展:

  Class.forName()和ClassLoader.loadClass()区别

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

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

其他:

  ①,类加载过程中,在内存中创建Class对象,这里的内存不一定是堆内存,根据虚拟机而定,如:HotSpot虚拟机是存放在方法区

  ②,加载阶段和连接阶段交叉进行,加载未结束连接阶段的字节码验证可能就开始了

  ③,类加载可以分为非数组类和数组类加载

    非数组类:通过1,系统提供的类加载器  2,用户自定义的类加载器(重写类加载器的loadClass方法)

    数组类:不使用类加载器创建,由虚拟机直接创建

    数组类创建规则:1.如果数组组件是引用类型,数组会在加载该引用类型的类加载器的类名称上进行标识

            2.如果数组组件不是引用类型,java虚拟机将数组标记为和引导类加载器关联

            3.数组可见性和它的组件类型一致,如不是不是引用类型默认是public

二,验证(连接)

定义:对Class文件的字节流包含的信息进行验证是否符合虚拟机要求,避免危害虚拟机安全

为什么需要验证:Class文件不一定都是Java源码编译而来,例如:使用十六进制编辑器直接编写Class文件,而这些非Java源码编译而来的Class文件不一定格式正确

关闭大部分的类验证配置: -Xverifynone

内容:【1】,文件格式验证  (文件)

         检查字节流是否符合Class文件格式规范,是否能被当前版本的虚拟机处理

                    如: ①,是否已魔数0xCAFEBABE开头

                          ②,主次版本是否在当前虚拟机处理范围之内

            ③,常量池是否有不支持的常量类型

          ④,指向常量的索引值是否指向不存在的常量和不符合类型的常量

            ⑤,CONSTANT_Utf8_info型的常量是否符合UTF8编码

                  ... ... 

    【2】,元数据验证  (数据类型)

          对字节码信息进行语义分析,是否符合Java语言规范

        如:①,是否有父类(除java.lang.Object)

          ②,父类是否继承了不允许被继承的类(final修饰类)

            ③,非抽象类是否实现了父类和接口中要求实现的所有方法

            ④,类的字段和方法是否和父类出现冲突(如:覆盖父类final字段,错误的重载等)

            ... ... 

    【3】,字节码验证  (方法)

        通过数据流和控制流分析,确定程序语义是否合法

        如:①,操作数栈的数据类型与指令代码序列都能配合工作不会出现类型不一致的情况

            ②,跳转指令不会跳转到方法体外

          ③,方法体内的类型转换是有效的,如将父类对象赋值给子类数据类型是不合法的

        关闭StackMapTable对于字节码验证优化配置:

             -XX:-UseSplitVerifier 

           -XX:+FailOverToOldVerifier

     【4】,符号引用验证

        对类自身以外的信息进行匹配性校验

        目的:确保解析动作能正常执行

          发生的时间:解析阶段中虚拟机将符号引用转为直接引用的时候进行符号引用验证 

        校验内容:①,符号引用中通过字符串描述的全限定名是否能找到对应的类

                   ②,指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段

               ③,符号引用中的类,字段,方法的访问性是否可被当前类访问

               ... ... 

        可能的错误:java.lang.NoSuchFieldError, java.lang.NoSuchMethodError等

三,准备(连接)

定义:为类的静态变量在方法区分配内存,设置初始零值

    

其他:

public static final int value = 123;   //准备阶段:设置为123

public staitc int value = 123;          //准备阶段:设置为0,将value赋值为123的putstatic指令编译后存放于类构造器<clinit>()方法中, 初始化阶段:设置为123

四,解析(连接)

定义:虚拟机将常量池内的符号引用替换为直接引用的过程

内容:解析主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄,调用点限定符等7类符号引用

其他:

       1.符号引用:

    用一组符号描述所引用的目标,如:CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info

    和虚拟机实现的内存布局无关

    引用的目标不一定已加载到内存

   2.直接引用:

    直接指向目标的指针,相对偏移量,句柄

    和虚拟机实现的内存布局有关

    引用的目标一定已加载到内存

   3.什么时候开始解析?

    执行anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new,

    putfield, putstatic这16个字节码指令操作符号引用之前,对符号引用进行解析;  

    扩展:

    根据不同的虚拟机实现: 可以在类加载时对常量池中的符号引用符号引用解析或者在符号引用将要被使用前解析

    invokedynamic之外的字节码指令:虚拟机会在第一次解析时对结果进行缓存, 避免重复解析; 

    invokedynamic字节码指令: 本质用于支持动态语言,它对应的引用称为“动态调用点限定符”,只有当程序实际执行到这条指令的才会进行解析

    4.解析的符号引用有哪些?

    (共7类)类或接口,字段,类方法,接口方法,方法类型,方法句柄,调用点限定符

    对应常量池的符号引用:CONSTANT_Class_info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info, CONSTANT_InterfaceMethodref_info,

               CONSTANT_MethodType_info, CONSTANT_MethodHandle_info, CONSTANT_InvokeDynamic_info

           【1】,类或接口的解析

        对于类D将符号引用N解析为一个类或接口C的直接引用的步骤:

        ①, C不是数组类型:虚拟机将N全限定名-->传递给D的类加载器-->去加载类或接口C

        ②, C是数组类型并且元素类型为对象:如果N描述符类似“[Ljava/lang/Integer”形式,将按照第①规则加载;

                          如果N描述符类似“CONSTANT_*”形式,先加载“java.lang.Integer”,然后虚拟机生成一个数组对象;

            ③,经过①②步骤C成为有效的类和接口,解析完成之前对符号引用验证,D是否具备C的访问权限  

    【2】,字段解析

        ①解析字段所属的类或接口C的符号引用

        ②虚拟机对C进行字段搜索:

          C本身包含与目标字段,返回该字段的直接引用,查找结束

          C实现了接口,将或按照继承关系从下往上递归搜索各个接口,如果接口中包含目标字段,返回该字段的直接引用,查询结束

              C不是java.lang.Object,将按照继承关系从下往上递归搜索父类,父类中包含的简单名称和字段描述都与目标字段相匹配,返回该字段的直接引用,查询结束

          否则:查询失败,java.lang.NoSuchFieldError异常

        ③,搜索成功后,对字段权限验证,是否具备访问权限, java.lang.IllegalAccessError异常

    【3】,类方法解析

        ①,解析方法所属的类或者接口C的符号引用

          ②,虚拟机对C进行方法搜索:

          类方法和接口方法符号引用的常量定义是分开的,如果在类方法中发现class_index中索引的C是个接口发生异常java.lang.IncompatibleClassChangeError

          直接在C中查找,查到返回

            在父类中查找,查找返回

          在C实现的接口列表和父接口查找成功则说明C是一个抽象类,发生异常java.lang.AbstractMethodError

          否则:查询失败,java.lang.NoSuchMethodError异常

        ③,查找成功返回引用前权限验证,是否具备访问权限,java.lang.IllegalAccessError异常

    【4】,接口方法解析  

        ①,解析接口方法所属的类或者接口C的符号引用

        ②,虚拟机对C进行接口方法搜索:

          在接口方法表中发现class_index中的索引C是个类发生异常java.lang.IncompatibleClassChangeError

          直接在接口C中查找,查到返回

          在父接口中查找,查到返回

          否则:查询失败,java.lang.NoSuchMethodError异常

        ③,接口方法默认是public不用检查权限,直接返回引用

五,初始化

定义:执行类构造器<clinit>(),对类变量赋予正确的初始值(如果静态变量)

其他:

      【1】.<clinit>()方法是编译器自动收集类中所有变量的赋值动作和静态语句块(static{})中的语句合并而成的;

     由于静态语句块只能访问在静态语句块之前的变量,如果访问之后的变量会异常“非法向前引用变量”

package com.classload.temp;

public class Test {

    static{
        i = 0;
        System.out.println(i);    //非法向前引用  Cannot reference a field before it is defined
    }
    
    static int i = 1;
}

  【2】.<clinit>()方法与类的构造函数不同,不需要显式调用父类构造器

     子类的<clinit>()方法执行之前,父类的<clinit>()方法已执行完毕

  【3】.父类的静态语句块优先于子类的变量赋值操作

package com.classload.temp;

public class MainTest {
    
    static class Parent{
        public static int A = 1;
        static{
            A = 2;
        }
    }
    
    static class Sub extends  Parent{
        public static int B = A;
    }
    
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
    
    
    //执行结果: 2
}

  【4】,<clinit>()方法不是类或接口所必须的; 如:没有静态语句块和常量赋值

  【5】,接口的<clinit>()方法,只有当父接口中变量使用时,父接口才会初始化; 接口的实现类初始化也不会执行接口的<clinit>()方法

  【6】,同一时间只有一个线程执行类的<clinit>()方法,虚拟机保证线程安全,因此多线程可能会有阻塞发生   

package com.classload.temp;

public class MainTest {
    
    static class DeadLoopClass{
        static{
            if(true){
                System.out.println(Thread.currentThread() + " init MainTest ... ");
                while(true){
                    
                }
            }
        }
    }
    
    public static void main(String[] args) {
        Runnable script = new Runnable(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + " start");
                DeadLoopClass dead = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " end");
            }
        };
        
        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }

   //执行结果:

    Thread[Thread-0,5,main] start
    Thread[Thread-1,5,main] start
    Thread[Thread-0,5,main] init MainTest ...    //第一个线程死循环,第二个线程阻塞等待


}

       

原文地址:https://www.cnblogs.com/wanhua-wu/p/6575654.html