ps: 最后推荐一下本人的通用后台管理项目spring-mybatis-admin
类得生命周期
在讲类的加载机制前,我们都知道一个对象的生命周期指的是这个对象从创建到销毁的过程,这个国政简单的一句话概括:从JVM将字节码文件加载进内存到卸载出内存为止
。
它的整个生命周期包括:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)和
- 卸载(Unloading)7个阶段
其中准备、验证、解析3个部分统称为连接(Linking)。
1. 验证:验证被加载类的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
2. 验证:验证被加载类的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
- 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
3. 准备:为类的静态变量分配内存空间并将其初始化为默认值
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
- 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>
()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。这里还有一下注意点:
- 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
- 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
- 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
- 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值
4. 初始化:初始化阶段是执行类构造器<clinit>
()方法的过程
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下:
public class Test{ static{ i = 0; System.out.printin(i);// error,非法向前引用 } static int i = 1; }
如果现在去掉错误的那句,改成如下,那么访问结果又是怎样?
public class Test{ static{ i = 0; // System.out.printin(i); } static int i = 1; public static void mian(string args[]){ System.out.println(i); } }
在这里如果你的答案是0,那么恭喜你,你的类加载机制还需要看个四五遍。那么这里简单的分析这份字节码被加载进内存中类变量是如何初始化的。
为类变量设置初始化值主要是在准备和初始化阶段:
- 准备阶段中为类变量 i 设置默认初始值 0
- 初始化阶段按照顺序执行,首先执行static块中的i=0,接着执行static赋值操作i=1,最后在main方法中获取i的值为1。
这里主要就是明白一点:假如类中有初始化语句,则系统依次执行这些初始化语句。
接下来我们在说下类的初始化时机:只有当对类的主动使用的时候(引用该类)才会导致类的初始化,类的主动使用包括以下六种:
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(如Class.forName(“com.shengsiyuan.Test”))
- 初始化某个类的子类,则其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
这里说到了类的主动使用,拿什么是类的被动使用?
- 通过子类引用父类的静态字段,不会导致子类初始化
- 通过数组定义来引用类,不会触发此类的初始化
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
代码实战
public class FinalTest{ public static void main(String[] args){ System.out.println(Final.a); } } calss Final{ public static int a = 10; static{ System.out.println(1); } { System.out.println(2); } public Final{ System.out.println(3); } }
结果:
10
我们把这段代码改一下
public class FinalTest{ public static void main(String[] args){ System.out.println(Final.a); } } calss Final{ public static int a = 10; static{ a = 100; System.out.println(1); } { a = 1000; System.out.println(2); } public Final{ a = 1000; System.out.println(3); } }
结果:
1 10
可以看出代码值经过了Final
类的<clinit>()
,由此在Final中按顺序执行了类变量的初始化和static块的中变量的初始化。(这段断码并没有触发对象的初始化,因为按顺序执行并没有执行Final对象的new操作)接下来再把代码改一下:
public class FinalTest{ public static void main(String[] args){ System.out.println(new Final().a); } } calss Final{ public static int a = 10; static{ a = 100; System.out.println(1); } { a = 1000; System.out.println(2); } public Final{ a = 10000; System.out.println(3); } }
结果
1 2 3 10000
相信看到这里,大部分应该都明白这个 执行结果,创建对象时先执行顺序:
- 父类的静态块
- 子类的静态块
- 父类的成员变量,实例代码块,构造函数
- 子类的成员变量,实例代码块,构造函数,
明白这个这段代码的执行结果就很好理解了。接下来再把这段代码改变一下:
public class FinalTest{ public static void main(String[] args){ System.out.println(Final.a); } } calss Final{ public static Final2 f2 = new Final2(); public static int a = 10; static{ a = 100; System.out.println(1); } { a = 1000; System.out.println(2); } public Final{ a = 10000; System.out.println(3); } } calss Final2{ public static int b = -10; static{ b = -100; System.out.println(4); } { b = -1000; System.out.println(5); } public Final{ b = -10000; System.out.println(6); } }
结果
4 5 6 1 100
这段代码经过了Final
类的<clinit>()
,在Final类中按顺序执行了Final2对象的初始化和类变量的初始化和static块的中变量的初始化。
在此本章说明类加载机制已经全部说完,集中类初始化时机也有所是举例,但是还有一种特俗的情况,见如下代码:
public class FinalTest{ public static void main(String[] args){ System.out.println(Final.a); } } calss Final{ public static int a = 10; public static Final f = new Final(); static{ a = 100; System.out.println(1); } { a = 1000; System.out.println(2); } public Final{ a = 10000; System.out.println(3); } }
结果
2 3 1 100
f
静态变量的实例初始化嵌入到了静态(类)初始化流程中,并且在上面的程序中,嵌入到了静态(类)初始化的起始位置。这就导致了实例初始化完全发生在静态初始化之前。
补充说明:本文后半部分一直在介绍类得初始化,其实跟类得出事化相仿得还有对象得初始化,只不过一般情况类得初始化发生对象初始化之前(可以理解为:在类生命周期中类初始化之后是使用阶段,当在代码里中有new操作时就会发生对象得初始化操作,不过如上文提到也会有特俗情况)。
下面附上对象得初始化时机:
- 使用new关键字创建对象
- 使用Class类的newInstance方法(反射机制)
- 使用Constructor类的newInstance方法(反射机制)
- 使用Clone方法创建对象
- 使用(反)序列化机制创建对象
ps: 最后推荐一下本人的通用后台管理项目spring-mybatis-admin