JVM中类的加载流程

        类的加载机制分为如下三个阶段:加载,连接,初始化。其中连接又分为三个小阶段:验证,准备,解析。

加载阶段:

JVM加载class文件的方式有以下几种:

从本地系统中直接加载

通过网络下载.class文件

从zip,jar等归档文件中加载.class文件

从专有数据库中提取.class文件

将Java源文件动态编译为.class文件

加载阶段是类生命周期的第一步,它涉及到三个主要的活动:查找类的二进制数据、将这些数据带入JVM中、将其放在运行时数据区的方法区内、以及在堆内存中创建一个java.lang.Class对象来用来封装类在方法区内的数据结构,代表这个类。

具体来说,加载阶段可以分为以下几个步骤:

  1. 类的定位:这个步骤涉及到通过类的完全限定名来查找此类的二进制形式。JVM可以通过多种方式来定位和加载类,包括从本地文件系统读取.class文件、通过网络下载类的定义、从zip包中提取、运行时计算生成类的定义等。
  2. 类的加载:一旦JVM找到了类的二进制数据,它会将这些数据加载到内存中。这个过程通常涉及到将读取到的字节码数据分配到JVM的方法区(Method Area)中。在Java 8及之前的版本中,方法区的物理实现是永久代(PermGen space);在Java 8及以后的版本中,方法区的物理实现被称为元空间(Metaspace)。
  3. 类的解析:加载阶段的最后一个步骤是在堆内存中创建一个java.lang.Class对象,这个对象用来代表刚刚被加载进来的类。这个Class对象是访问类的元数据的入口点,包括类的方法、字段和构造函数等。

值得注意的是,都说JVM为了减少内存的使用,采用的是懒加载策略,也就是说,类会在首次使用时才被加载。但其实类加载并不需要等到某个类被主动使用的时候才加载,JVM规范允许类加载器在预料到某个类要被使用的时候就预先加载。如果预先加载过程中报错,类加载器必须在首次主动使用的时候才会报错。如果类一直没有被使用,就不会报错。

链接阶段

链接阶段主要负责将类的二进制数据合并到JVM的运行时状态中,确保类的结构正确,并为类的初始化做准备。

1. 验证(Verification)

        验证是链接过程中的第一步,其主要目的是确保被加载的类或接口的二进制表示在结构上是正确的,不会危害到JVM的安全。这个过程是为了检测和防止潜在的安全问题,比如确保类中的数据不会溢出、循环引用等。验证过程主要包括以下几个检查:

        此阶段验证的内容如下:

        类文件的结构检查:

        确保类文件遵从java类文件的固定头格式,就像平时做文件上传验证文件头一样。还会验证文件的主次版本号,确保当前class文件的版本号被当前的ivm兼容。验证类的字节流是否完整,根据md5码进行验证。

        语义检查:

检查这个类是否存在父类,父类是否合法,是否存在

检查该类是不是final的,是否被继承了。被final修饰的类是不允许被继承的。

检查该类的方法重载是否合法。

检查类方法翻译后的字节码流是否合法。

引用验证,验证当前类使用的其他类和方法是否能够被顺利找到。

2. 准备(Preparation)

通过验证阶段之后,开始给类的静态变量分配内存,设置认的初始值。类变量的内存会被分配到方法区中,实例变量会被分配到堆内存中。准备阶段的变量会赋予初始值,但是final类型的会被赋予它的值,可以理解为编译的时候直接编译成常量赋值。如果是一个int类型的变量会分配给他4个字节的内存空间,并赋予值为0。如果是long会赋予给8个字节,并赋予0。

3. 解析(Resolution)

解析是链接阶段的最后一步,此阶段涉及将类、接口、字段和方法的符号引用替换为直接引用。比如Worker类的gotoWork方法会引用car类的run方法。

在work类的二进制数据,包含了一个Car类的run的符号引用,由方法的全名和相关描述符组成。解析阶段,java虚拟机会把这个符号引用替换成一个指针,该指针指向car类的run方法在方法区中的内存位置,这个指针就是直接引用。

这一步是必需的,因为Java程序在编译时生成的是包含符号引用的代码,而在JVM运行时,需要将这些符号引用转换为可以直接指向目标的引用,以便快速访问和执行。

  • 类和接口的解析:将类和接口的符号引用转换为实际的内存地址指向的Class对象的引用。
  • 字段解析:将字段的符号引用转换为具体的字段地址或表示。
  • 方法解析:将方法的符号引用转换为可以直接调用的方法入口。

解析过程可能不会在链接阶段一次性完成,它可以是动态的,也就是说,在Java中,某些符号引用是在首次使用时才解析的。

初始化

初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码)。

这个阶段就是对类中所有变量赋予正确的值,静态变量的赋值和成员变量的赋值都在此完成。初始化的顺序参考上方的整理。

初始化阶段利用了一种懒加载的思想,所有Java虚拟是执行类构造器机实现必须在每个类或接口被Java程序首次主动使用才初始化。

初始化有几点需要注意。如果类还没有被加载和连接,就先进行加载和连接。如果存在直接的父类,父类没有被初始化,则先初始化父类。

初始化时机:

类分为主动使用和被动使用。主动使用使类进行初始化,被动使用不会初始化。

主动使用有以下六种情形:

创建类的实例

访问某个类或接口的静态变量,或者对静态变量进行赋值

调用类的静态方法

反射

初始化一个类的子类.

具有main方法的java启动类.

需要注意的是:

初始化一个类的时候,要求他的父类都已经被初始化,此条规则不适用于接口。初始化一个类的时候,不会初始化它所实现的接口,在初始化一个接口的时候,并不会初始化他的父接口。

只有到程序访问的静态变量或者静态方法确实在当前类或当前接口中定义的时候,才可以认为是对类或接口的主动使用。

注意,调用classloader类的loadclass方法加载一个类,不是对类的主动使用。因为loadclass调用的一个子方法具有两个参数,name和resolve,由于resolve是false。在代码中并不会调用resolveClass,所以不会对类进行解析。

被动使用的几种情况:

(1)通过子类引用父类的静态字段,为子类的被动使用,不会导致子类初始化。

(2)通过数组定义类引用类,为类的被动使用,不会触发此类的初始化。

(3)常量在编译阶段会存入调用方法所在类的常量池中。再引用就是直接用常量池中的值了。

Java类实例化时,JVM 初始化顺序?

正确的顺序如下

1. 父类静态代码块

2. 父类静态变量

3. 子类静态代码块

4. 子类静态变量

5. 父类成员变量赋值

6. 父类构造方式开始执行

7. 子类成员变量赋值

8. 子类构造方式开始执行

需要注意的地方是静态变量和静态代码块谁在前面谁先执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值