目录
前言
在使用java语言进行开发工作的时候,几乎每天都需要每天都需要使用的一条语句就是
Object object = new Object();
虽然每天都在使用,但是看似一个简单的new其实java编译器、jvm进行了大量的操作。并且类里面定义的一些变量、静态变量、内部类、静态内部类、构造方法等会在什么时刻初始化和执行呢,执行的顺序又是怎么样的呢?这篇文章就来重点的说一下类创建过程是怎么样的,类里面的成员变量或者方法的初始化或者执行时机又是如何。
一、类的生命周期
类的生命周期主要分为五步,加载、链接、初始化、使用、卸载。其中链接又包括验证、准备、解析三个步骤。加载、验证、准备、初始化的先后顺序是一定的,但是解析可以发生在初始化之后,因为java支持运行时绑定。
Java的动态绑定是由于java存在继承和多态,所以类注入之后,调用类的方法调用的是子类重写的方法,所以初始化的时候有可能并不知道调用的是什么方法,之后运行的时候再先查看子类是否有重写方法,如果子类没有重写则继续查看父类方法,这样一层一层的动态寻找。
二、类创建流程详细分析
1.加载阶段
加载阶段主要是将.class文件通过二进制字节流的方式读入JVM中,虚拟机需要完成一下三件事:
通过classloader在classpath中获取XXX.class文件,将其以二进制流的形式读入内存
将字节流所代表的静态存储结构转化为方法区的运行时数据结构。包括类的全限定名,类的静态变量以及方法
在内存中生成一个该类的Class对象,该对象有指向方法区的类的数据结构的指针。一个类指挥产生一个Class对象,且Class对象存储在堆中。
2、链接阶段
类的链接阶段分为三步:
- 验证:用于检验被加载的类是否有正确的内部结构,比如:是否实现了父类的抽象方法、是否重写了父类的final修饰的方法、通过符号引用是否能找到对应的类和方法
- 准备:为类的静态变量分配内存并赋默认值。对于static修饰的变量或者final static修饰的非字面值静态常量赋默认值,对于final static修饰的静态字面值常量直接赋初值
- 解析:将类的二进制数据中的符号引用替换成直接引用。符号引用使用一组符号描述所引用的目标,这个目标不一定是以及存在内存中的;直接引用则是直接指向目标的指针,是在内存中正式存在的。
三、类的初始化
类在初始化之前必须以及加载、验证、准备完成之后。
类的初始化主要执行的操作为:静态变量的初始化(为静态变量赋初始值。因为在链接的准备阶段static修饰的变量以及final static修饰的非字面值静态常量赋的是默认值)、静态代码块的执行。执行顺序则跟在代码中出现的顺序决定。
在编译生成.class文件时,编译器会产生两个方法加于class文件中:clinit方法和init方法
- clinit方法:是Class类的构造方法,主要是在初始化的时候执行,完成静态变量的赋值和静态代码块的执行
- init方法:主要作用是在类实例化的时候执行,执行内容包括:成员变量的初始化和构造代码块的执行
3.1 在下面这五种情况下类必须进行初始化
- 创建类的实例,也就是new一个对象;获取类的静态变量或者静态非字面值常量
- 调用类的静态方法
- 通过反射获取类的Class对象(Class.forName("xxx"))
- 初始化类的字类
- 启动程序所使用的main方法所在类
四、类的初始化顺序
public class DemoParent {
static {
System.out.println("父类静态代码块!");
}
public DemoParent(){
System.out.println("父类构造方法!");
}
}
子类类
package com.example.test;
public class DemoSon extends DemoParent {
private int a=1;
/**
* 静态变量
*/
private static int b=1;
/**
* 静态字面值常量
*/
private final static int c=1;
/**
* 静态非字面值常量
*/
private final static Integer d=new Integer(1);
private final static DemoMember member = new DemoMember();
{
System.out.println("构造代码块执行:a="+a+",b="+b+",c="+c+",d="+d+",e="+e);
}
static {
System.out.println("静态代码块执行:"+"b="+b+",c="+c+",d="+d);
}
/**
* 静态非字面值常量
*/
private final static Integer e=new Integer(1);
/**
* 构造方法
*/
public DemoSon() {
System.out.println("构造方法执行:a="+a+",b="+b+",c="+c+",d="+d+",e="+e);
}
class innerClass {
public innerClass() {
System.out.println("内部类构造方法执行");
}
}
static class staticInnerClass {
public staticInnerClass() {
System.out.println("静态内部类构造方法执行");
}
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public static int getB() {
return b;
}
public static void setB(int b) {
DemoSon.b = b;
}
public static int getC() {
return c;
}
public static Integer getD() {
return d;
}
public static Integer getE() {
return e;
}
public static DemoMember getMember() {
return member;
}
}
public class DemoMember {
public DemoMember() {
System.out.println("DemoMember的构造方法执行");
}
static {
System.out.println("DemoMember的静态代码块执行");
}
}
在当前DemoSon类中写一个main方法
public static void main(String[] args) {
System.out.println();
}
执行结果为:
父类静态代码块!
DemoMember的静态代码块执行
DemoMember的构造方法执行
静态代码块执行:b=1,c=1,d=1
这就证明 启动程序main方法所在的类必须执行初始化,执行顺序是先执行父类的静态代码块再执行子类静态代码块,并且在类初始化的时候静态非字面值也会进行赋值操作,因为此时DemoMember也实例化了。
public static void main(String[] args) {
DemoSon demoSon = new DemoSon();
System.out.println("=================================");
DemoSon demoSon1 = new DemoSon();
System.out.println("=================================");
}
执行结果为:
父类静态代码块!
DemoMember的静态代码块执行
DemoMember的构造方法执行
静态代码块执行:b=1,c=1,d=1
父类构造方法!
构造代码块执行:a=1,b=1,c=1,d=1,e=1
构造方法执行:a=1,b=1,c=1,d=1,e=1
=================================
父类构造方法!
构造代码块执行:a=1,b=1,c=1,d=1,e=1
构造方法执行:a=1,b=1,c=1,d=1,e=1
new也会执行初始化,并且初始化之后生成Class对象,之后创建对象的时候就不会在进行初始化操作。先调用父类的构造方法再调用字类的构造方法。从运行结果也可以看出,内部类和静态内部类不会进行初始化和实例化。
总结
本文介绍了类的加载过程,以及类初始化和实例化的流程以及流程的执行顺序。大概可以总结为:类进行实例化之前会先进行加载,加载过程会产生Class对象并且进行Class对象的初始化给静态变量赋值并且调用静态代码块,如果存在父类则会先调用父类的静态代码块。实例化对象的时候会先调用父类的构造方法再调用字类的构造方法。Class类只会初始化一次,所以初始化Class对象的时候所执行的操作只会执行一次,所以无论创建多少个对象,他的静态代码块只会执行一次,并静态变量也是一样的。
本文参考:类的加载机制