类加载的定义:Java代码经过编译,会生成JVM能够识别的二进制字节流文件(*.class)。在Java程序的运行时,JVM把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
Class文件中的类,从加载到JVM内存,到卸载出内存,共有七个生命周期阶段,分别是加载、验证、准备、解析、初始化、使用、卸载。类加载机制包含了前五个阶段:
初始化和解析发生的先后顺序并不确定,解析可能发生在初始化之前,也可以发生在初始化之后(为了支持Java的动态绑定机制,由于Java多态,暂时不知道到底哪段代码会被调用,所以解析阶段可能会延后)。
类加载的时机:类加载无需等到程序中“首次被使用”时才开始,JVM预先加载某些类也是允许的。
类加载的五个阶段可以总结为三步:加载、链接、初始化。
1.加载
主要完成三件事:
- 通过一个类的全限定名(包名+类名)来获取定义此类的二进制字节流(Class文件)。
- 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构。(方法区就是用来存放已被加载的类信息、常量、静态变量的运行时内存区域)
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
总的来说,加载过程就是将一个类的二进制字节流Class文件转化成对应的java.lang.Class对象的过程。
2.链接
类加载后生成了java.lang.Class对象,接着会进入链接阶段,将类的二进制数据合并入JRE(Java运行时环境)中,链接包含三个阶段:
- 验证:验证被加载的类是否有正确的结构,类数据是否会符合虚拟机的要求,确保不会危害虚拟机的安全。
- 准备:为类的静态变量(static)在方法区分配内存,并赋默认初值(0或者null)。注意如果是静态常量(static final),早在编译期就已经存放在常量池中了,而现在再次在方法区里分配内存,并直接赋设定初值,而非默认初值。如果是普通成员成员变量,则要等到实例化的时候才会在堆中分配内存。
- 解析:将Class文件常量池(不是运行时常量池)内的符号引用替换为直接引用。(静态绑定和动态绑定就在这时进行)直接引用可以是直接指向目标的指针、或是一个能直接定位到目标的句柄。如果有了直接引用,那么引用的目标必定已经在内存中存在。
3.初始化
类的初始化是类加载机制的最后一步,到了初始化阶段才真正执行Java代码。主要工作是为静态变量(static,但不包括static final)赋设定初值。
JVM规范中有且仅有五种情况必须对类进行初始化(类加载机制的前几个阶段自然而然也要在初始化之前进行):
使用new字节码指令创建类的实例,或者使用getstatic、putstatic读取或设置一个静态字段的值(在编译期就已经放入常量池中的static final常量除外),或者调用一个静态方法的时候,对应类必须进行过初始化。
通过java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则要首先进行初始化。
当初始化一个类的时候,如果发现其父类没有进行过初始化,则首先触发父类初始化。
当虚拟机启动时,用户需要指定一个主类(包含main()方法的类),虚拟机会首先初始化这个类。
使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
上面这五种情况被称为主动引用,除了这五种情况,其他所有的类引用方式都不会触发类的初始化,被称为被动引用(三种):
- 通过子类引用父类的静态字段,对于父类属于“主动引用”的第三种情况,父类会被初始化;而对于子类属于“被动引用”,故子类不会初始化。
//父类 |
- 通过数组来引用类,因为是new数组而不是new类的实例,因此属于被动引用。
//父类 |
- 读取类的静态常量(static final),因为已经在编译时就存在常量池了,可以直接从常量池中读,因此不会触发类的初始化,属于被动引用。
//常量类 |