类的生命周期及执行顺序
类的生命周期
一个类完整的生命周期,会经历五个阶段,分别为:加载、连接、初始化、使用和卸载。其中的连接又分为验证、准备和解析三个步骤。如下图所示:
简单一句话概括,类的加载机制就是:找到需要加载的类并把类的信息加载到jvm的方法区中),然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。结合jvm的内存结构会比较好理解。
加载(Loading)
类的加载方式比较灵活,总结下来有如下几种:
- 据类的全路径名找到相应的class文件,然后从class文件中读取文件内容(常用)
- 从jar文件中读取(常用)
- 从网络中获取:早些年十分流行的Applet。
- 根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应的类自动生成它的代理类。
- 从非class文件中获取,其实这与直接从class文件中获取的方式本质
自定义类加载器也是在这个加载阶段工作的,可以自定义读取类的地方,比如:网络、硬盘、数据库等。
连接(Linking)
连接阶段包括3部分:验证、准备、解析
验证
进行类的合法性校验。会对比如字节码格式、变量与方法的合法性、数据类型的有效性、继承与实现的规范性等等进行检查,确保别加载的类能够正常的被jvm所正常运行。
验证阶段会完成以下校验:
- 文件格式验证
验证字节流是否符合Class文件格式的规范。例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型 ...... 等等
- 元数据验证
对字节码描述的元数据信息进行语义分析,要符合Java语言规范。例如:是否继承了不允许被继承的类(例如final修饰过的)、类中的字段、方法是否和父类产生矛盾 ...... 等等
- 字节码验证
对类的方法体进行校验分析,确保这些方法在运行时是合法的、符合逻辑的。
- 符号引用验证
发生在解析阶段,符号引用转为直接引用的时候,例如:确保符号引用的全限定名能找到对应的类、符号引用中的类、字段、方法允许被当前类所访问 ...... 等等
验证阶段不是必须的,虽然这个阶段非常重要,但是它对程序运行期没有影响,只影响类加载的时间,也就是说程序的启动耗时。Java虚拟机允许程序员主动取消这个阶段,用来缩短类加载的时间,可以根据自身需求,使用 -Xverify:none参数来关闭大部分的类验证措施。
准备
为类的静态变量分配内存,并设为jvm默认的初值;对于非静态的变量(对象实例化时才分配内存),则不会为它们分配内存。简单说就是分内存、赋初值。注意:设置初始值为jvm默认初值,而不是程序设定。规则如下
- 基本类型(int、long、short、char、byte、float、double)的默认值为0,boolean默认值false
- 引用类型的默认值为null
- 常量的默认值为我们程序中设定的值,对于final修饰的静态变量,final static int a = 100,则准备阶段中a的初值就是100,而不是0。非静态的final常量在初始化阶段赋值,比如:static int a = 5,则在准备阶段初始值就是0而非5。
在JDK8取消永久代后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在Java堆中的。
解析
这一阶段的任务就是把Class文件中、常量池中的符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。我们可以把解析阶段中,符号引用转换为直接引用的过程,理解为当前加载的这个类,和它所引用的类,正式进行“连接“的过程。
初始化(Initialization)
类初始化阶段是类加载过程的最后一步。而也是到了该阶段,才真正开始执行类中定义的java程序代码(字节码),之前的动作都由虚拟机主导。
注意这里一定要注意这是类的初始化,不是对象的初始化哦,对象的初始化也就是创建类实例的时候执行。
jvm对类的加载时机没有明确规范,但对类的初始化时机有:只有当类被直接引用的时候,才会触发类的初始化。类被直接引用的情况有以下几种:
- 通过以下几种方式:
- new关键字创建对象
- 读取或设置类的静态变量(注意:在准备阶段就已经赋值的变量,读取时不会触发初始化)
- 调用类的静态方法
- 通过反射方式执行1里面的三种方式;
- 初始化子类的时候,会触发父类的初始化;
- 作为程序入口直接运行时(调用main方法);
- 接口实现类初始化的时候,会触发直接或间接实现的所有接口的初始化。
关于类的初始化,记住两句话
1、类的初始化,会自上而下运行静态代码块或静态赋值语句,非静态与非赋值的静态语句均不执行(是在类实例化对象的时候执行)。
2、如果存在父类,则父类先进行初始化,是一个典型的递归模型。
区别于对象的初始化(实例化),类的初始化所做的一切都是基于类变量或类语句的(static修饰的),也就是说执行的都是共性的抽象信息。而我们知道,类就是对象实例的抽象。
使用(Using)
类的使用分为直接引用和间接引用。
直接引用与间接引用等判别条件,是看对该类的引用是否会引起类的初始化
直接引用已经在类的初始化中的有过阐述,不再赘述。而类的间接引用,主要有下面几种情况:
- 当引用了一个类的静态变量,而该静态变量继承自父类的话,不引起初始化
- 定义一个类的数组,不会引起该类的初始化;
- 当引用一个类的的常量时,不会引起该类的初始化
卸载((Unloading)
当类使用完了之后,类就要进入卸载阶段了。可卸载需要具备以下条件:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
- 加载该类的ClassLoader已经被回收。
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
static关键字
static关键字修饰的数据存储在我们的方法区中的静态常量池中,static可以修饰方法、变量和代码块
static修饰方法:指定不需要实例化就可以激活的一个方法。this关键字不能在static方法中使用,静态方法中不能调用非静态方法,非静态方法可以调用静态方法。
static修饰变量:指定变量被所有对象共享,即所有实例都可以使用该变量。变量属于这个类。
static修饰代码块:通常用于初始化静态变量,静态代码块属于类。没加static的代码块认为是构造代码块
执行顺序
- 实例化对象前,先加载类(对象载入之前,一定要是类先被载入)
- 类(或者可以说静态变量和静态代码块)在生命周期结束前,只执行一次
- 静态变量(属性)和静态代码块谁先声明谁先执行(同一个类中)
- 非静态变量(属性)和非静态代码块谁先声明谁先执行(同一个类中)
- 静态构造代码块是和类同时加载的,静态构造代码块是在实例化之后执行构造方法之前执行的,构造方法是在构造代码块执行完之后才执行的。
- 静态方法属于类的,加载完类就可以调用静态方法(可以执行多次,注意区别于静态代码块,静态代码块只会执行一次);非静态方法是属于对象的,加载完对象就可以调用非静态方法。
- 每创建一个对象,即每载入一个对象,非静态代码块都执行一次。执行类对象的载入之前就会调用
案例一
我们来通过一个例子来验证以下上面的观点
public class InitializeDemo {
private static int k = 1;
private static InitializeDemo t1 = new InitializeDemo("t1");
private static InitializeDemo t2 = new InitializeDemo("t2");
private static int i = print("i");
private static int n = 99;
{
print("初始化块");
j = 100;
}
public InitializeDemo(String str) {
System.out.println((k++) + ":" + str + " i=" + i + " n=" + n);
++i;
++n;
}
static {
print("静态块");
n = 100;
}
private int j = print("j");
public static int print(String str) {
System.out.println((k++) + ":" + str + " i=" + i + " n=" + n);
++n;
return ++i;
}
public static void main(String[] args) {
InitializeDemo test = new InitializeDemo("test");
}
}
输出结果:
1:初始化块 i=0 n=0
2:j i=1 n=1
3:t1 i=2 n=2
4:初始化块 i=3 n=3
5:j i=4 n=4
6:t2 i=5 n=5
7:i i=6 n=6
8:静态块 i=7 n=99
9:初始化块 i=8 n=100
10:j i=9 n=101
11:test i=10 n=102
我们来逐个分析,
一开始调用main方法,main方法内实例化InitializeDemo的对象,在对象载入之前,一定要是类先被载入
所以我们先加载InitializeDemo类,加载类的同时,会加载静态变量和静态代码块,但是是按顺序执行,且只执行一次
先加载如下静态变量
private static int k = 1;
加载如下静态变量的时候,发现要去加载类,由于类已经被加载了,所以会实例化这个对象,这个对象实例化前,会执行非静态代码块和为非静态属性赋值,然后再执行构造方法,按在代码中顺序执行。
private static InitializeDemo t1 = new InitializeDemo("t1");
所以先执行非静态代码块的内容:
{
print("初始化块");
j = 100;
}
输出:1:初始化块 i=0 n=0
初始的时候i的值和n的值默认值是0,执行完这个方法后会变成i=1,n=1
接着为非静态属性赋值:
private int j = print("j");
输出:2:j i=1 n=1
输出时i=1,n=1,执行完这个方法后会变成i=2,n=2
然后执行构造方法
public InitializeDemo(String str) {
System.out.println((k++) + ":" + str + " i=" + i + " n=" + n);
++i;
++n;
}
输出:3:t1 i=2 n=2
输出时i=2,n=2,执行完这个方法后会变成i=3,n=3
t1的实例化执行结束,接着执行t2的实例化
private static InitializeDemo t2 = new InitializeDemo("t2");
结果和上述一致,按非静态代码块和非静态属性然后构造方法方法的顺序执行
输出:
4:初始化块 i=3 n=3
5:j i=4 n=4
6:t2 i=5 n=5
两个静态属性(实例化)执行完,执行如下代码
private static int i = print("i");
输出:7:i i=6 n=6
这里执行完成后,i=7,n=7
接着执行下面的代码,此时n变成了99
private static int n = 99;
注意:执行完这行代码后,n的值就被赋成99了,i的值还是7。
接着执行静态代码块
static {
print("静态块");
n = 100;
}
输出:8:静态块 i=7 n=99
输出时i=7,n=99,执行完这个方法后会变成i=8,n=100
到此类加载完毕,可以看到static变量和静态代码块都按顺序执行了,然后开始实例化test对象,参考t1,t2的实例化,按非静态代码块和非静态属性然后构造方法方法的顺序执行,这里就不会再处理static相关的代码了。
输出:
9:初始化块 i=8 n=100
10:j i=9 n=101
11:test i=10 n=102
案例二
继承中的static执行顺序,看以下例子
public class Test3 extends Base {
static {
System.out.println("test static");
}
public Test3() {
System.out.println("test constructor");
}
public static void main(String[] args) {
new Test3();
}
}
class Base {
static {
System.out.println("Base static");
}
public Base() {
System.out.println("Base constructor");
}
}
输出结果:
Base static
test static
Base constructor
test constructor
执行Test3的构造方法,要先加载Test3的类加载,由于Test3继承于Base,所以他要先加载父类Base,静态代码块先执行。
则会先输出:Base static
再输出:test static
再执行子类的构造方法的时候,要先执行父类的构造方法(一般是找默认的构造方法即无参构造方法,除非在子类的构造方法里指定要调用父类的构造方案),如果是多级继承,会先执行最顶级父类的构造方法,然后依次执行各级子类的构造方法。
所以再输出:Base constructor
然后输出:test constructor
结果就如上。
案例三
再举一个例子
public class MyTest {
MyPerson person = new MyPerson("test");//这里可以理解为成员变量辅助,,要先把MyPerson先加载到jvm中
static {
System.out.println("test static");//1
}
public MyTest() {
System.out.println("test constructor");//5
}
public static void main(String[] args) {//main方法在MyTest类中,使用mian方法先加载MyTest的静态方法,不调用其他,
MyClass myClass = new MyClass();//对象创建的时候,会加载对应的成员变量
}
}
class MyPerson {
static {
System.out.println("person static");//3
}
public MyPerson(String str) {
System.out.println("person " + str);//4 6
}
}
class MyClass extends MyTest {
MyPerson person = new MyPerson("class");//这里可以理解为成员变量辅助,要先把MyPerson先加载到jvm中
static {
System.out.println("class static");//2
}
public MyClass() {
//默认super()
System.out.println("class constructor");//7
}
}
输出:
test static
class static
person static
person test
test constructor
person class
class constructor
下面分析执行步骤:
- 先看MyTest类及其静态的变量,方法和代码块会随类的加载而开辟空间,有一个静态代码块,先执行,所以输出:test static,且此时MyTest类的其他语句不执行,此时MyTest类加载完成。
- 接着看main方法中调用了MyClass myClass =new MyClass(),实例化了一个MyClass类的对象,这时会先加载MyClass类,而MyClass类继承于MyTest类,在加载MyClass类前,会先加载MyTest类,但是MyTest类以及其静态的变量和静态代码块已经加载(在类的生命周期只执行一次),所以返回到子类(MyClass类)的加载,这时候会调用MyClass类的静态的变量和静态代码块。所以输出:class static。
- MyClass类加载完后,在执行MyClass类的构造方法前,先初始化对象的成员变量(先初始化父类MyTest的成员变量),所以执行父类MyTest(类已加载过,这里就直接执行成员变量的初始化)的成员变量:MyPerson person = new MyPerson(“test”),于是会加载MyPerson类和其静态的变量和静态代码块。则先输出:person static
- 加载完MyPerson类和其静态的变量和静态代码块后,回到MyClass类开始执行非静态代码块和属性,由于MyClass继承了MyTest,所以会先初始化MyTest,初始化MyTest类的属性:MyPerson person = new MyPerson("test");会调用MyPerson类的有参构造方法,即输出:person test
- MyTest类的非静态属性和非静态的代码块执行完成后,然后接着执行父类构造方法,即输出:test constructor
- 父类MyTest构造方法执行结束,回到子类MyClass,子类再调用构造方法前,先初始化对象的成员变量MyPerson person = new MyPerson(“class”);,这时候会先先加载MyPerson 和其静态的变量和静态代码块,由于上述类已经加载,而且MyPerson没有非静态属性及非静态代码块,所以直接执行其有参构造方法,即输出:person class
- MyClass再无其他非静态属性及非静态代码块,执行无参构造方法,即输出:class constructor
- MyClass实例化完成,回到MyTest类,无后续代码,至此程序执行完成。
总结
使用一个类创建对象的时候,一般都是先加载类,完成类的初始化(静态变量的赋值和静态代码块的执行,按代码中的先后顺序,整个生命周期中只会执行一次),然后实例化对象,实例化对象时不再处理静态变量和静态代码块,会处理非静态属性和非静态代码块,也是按照代码中的先后顺序执行,最后才调用构造方法。如有继承关系,则按照此规则先执行父类后执行子类。
类加载顺序的三个原则是
- 1、父类优先于子类
- 2、属性和代码块(看先后顺序)优先于构造方法
- 3、静态优先于非静态
整个程序执行顺序:
父类静态变量、父类静态语句块(按代码中的先后顺序执行)--> 子类静态变量、子类静态语句块(按代码中的先后顺序执行)--> 父类非静态变量、父类非静态语句块(按代码中的先后顺序执行)--> 父类构造器 --> 子类非静态变量、子类非静态语句块(按代码中的先后顺序执行)--> 子类构造器 --> 完成
来一张图更直观些。