类的生命周期:加载,连接(该阶段比较复杂,可以被分为验证、准备、解析,这三个阶段),初始化(最重要,因为程序员可以干涉),使用,卸载。
加载:
第一步:是类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息,并存入内存。
第二步:类加载器在加载完类后,Java虚拟机会将字节码中的信息保存到方法区中。
第三步:生成一个InstanceKlass对象(这个对象是用c++编写的,Java不能直接操作),保存类的所有信息,里面还包含特点功能,如,多态的消息。
第四步:同时,Java虚拟机还会在堆中生成一份与方法区中数据类似的java.lang.Class对象(主要用于实现反射,包含了字段,方法和静态字段的数据等)这个堆中的class对象和方法区的InstanceKlass文件互联,可以互相找到彼此。作用是在Java代码中去获取类的消息以及存储静态字段的数据(jdk8及以后)。
为什么要在堆中复制一份方法区的对象数据信息呢?
因为方法区的Klass文件是c++编写的,Java不能直接操作,且对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中的所有信息。 堆中的Class文件中的字段信息是少于方法区的Klass文件的,因为我们并不需要所有字段信息。
连接:验证、准备、解析
验证: 验证内容是否满足《Java虚拟机规范》。
主要验证4部分:
1、文件格式验证,比如class文件的魔术验证
2、元信息验证,比如类必须要有父类,不指定则默认为java.lang.object
3、验证程序执行指令的语义,比如方法中的强行跳转指令。验证指令的正确性。
4、符号引用验证,例如是否访问了其他类中的private方法等。
准备:给静态变量分配内存并赋初始值0(不管代码中写的赋值多少,这里int、short、byte赋值0;long赋值0L;double赋值0.0;boolean赋值false;char赋值'\u0000';引用类型赋值为null。
final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。就是不会赋值给默认初始值了。
解析:将常量池中的符号引用替换成指向内存的直接引用。符号引用就是在字节码文件中使用编号来访问常量池中的内容。
初始化
执行静态代码块中的代码,并为静态变量赋值。会执行字节码文件中的clinit方法(就是class+init,类初始化)部分的字节码指令。
导致类的初始化的几种方式:
1、访问一个类的静态变量或静态方法。注意:变量是final修饰的且等号右边是常量,是不会触发类初始化的。
2、调用Class.forName(Stirng className)
3、new一个该类的对象。
4、执行main方法的当前类。
clinit指令在特定情况下不会出现,如以后几种:
1、无静态代码块且无静态变量赋值语句。
2、 有静态变量的声明,但没有赋值语句。
3、静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
注意:
数组的创建不会导致数组中元素的类进行初始化。
final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化。
复合类的初始化(如继承)
1、直接访问父类的静态变量,不会触发子类的初始化。
2、子类的初始化clinit调用前,会先调用父类的clinit初始化方法。