类加载的流程分7个阶段
分别是:加载-->验证-->准备-->解析-->初始化-->使用-->卸载
其中:加载-->验证-->准备,初始化,卸载这5个的加载顺序不能变
类的加载
- 通过类的全限定名获取类的二进制字节流
- 获取字节流可以从jar、zip、war等文件
- 网络中获取
- 运行时计算生成,如动态代理
- 其他文件生成,如jsp
- 数据库读取,可以程序安装到数据库实现代码在集群的分发
- 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.long.Class对象,放在方法区中,作为程序访问这些类型数据的外部入口,c
数组比较特殊:数组不是类加载器创建的,是虚拟机创建的
验证
验证的主要目的是保证class文件中的字节流信息符合虚拟机要求,验证包含以下几点
1.文件格式验证,验证通过后才会在方法区存储
- 是否魔数0XCAFEBABE开头:魔数是class文件的标志,是一个固定值: 0XCAFEBABE 。 他是判断一个文件是不是class格式的文件的标准, 如果开头四个字节不是0XCAFEBABE,那么它不是class文件, 不能被JVM识别。
- 主次版本号是否支持:紧接着魔数的四个字节是class文件的次版本号和主版本号,当前虚拟机版本是否支持
- 常量类型是不是被支持,以及闻不见是否被附加或被删除的其他信息等
2.元数据验证:主要是语义验证,保证语义信息符合java语言的规范,这种一般在编辑器可以识别
- 类是否有父类
- 类是否继承了不允许继承的类,重写了不允许重写的方法等
- 非抽象类是否实现了父类或接口要求实现的全部方法等等
3.字节码验证:主要通过对数据流和控制流的分析,确定语义合法,符合逻辑
- 任何时刻的操作数栈的数据类型和指令代码匹配,不能出现栈中放入int类型,使用是按long类型载入
- 指令跳转不会到方法体意外
- 方法体内的类型转换有效等,如子类赋值给父类安全,父类赋值给之类不允许
4.符号引用验证:发生在解析阶段,是对类自身以外的信息进行匹配性校验
- 类引用的类能找到对应的类
- 引用的类方法是都在引用类中存在
- 访问权限是否合法等
准备
准备阶段:在方法区中为类变量(static修饰的变量)分配内存,并设置初始值
- 初始化:static初始值不是变量的复制,例如static int value = 123,初始值是0;不是123,赋值123的putstatic指令编译后,存放于类构造器<clinit>()方法中,所以赋值为123是在初始化阶段执行
- 对于final常量会直接赋值123(字段存在CobstantValue属性都会初始化为指定的值)
解析
将常量池中的符号引用转变为直接引用
- 符号引用:引用以一组符号来描述
- 直接引用:直接指向目标的指针、偏移量等能定位到目标的句柄
解析动作主要针对,类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符等
- 类或接口解析:查找对应类的全限定名,并交给引用类的加载器加载,可能触发一系列加载动作
- 字段:先进行类解析,然后在找对应类变量,有就返回,没有就去找父类接口,直到找到再返回,如果都没有找到,抛出notSuchFieldError异常
- 类方法解析:先进行类解析,找到类后才能继续
- 引用方法时发现是接口,报错IncompatibleClassChange
- 在类中找到方法返回,没找到,则递归去父类中找,直到找到为止,都没有找到报错NoSuchMethodError
- 接口方法:先解析接口引用,成功继续
- 发现是类,抛出异常
- 现在接口中找,有就直接返回,没有递归父接口查找,能找到就返回结束,没有找到报错
初始化
初始化是类加载的最后一步
类初始化是执行类构造器<clinit>()的过程,
- <clinit>()方法是编辑器自动收集类中的所有类变量赋值动作和静态语句块合并生成的,执行顺序和代码中定义的顺序相同,因此静态变量只能访问定义在前面的静态变量
- 执行类的<clinit>()方法会之前先执行父类的<clinit>()方法,因此父类中的静态语句块先于子类之行
- <clinit>()方法不是必须的,如果类或接口没有静态变量和静态语句块,可以没有
- 接口中不能使用静态语句块,但是有变量初始化赋值操作,接口和类一样生成<clinit>()方法,但是接口不需要先执行父<clinit>()方法,只有当父接口中定义的变量使用时,父接口才会初始化
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁,同步:即当多个线程去初始化一个类时,只会有一个线程执行<clinit>()方法
- 虚拟规定,5种情况必须对类继续进行初始化,也可以概括为,对一个类的主动引用时会触发初始化
- 遇到 new(创建对象) 、getstatic(获取静态变量)、putstatic(设置一个类的静态字段)、invokestatic(调用静态方法)指令时必须初始化
- java.lang.reflect包方法对类进行反射调用时,如果类没有初始化,先进行初始化
- 初始化类时,父类还没有初始化,先初始化父类
- 虚拟机启动指定执行的主类,虚拟机会先初始化主类(main方法的类)
- 实例解析的结果包含REF_getstatic、REF_putStativ、REF_invokeStatic的方法句柄,如果方法对应的类没有初始化,先进行初始化
- 被动引用不会触发初始化
- 数组定义,如果定义class Super;定义Super[] supers = new Supers[10],不会触发
- 静态变量调用会初始化定义静态变量的类:通过子类名直接调用父类的静态变量,父类会初始化,子类不一定会出初始化
例如 class Super{ static{ System.out.print("Super class init") } public static value = "imstatic" } class Son extends Supper{ static{ System.out.print("Son class init") } } class TestMian{ public static void main(String[] args){ System.out.print(Son.value) } } 输出结果:Super class init 没有Son class init
- 常量引用不会初始化类
例如 class Test01{ static{ System.out.print("Test01 class init"); } public static final String HELLO = "hello world"; } class Testmain{ public static void main(String[] args){ System.out.print(Test01.HELLO ); } } 不会输出:Test01 class init 因为 常量HEllO在编译阶段通过常量优化,将常量的值存储到NotInitialization类的常量池中,以后Test01.HELLO的引用实际转化为 NotInitialization类对自身常量池的引用