快速理解类的加载过程

发布于:2024-12-18 ⋅ 阅读:(15) ⋅ 点赞:(0)

当程序主动使用某个类时,如果该类还未加载到内存中,则系统会通过如下三个步骤来对该类进行初始化:

1.加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象;

2.链接:Java类的二进制代码合并到JVM的运行状态之中的过程。

(1)验证:确保加载的类信息符合JVM规范,没有安全方面的问题;

(2)准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配;(static是在内初始化之前就完成了)

(3)解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。()

3.初始化:

(1)执行类构造器<clinit>()方法的过程:类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的(类构造器是构造类信息的,不是构造该类对象的构造器);(JVM去完成的)

(2)当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化;

(3)虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。

总结:

虚拟机把Class文件加载到内存,并对数据进行校验、转换解析和初始化,形成可以虚拟机直接使用的Java类型,即java.lang.Class,如下图:

接下来会对该三个步骤逐一进行解释。

一.类的加载

作用:查找和导入Class文件。

步骤:

1.通过一个类的全限定名获取定义此类的二进制字节流;(那么这个时候需要一个寻找器,来寻找获取我们的二进制字节流,而java中恰好有这么一段代码模块,可以实现通过类全名来获取此类的二进制字节流这个动作,并且将这个动作放到放到java虚拟机外部去实现,以便让应用程序决定如何获取所需要的类,实现这个动作的代码模块成为“类加载器”)

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

3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。(此时静态数据结构已经放进了方法区,但是此时我们没有办法去进行访问,java当中去访问数据的方法是通过引用去操作对象,然后通过对象去操作数据,所以还需要再堆当中去生成一个代表代表这个类的java.lang.Class对象,作为方法区中的数据访问入口)

在加载阶段完成之后,这个时候在内存当中,运行时数据区的方法区以及堆就已经有数据了:

(1)方法区:类信息,静态变量,常量;

(2)堆:代表被加载类的java.lang.Class对象。

及时编译之后的热点代码并不在这个阶段进入方法区。

类加载器:

类加载器作用是用来把类(class)装载进内存的,JVM 规范定义了如下类型的类的加载器:

二.类的链接

1.验证

作用:

验证只要是为了确保Class文件中的字节流包含的信息完全符合当前虚拟机的要求,并且还要求我们的信息不会危害虚拟机自身的安全,导致虚拟机的崩溃。

验证内容:

文件格式的验证、元数据验证、字节码的验证、符号引用的验证。

2.准备

作用:

为类的静态变量分配内存,并且初始化为当前类型的默认值。

解释:

1.这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化,这里不会为实例变量(也就是没加static)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中;

2.进行分配内存的只是包括类变量(静态变量),而不包括实例变量,实例变量是在对象实例化时随着对象一起分配在java堆中的,通常情况下,初始值为零值,假设public static int a=1,那么a在准备阶段过后的初始值为0,不为1,这时候只是开辟了内存空间,并没有运行java代码,a赋值为1的指令是程序被编译后,存放于类构造器()方法之中,所以a被赋值为1是在初始化阶段才会执行。

3.解析

作用:

把类中的符号引用转换为直接引用:

(1)符号引用就是一组符号来描述目标,可以是任何字面量;

(2)直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

(因为再一个Class文件中,没有办法去表示引用关系,只能告诉你引用到10行或者20行这种,直接应用就表示你执行的位置在内存当中有一块具体的地址,比如说a指向b是符号应用,a指向b所在的位置0x01,在内存中这叫直接引用)

解释:

(1)解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行;

(2)直接应用是与虚拟机内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般是不相同的,如果有了直接引用,那引用的目标必定存在内存中;

(3)类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过VM垃圾回收机制可以回收这些Class对象。(同一符号引用进行多次解析请求是很常见的,除invokedynanic指令以外,虚拟机实现可以对第一次解析结果进行缓存,来避免解析动作重复进行,无论是否真正执行了多解析动作,虚拟机需要保证的是在同一个实体中,如果一个引用符号之前已经被成功解析过,那么后续的引用能析请求就应当一直成功,同样的,如果第一次解析失败,那么其他指令对这个符号的解析请求也应该收到相同的异常)

(4)inDy(invokedynamic)是java7引入的一条新的虚拟机指令,这是自 1.0 以来第一次引入新的虚拟机指令,到了 java 8 这条指令才第一次在java 应用,用在 lambda 表达式中,indy 与其他 invoke 指令不同的是它允许由应用级的代码来决定方法解析。

三.类的初始化

作用:

初始化阶段是执行类构造器Clinit()方法的过程,或者讲得通俗易懂些,加载和链接以外的工作都需要在初始化中去完成。

在准备阶段,类变量已赋过一次系统要求的初始值,而在初始化阶段,则是根据自己通过程序制定的主观计划去初始化变量和其他资源,比如赋值。

什么时候发生类初始化:

1.类的主动引用(一定会发生类的初始化):

(1)当虚拟机启动,先初始化main方法所在的类;

(2)new一个类的对象;

(3)调用类的静态成员(除了final常量)和静态方法;

(4)使用java.lang.reflect包的方法对类进行反射调用;

(5)当初始化一个类,如果其父类没有被初始化,则先会初始化它的父类;

2.类的被动引用(不会发生类的初始化):

(1)当访问一个静态域时,只有真正声明这个域的类才会被初始化。如:当通过子类引用父类的静态变量不会导致子类初始化;

(2)通过数组定义类引用,不会触发此类的初始化;

(3)引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)。

在Java中对类变量进行初始值设定有两种方式:

1.声明类变量是指定初始值;

2.使用静态代码块为类变量指定初始值。

按照程序逻辑,必须把静态变量定义在静态代码块的前面,因为两个的执行是会根据代码编写的顺序来决定的,顺序搞错了可能会影响你的业务代码。

JVM初始化步骤:

1.假如这个类还没有被加载和连接,则程序先加载并链接该类;

2.假如该类的直接父类还没有被初始化,则先初始化其直接父类;

3.假如类中有初始化语句,则系统依次执行这些初始化语句。

四.举例

代码如下:

public class Test {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(A.m);
        /*
        1.加载到内存 ,会产生一个类对应class对象
        2.链接 ,链接结束后 m = 0
        3.初始化 m= 100:
            <clinit>(){
                System.out.println("A类静态代码块初始化”);
                m = 300;
                m=100
            }
         */
    }
}

class A {
    static {
        System.out.println("A类静态代码块初始化");
        m = 300;
    }

    static int m = 100;


    public A() {
        System.out.println("A类的无参构造初始化");
    }
}

输出结果:

A类静态代码块初始化
A类的无参构造初始化
100

 大概流程如下:

首先在方法区产生了一些该类的静态数据,然后在加载的类的时候,就产生了对应的class,然后看main()方法,main()方法后就开始链接,此时m有一个初始值0,等链接完没有问题的时候,就开始执行代码,此时new A(),就产生了一个A类的对象,这个对象就会去找到自己的Class类(指向),通过A类的数据结构,给A类对象赋值(拿到数据),赋值完成后,通过clinit()方法初始化数据,得到m等于100。


网站公告

今日签到

点亮在社区的每一天
去签到