声明:本文总结自《深入理解JAVA虚拟机》(第二版),周志明著
类加载机制:虚拟机把描述类的数据从class文件加载到内存,然后对数据进行校验、解析和初始化最终形成可以被虚拟机直接使用的java类型。
java语言天生动态扩展的语言特性是运行期动态加载和动态连接,无论是JSP还是相对复杂的OSGi都是用java运行期类加载的特性。
类加载机制的时机
类被加载到被卸载的整个生命周期:
加载、验证、准备、解析、初始化、使用、卸载。
java虚拟机严格规范了初始化的6种情况:
- 四条指令:newgetstaticputstaticinvokestatic;常见的是:当有new关键字实例化对象的时候、读取或设置一个类的静态字段、调用一个类的静态方法的时候
- 反射调用时,类没有初始化,需要触发
- 有继承时,子类初始化的时候,父类必须先初始化;
- 当要执行main方法的主类时,需要先初始化这个主类
- 使用jdk1.7的时候,如果java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且这个方法句柄所对应的类没有初始化,需要出发初始化。
- JDK8中 default修饰的方法的接口,其实现类发生初始化的时候,该接口也需要立即初始化。(第三版中补充)
以上5种情况都是主动引用,被动引用是不会触发初始化的:
1.通过子类引用父类的静态字段,子类不会初始化,但是父类会。
2.通过数组定义来引用类,不会触发初始化。
例如:Dog[] dogs = new Dog[6];
3.常量在编译阶段会存入调用类的常量池,本质上没有引用到定义常量的类,所以不会触发初始化。
例如:public static final String HE = “he”;
类.HE;调用 不会触发初始化。
接口与类的初始化异同:
类的初始化要求父类必须全部初始化,而接口只有在调用到父接口的常量时才会将父接口初始化;
类加载过程
1.加载:
1). 通过类的全限定名来获取此类的二进制字节流;
2). 将此字节流所代表的静态存储结构转化为方法区运行时数据结构;
3). 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的数据访问入口
2.验证:
文件格式验证:这个阶段是基于字节流进行的,验证过后才会进入内存的方法区中进行存储。
元数据验证: 对类的元数据信息进行语义校验。
字节码验证:通过数据流和控制流进行分析确定程序语义是合法的
符号引用验证:确保解析动作能正常执行,如果无法通过会抛出IllegalAccessError、NoSuchFieldError、NoSuchMethodError等。
3.准备:
准备阶段是为类变量 分配内存并设置类变量初始值的阶段,这些变量所使用的内存是在方法区进行分配的。
注意:
- 此时分配内存的仅仅包含类变量(被static修饰的),而不是实例变量。
- 初始值一般为零值。
- final修饰的则初始为常量。
4.解析:
虚拟机将常量池内的符号引用替代为直接引用的过程。
5.初始化:
初始化阶段是执行类构造器< c l i n i t>()方法的过程。
- < c l i n i t>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而成,编译器收集的顺序是按照源码中出现的顺序决定的。
- < clinit>()与类的构造函数(实例构造器< init>())不同,他不需要显示的调用父类构造器,虚拟机会保证子类的类构造器< clinit>()方法执行前,父类先执行完毕。
- 父类中< clinit>()先执行,故父类定义的静态代码块比子类的变量赋值优先。
- < clinit>()对于类或者接口不是必须的,当类没有赋值操作、没有静态代码块,编译器可以不为它生成此方法。
- 接口不能使用静态代码块,但仍会有变量初始化赋值,因此也会生成< clinit>()方法;和类不同,接口无需父接口执行它的< clinit>()。
- 多线程环境下,只有一个线程会执行此类的< clinit>()方法。