【JVM】类的生命周期【转+整理】
参考如下三篇并整理。
类的生命周期是从被加载到虚拟机内存中开始,到卸载出内存结束。过程共有七个阶段。
1.加载---2.验证---3.准备---3.解析---5.初始化---6.使用---7.卸载
|________连接________|
|______________类的加载过程_____________|
|______________________类的生命周期_______________________|
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。
在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。
另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
【1.加载(装载)】
在装载阶段,虚拟机需要完成以下3件事情
(1) 通过一个类的全限定名来获取定义此类的二进制字节流。
(2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
虚拟机规范中并没有准确说明二进制字节流应该从哪里获取以及怎样获取,这里可以通过定义自己的类加载器去控制字节流的获取方式。
这里第1条中的二进制字节流并不只是单纯地从Class文件中获取,比如它还可以从Jar包中获取、从网络中获取(最典型的应用便Applet)、由其他文件生成(JSP应用)等。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
【2.验证(校验、检查)】
虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统奔溃。
1.文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
2.元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
4.符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,
那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
【3.准备】————为类的静态变量分配内存,并将其初始化为默认值
这个阶段正式为类变量(被static修饰的变量)分配内存并设置类变量初始值,这个内存分配是发生在方法区中。
1、注意这里并没有对实例变量进行内存分配,实例变量将会在对象实例化时随着对象一起分配在JAVA堆中。
2、这里设置的初始值,通常是指数据类型的‘“零”值。
public static int value = 123;
value在准备阶段过后的初始值为0而不是123,而把value赋值的putstatic指令将在初始化阶段才会被执行。
注意:
(1)对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值。
(2)对基本数据类型来说,对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
(3)对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;
(4)只被final修饰的常量则既可以在声明时显式地赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
(5)对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
(6)如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
3、static final常量在准备阶段变量value就会被初始化为ConstValue属性所指定的值,将其结果放入了调用它的类的常量池中。
public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。
下表列出了Java中所有基本数据类型以及reference类型的默认零值:
【4.解析】————把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
【5.初始化】
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下(四大种)六小种:
【1】在如下三个场景(能生成四个字节码指令的场景),如果类还未初始化,则初始化。
(4个指令【new、getstatic、putstatic、invokestatic】)
A.new
new Test();
B.读取或设置类的静态变量
int b=Test.a;
Test.a=b;
C.调用静态函数
Test.doSomething();
【2】反射调用。
Class.forName(“com.mengdd.Test”);
【3】子类初始化,要先初始化父类(所有父类)。
【4】虚拟机启动时,要初始化主类。直接使用java.exe命令来运行某个主类。
只有上述四种情况会触发初始化,也称为对一个类进行主动引用。
除此以外,有其他方式都不会触发初始化,称为被动引用。
注意:
(1)子类引用父类的静态变量,不会导致子类初始化。
(2)通过数组定义引用类,不会触发此类的初始化
(3)引用常量时,不会触发该类的初始化
举例:
(1)子类引用父类的静态变量,不会导致子类初始化。
public class SupClass
{
public static int a = 123;
static
{
System.out.println("supclass init");
}
}
public class SubClass extends SupClass
{
static
{
System.out.println("subclass init");
}
}
public class Test
{
public static void main(String[] args)
{
System.out.println(SubClass.a);
}
}
执行结果:
supclass init
123
(2)通过数组定义引用类,不会触发此类的初始化
public class SupClass
{
public static int a = 123;
static
{
System.out.println("supclass init");
}
}
public class Test
{
public static void main(String[] args)
{
SupClass[] spc = new SupClass[10];
}
}
执行结果:
(3)引用常量时,不会触发该类的初始化
public class ConstClass
{
public static final String A= "MIGU";
static
{
System.out.println("ConstCLass init");
}
}
public class TestMain
{
public static void main(String[] args)
{
System.out.println(ConstClass.A);
}
}
执行结果:
MIGU
用final修饰某个类变量时,它的值在编译时就已经确定好放入常量池了,所以在访问该类变量时,等于直接从常量池中获取,并没有初始化该类。
初始化的步骤
1、如果该类还没有加载和连接,则程序先加载该类并连接。
2、如果该类的直接父类没有加载,则先初始化其直接父类。
3、如果类中有初始化语句,则系统依次执行这些初始化语句。
在第二个步骤中,如果直接父类又有直接父类,则系统会再次重复这三个步骤来初始化这个父类,
依次类推,JVM最先初始化的总是java.lang.Object类。
当程序主动使用任何一个类时,系统会保证该类以及所有的父类都会被初始化。
【6.使用】
【7.卸载】
在如下几种情况下,Java虚拟机将结束生命周期
1.执行了System.exit()方法
2.程序正常执行结束
3.程序在执行过程中遇到了异常或错误而异常终止
4.由于操作系统出现错误而导致Java虚拟机进程终止