Class对象在执行引擎中的初始化过程

Class对象在执行引擎中的初始化过程

一个class文件被加载到内存中需要经过三大步:装载,链接,初始化。其中链接又可以细分为:验证,准备,解析三小步。用一张图来描述class文件加载到内存的步骤如下:

在这里插入图片描述

1. 装载

什么是装载

装载是指Java虚拟机查找.class文件并生成字节流,然后根据字节流创建java.lang.Class对象的过程。

这一过程主要完成以下三件事:

  1. ClassLoader通过一个类的全限定名(包名+类名)来查找.class文件,并生成二进制字节流:其中class字节码文件的来源不一定是.class文件,也可以是jar包,zip包,甚至是来源于网络的字节流。
  2. 把.class文件的各个部分分别解析(parse)为JVM内部特定的数据结构,并存储在方法区。在这里JVM会将这些.class文件的结构转化为JVM内部的运行时数据结构。
  3. 在内存中创建一个java.lang.Class类型的对象:接下来程序在运行过程中所有对该类的访问都通过这个类对象,也就是这个Class类型的类对象是提供给外界访问该类的接口。

加载时机

一个项目经过编译之后,往往会生成大量的.class文件。当程序运行时,JVM并不会一次性的将这些.class文件全部加载进内存。对此,Java虚拟机并没有严格规定何时加载.class文件,不同的虚拟机有不同的实现。不过以下两种情况一般会对class进行装载操作。

  • 隐式装载:在程序运行过程中,当碰到通过new等方式生成对象时,系统会隐式调用ClassLoader去装载对应的class到内存。
  • 显示装载:在编写源代码时,主动调用Class.forName()等方法也会进行class装载操作,这种方式通常称为显示装载。

2.链接

链接过程分为3步:验证,准备,解析

验证

验证是链接的第一步,目的是为了确保.class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危及虚拟机本身的安全,主要包含以下几个方面的检验:

  1. 文件格式检验:检验字节流是否符合class文件格式的规范,并且能被当前版本的虚拟机处理。
  2. 元数据检验:对字节码描述的信息进行语义分析,以保证其描述的内容符合Java语言规范的要求。
  3. 字节码检验:通过数据流和控制流分析,确定程序语义是合法,符合逻辑的。、
  4. 符号引用检验:符号引用检验可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

实例分析

我们使用以下Foo.java来演示验证阶段的几种情况:

在这里插入图片描述

使用javac编译Foo.java生成Foo.class字节码文件,然后使用16进制编辑器打开Foo.class文件,部分如下:

在这里插入图片描述

正常情况下,使用java Foo执行效果如下:

在这里插入图片描述

如果使用16进制编辑器修改class文件中的魔数,如下所示:

在这里插入图片描述

将“cafe babe”修改为“cafe babb”,重新运行则会报如下错误:

在这里插入图片描述

class文件中魔数后的“0034”为版本号,如果将其修改为“0035”则会报如下错误:
在这里插入图片描述

版本号“0034”之后的“0036”是常量池计数器,表示常量池中有54个常量。如果将这个值进行修改也有可能造成运行时错误,比如将“0036”改为“0032”

在这里插入图片描述

重新执行java Foo,则会报如下错误:
在这里插入图片描述

虽然说JVM会检查各种对class字节码文件的篡改行为,但是依然无法百分之百保证class文件的安全性。比如继续使用Foo.java举例,在Foo.java中的print方法中,分别打印出父类和自身类的hashCode值,分别是:2018699554和111.我们可以在class字节码的基础上进行篡改,将父类的hashCode也返回111.

通过javap -v Foo命令可以查看Foo.class中常量池的具体信息:

在这里插入图片描述

图中1处指向了父类Object的hashCode方法,图中2处指向了Foo的hashCode方法。CONSTANT_Methodref_info的结构如下:

CONSTANT_Methodref_info{
    u1 tag = 10;
    u2 class_index;		//指向此方法的所属类
    u2 name_type_index;	//指向此方法的名称和类型
}

其中class_index就是指向方法的所属类,因此只需要使用16进制编辑器将指向Object的class_index改为执行Foo的class_index即可。具体修改如下:

在这里插入图片描述

按照图中修改并保存,重新运行java Foo效果如下:

在这里插入图片描述

可以看出,虽然在Java源文件中调用的是super.hashCode()方法,但是经过篡改之后,Foo.class文件成功通过JVM的校验,并成功执行最终打印出我们想要的结果。

准备

准备是链接的第二步,这一阶段主要目的是为类中的静态变量分配内存,并为其设置“0值”。比如:

public static int value = 100;

在准备阶段,JVM会为value分配内存,并将其设置为0。而真正的值100是在初始化阶段设置。并且此阶段进行内存分配的仅包括类变量,而不包括实例变量(实例变量将会在对象实例化时随着对象一起分配在Java堆中)。

有一种情况比较特殊——静态常量,比如:

public static final int value = 100;

以上代码会在准备阶段就为value分配内存,并设置为100.

Java中基本类型的默认“0值”如下:

  • 基本类型(int,long,short,char,byte,boolean,float,double)的默认值为0;
  • 引用类型默认值为null

解析

解析时链接的最后一步,这一阶段的任务是把常量池中的符号引用转换为直接引用,也就是具体的内存地址。在这一阶段,JVM会将常量池中的类,接口名,字段名,方法名等转换为具体的内存地址。

比如上面Foo.java中编译之后的main方法的字节码如下:

在这里插入图片描述

在main方法中通过invokevirtual指令调用了print方法,“Foo.print:()V"就是一个符号引用,当main方法执行到此处时,会将符号引用”Foo.print:()V“解析成直接引用,可以将直接引用理解为方法的真正内存地址。

3.初始化

这是class加载的最后一步,这一阶段是执行类构造器方法的过程,并真正初始化类变量。比如:

public static int value = 100;

在准备阶段value被分配内存并设置为0,在初始化阶段value的值会被设置为100.

初始化时机

对于装载阶段,JVM并没有规范何时具体执行。但是对于初始化,JVM规范中严格规定了class初始化的时机,主要有以下几种情况会触发class的初始化:

  1. 虚拟机启动时,初始化包含main方法的主类
  2. 遇到new指令创建对象实例时,如果目标对象类没有被初始化则进行初始化操作
  3. 当遇到访问静态方法或者静态字段的指令时,如果目标对象类没有被初始化则进行初始化操作
  4. 子类的初始化过程如果发现父类还没有进行初始化,则需要先触发父类的初始化
  5. 使用反射API进行反射调用时,如果类没有被初始化则进行初始化操作
  6. 第一次调用java.lang.invoke.MethodHandle实例时,需要初始化MethodHandle指向方法所在的类

初始化类变量

在初始化阶段只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的语句块在实例化对象的时候才会执行。

比如执行以下代码:

在这里插入图片描述

然后在ClassInitTest.java中访问ClassInit的value值,如下:

在这里插入图片描述

执行上述代码,日志如下:

在这里插入图片描述

可以看出,非静态代码块并没有被执行。如果将ClassInitTest.java修改如下:

在这里插入图片描述

加了一行代码,使用new创建ClassInit对象实例。再次执行后非静态代码块也将会被执行,如下:
在这里插入图片描述

被动引用

上述的六种情况在JVM中被称为主动引用,除此六种情况之外所有引用类的方式都被称为被动引用。被动引用并不会触发class的初始化。

最典型的就是在子类中调用父类的静态变量,比如有以下两个类:
在这里插入图片描述

可以看出Child继承自Parent类,如果直接使用Child来访问Parent中的value值,则不会初始化Child类,比如如下的代码:

在这里插入图片描述

打印日志如下:

在这里插入图片描述

可以看出,Child中的静态代码并没有被执行。也就是说JVM并没有对Child执行初始化操作。

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过子类Child来引用Parent中定义的静态字段,只会触发父类Parent的初始化,而不会触发Child的初始化。至于是否要触发子类的加载和验证,在虚拟机中并未明确规定,可以通过XX:+TraceClassLoading参数来查看,比如使用如下命令再次执行NonInitTest

java -XX:+TraceClassLoading NonInitTest
在这里插入图片描述

可以看出虽然只有Parent被初始化,但是Parent和Child都经过了装载和验证阶段,并被加载到内存中。

猜你喜欢

转载自blog.csdn.net/qq_43621019/article/details/105876052
今日推荐