Java类加载过程 ——Thinking in Java学习笔记(六)

java中一个类从被加载开始,一直到被销毁为止,类的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
其中,类加载过程包括加载、验证、准备、解析、初始化,其中,验证、准备、解析又被合称为连接过程。

类加载过程

1、加载阶段

加载过程的主要工作有:

1)通过一个类的全限定名来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在java堆中生成一个代表这个类的Class对象,作为访问方法区中这些数据的入口。

java通过双亲委托模型来查找这些class对象,详见Java类选择器 ——Thinking in Java学习笔记(五)

2、验证阶段

验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证语义分析操作验证等。

格式验证:验证是否符合class文件规范

语义验证:检查一个被标记为final的类型是否包含子类;检查一个类中的final方法是否被子类进行重写;确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)

操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否通过富豪引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)

验证过程

3、准备阶段

这个阶段正式为类变量(被static修饰的变量)分配内存并设置类变量初始值,这个内存分配是发生在方法区中。

1、注意这里并没有对实例变量进行内存分配,实例变量将会在对象实例化时随着对象一起分配在JAVA堆中。

2、这里设置的初始值,通常是指数据类型的零值(默认值)。

private static int a = 3;

这个类变量a在准备阶段后的值是0,将3赋值给变量a是发生在初始化阶段。

4、解析阶段

将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。
可以认为是一些静态绑定的会被解析,动态绑定则只会在运行是进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写)

5、初始化阶段

初始化是类加载机制的最后一步,这个时候才正真开始执行类中定义的JAVA程序代码。在前面准备阶段,类变量已经赋过一次系统要求的初始值,在初始化阶段最重要的事情就是对类变量进行初始化,关注的重点是父子类之间各类资源初始化的顺序。

java类中对类变量指定初始值有两种方式:1、声明类变量时指定初始值;2、使用静态初始化块为类变量指定初始值

初始化的时机

1)创建类实例的时候,分别有:1、使用new关键字创建实例;2、通过反射创建实例;3、通过反序列化方式创建实例。

new Test();
Class.forName(“com.mengdd.Test”);

2)调用某个类的类方法(静态方法)

Test.doSomething();

3)访问某个类或接口的类变量,或为该类变量赋值。

int b=Test.a;
Test.a=b;

4)初始化某个类的子类。当初始化子类的时候,该子类的所有父类都会被初始化。

5)直接使用java.exe命令来运行某个主类。

除了上面几种方式会自动初始化一个类,其他访问类的方式都不会触发类的初始化,称为被动引用。

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 static final修饰的常量)

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类。当程序主动使用任何一个类时,系统会保证该类以及所有的父类都会被初始化。

在第三个步骤中,类初始化的顺序为:

1. 父类静态成员和静态初始化块
2. 子类静态成员和静态初始化块
3. 父类的实例成员和实例初始化块
4. 执行父类的构造方法
5. 子类实例成员和实例初始化块
6. 执行子类的构造方法

参考材料:
1、https://www.cnblogs.com/xiaoxian1369/p/5498817.html
2、https://www.cnblogs.com/dongguacai/p/5860241.html

猜你喜欢

转载自blog.csdn.net/qq_40509039/article/details/83268294