一个类的生命周期
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,解析阶段不一定,它在某些情况下可以在初始化阶段之后再开始。
什么时候执行类加载?
《Java虚拟机规范》没有明确规定什么事实执行类加载。
什么时候执行类的初始化?
《Java虚拟机规范》明确规定了有且只有发生以下几种情况必须立即对类进行初始化(加载、验证、准备需要在此之前开始)
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类还没有进行初始化,则进行初始化
- 使用new关键字实例化对象
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的除外)的时候
- 调用一个类型的静态方法的时候
- 对类进行反射调用的时候,如果没有初始化,则初始化
- 当初始化类的时候,发现其父类没有初始化,则先初始化父类
- 虚拟机启动时,用户指定的主类(包含main()方法的类),虚拟机会先初始化这个类
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化
- 接口中定义了defaut方法,当实现类初始化时,该接口要在其之前初始化
加载 -
“类加载”过程中第一个阶段,Java虚拟机需要完成以下三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
由于《Java虚拟机规范》对这三点要求并不是特别具体,因此Java应用可以灵活去控制,比如“通过一个类的全限定名来获取定义此类的二进制字节流”这条规则,并没有指定从哪里获取二进制字节流,因此实现者可以自行构建读取来源,例如:
- 从ZIP包中读取,JAR、EAR、WAR
- 从网络中获取,Wep Applet
- 运行时生成,动态代理Proxy
数组不通过类加载器加载,由Java虚拟机直接在内存中动态构造出来,但是数组的元素类型(ElementType)最红还是要靠类加载器来完成加载。
加载结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中,并且会在Java堆内存中实例化一个java.lang.Class类的对象(作为程序访问方法区中的类型数据的外部接口)
加载阶段与连接阶段的部分动作是交叉进行的额,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序。
验证
大致可分为以下四个阶段的检验工作:
- 文件格式验证,验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- 是否以魔术0XCAFFBABE开头
- 主、次版本号是否在当前Java虚拟机接受范围之内
- 常量池的常量中是否有不被支持的常量类型
- 指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据
- Class文件中各个部分及文件是否有被删除的或附加的其他信息
- 。。。。
- 元数据验证,对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
- 这个类的父类是否集成了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载(方法参数都一致,但返回类型却不同等)
- 字节码验证是整个验证过程中最复杂的一个阶段,主要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况
- 保证任何跳转指令都不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系,完全不相干的一个数据类型,这是危险和不合法的
- 符号引用验证,该行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段—解析阶段中发生。符号引用验证可以看做是对类自身以外的各类信息进行匹配性校验。
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当前类方法
准备
正式为类中定义的变量(即静态变量)分配内存并设置类变量初始值。
注意点:
1、这个时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中
2、这里所说的初始值“通常情况”下是数据类型的零值
比如:public static int value = 123
,那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,把value赋值为123的putstatic指令是否程序被编译后,存放于构造器()方法中,所以把value赋值为123的动作要到类的初始化阶段才被执行。
但在“不通常情况”下,当字段被final修饰为常量时,则在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值。
解析
将常量池内的符号引用替换为直接引用的过程。
符号引用
符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。引用的目标不一定是已经加载到虚拟机内存当中的内容。
直接引用
直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用引用的目标必定已经在虚拟机的内存中存在。
解析主要包括类或接口的解析、字段解析、方法解析、接口方法解析。
简单的来说就是我们编写的代码中,当一个变量引用某个对象的时候,这个引用在.class
文件中是以符号引用来存储的(相当于做了一个索引记录),在解析阶段就需要将其解析并链接为直接引用(相当于指向实际对象)。如果有了直接引用,那引用的目标必定在堆中存在。
初始化
《Java虚拟机规范》规定,必须在类的首次“主动使用”时才能执行类的初始化,初始化过程包括:
- 类构造器方法
- static静态变量赋值语句
- static静态代码块
同一个类中static静态变量赋值语句和static静态代码块的执行顺序是与定义的先后顺序有关的,如果一个子类进行初始化会先对其父类进行初始化,也就是会先执行父类的静态变量赋值语句/静态代码块。
为了提高性能,HotSpot JVM要等待类初始化时采取装载和链接类,因此如果A类引用了B类,在加载A时并不一定会加载B类(除非需要进行验证),主动对B类执行第一条指令时才会导致B类的初始化,这就需要先完成对B类的装载和链接。