JVM(二)

本文详细介绍了Java Class文件的结构,包括魔数、常量池、方法表集合等内容。解释了类加载过程中验证、准备、解析及初始化的具体操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

class文件结构

JVM的语言无关性的基础就是虚拟机和字节码存储格式,只有能把程序代码编译成class文件,虚拟机不关心class的来源是什么语言。
字节码指令提供的语义描述能力比java本身更强大,因此,java无法支持的语言特性并不代表字节码指令不能支持。

魔数

class文件的前四个字节称为魔数,用来标志该文件能否被虚拟机接受的class文件。这个魔数值为0xCAFEBABE。
高版本的JDK能向前兼容以前版本的class文件,但不能运行后面版本的class文件,即使文件格式并未发生变化。

常量池

常量池中常量的数量不是固定的,所以常量池前面有一项常量池容器计数值,计数从1开始,因此常量的数量=计数器-1。空出来的0索引在后面用来表示“不引用任何一个常量”。class文件中的其他表,计数从0开始。
常量池主要存放两大类常量:字面量和符号引用。字面量包括字符串常量、final的常量等;符号引用包括类或接口名、字段名和描述符、方法名和描述符。

方法表集合

方法中的java代码经过编译,存放在方法属性表集合的code属性里。
和字段表集合一样,如果父类方法在子类中没有被重写,方法表集合中就不会出现父类的方法信息。但是,方法表集合中可能会出现由编译器自动添加的方法,比如类构造器<clinit>方法和实例构造器<init>方法。
java中,重载一个方法,除了要与原方法具有相同的名称,还要有一个与原方法不同的特征签名(参数字段符号引用的集合),但是特征签名不包括返回值,无法只依据返回值的不同对一个方法进行重载。

编译

栈帧中的操作数栈和局部变量表都是以一个字长为单位的数组,不同的是操作数栈的访问通过入栈和出栈,局部变量表通过索引访问。
java编译时,并没有常规的”链接”步骤,而是在虚拟机加载class文件时动态链接:虚拟机运行时,从方法区的常量池中获得对应的符号引用,然后转换成具体的内存地址。

类加载

类从加载到卸载包括加载、验证、准备、解析、初始化、使用、卸载。这些阶段通常是互相交叉地混合式进行,在一个阶段执行的过程中调用或激活另外一个阶段。其中验证、准备、解析三部分称为链接。
当对一个类进行主动引用时,虚拟机立即对类进行初始化,其他的被动引用不会触发初始化。

对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。

加载

1.通过类的全限定名来获取定义此类的二进制字节流。可以从jar包中、applet、动态代理、数据库等中获取二进制字节流。
2.将字节流中的静态存储结构转化为方法区的运行时数据结构。方法区中的数据存储格式由虚拟机自行定义。
3.在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区的相关数据的访问入口。
加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器完成。

比较两个类是否“相等”,只有在这两个类由同一个类加载器加载的前提下才有意义。即使这两个类都来自同一个class文件,只要它们的类加载器不同,那么这两个类必定不相等。

验证

验证的目的是确保class文件的字节流符合当前虚拟机的要求,若不符合抛出java.lang.VerifyError异常。
通常验证的过程包括:文件格式验证、元数据验证、字节码验证和符号引用验证。
当然这些验证并不能一定保证class文件的正常执行,因为”Halting Problem”:通过程序去验证程序逻辑无法做到绝对准确。

准备

为方法区中类变量(被static修饰)分配内存并设置类变量初始值(这里的初始值通常是数据类型的零值),不包括实例变量,实例变量将会在对象实例化时随着对象在java堆中分配。

public static int value=123;

为类变量赋予用户定义的初始值的字节码 指令存放在类构造器<clinit>()方法中。这个方法将在初始化阶段才会被执行。
但是static变量带有final限定符,编译器会在类字段的字段属性表中添加ConstantValue属性,在准备阶段将变量初始化为ConstantValue属性所指定的值。

public static final int value=123;

解析

解析阶段,虚拟机将常量池内的符号引用替换为直接引用。
符号引用使用一组符号来描述引用的目标,引用的目标不一定已经加载到内存中。
直接引用可以是直接指向目标的指针,相对偏移量或一个能间接定位到目标的句柄。直接引用与虚拟机的实现有关。直接引用所引用的目标必定已经存在内存中。

对于字段解析,首先将字段所属的类或接口的符号引用解析,在解析后的类或接口中,首先查找是否包含这样的字段,若找到返回该字段的直接引用;否则,如果类实现了接口,按照继承关系从上往下递归搜索各个接口和它的父接口,如果接口中包含相应的字段,返回直接引用;否则,按照继承关系从上往下递归搜索其父类,如果父类中包含相应的字段,返回直接引用。如果还没有找到,则查找失败,抛出java.lang.NoSuchFieldError异常。

对于类或接口的解析,虚拟机将未解析的符号引用传给类加载器,类加载器加载符号引用对应的类或接口。无论是类、接口还是基本数据类型,虚拟机会在java堆中生成相应的对象。此外,在解析完成之前还要进行符号引用验证,确认当前对象是否有访问权限,如果不具备访问权限,抛出java.lang.IllegalAccessError。

初始化

前面的类加载过程,除了可以自定义类加载器,其他完全由虚拟机控制。初始化阶段,才真正开始执行类中代码。初始化阶段是执行类构造器<clinit>()方法的过程。
类构造器由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中语句合并产生的。编译器收集的顺序由在源文件中出现的顺序决定。静态语句块可以访问、赋值定义在静态语句块之前的变量,定义在它之后的变量,可以在静态语句块中赋值,但不能访问。
类构造器与类的构造函数(实例构造器<init>)不同,它不需要显式调用父类构造器。虚拟机保证父类构造器先于子类构造器执行完毕。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值