JVM之类文件结构

我们都知道Java源文件,通过编译器能够生产相应的.class文件,也就是字节码文件,而字节码文件又通过Java虚拟机中的解释器,编译成特定机器上的机器码 ,也就是:

  • 1)Java源文件—>编译器—>字节码文件
  • 2)字节码文件—>JVM—>机器码

一个java文件从编译到执行的整个生命过程如下图所示:

在这里插入图片描述

作为JVM运行的关键,.class文件扮演着一个什么样的角色,它到底长什么样?又发挥着什么作用?

1.无关性的基石:class文件


Java语言的跨平台性,由可以运行在不同操作系统平台上的虚拟机实现,虚拟机可以载入和执行同一种平台无关的字节码,从而实现一次编写,到处运行。

由于虚拟机的建立,大多程序语言选择与操作系统和机器指令无关的、平台中立的格式作为程序编译后的存储格式。不同平台的虚拟机都统一使用的程序存储——字节码(ByteCode)是构成平台无关性的基石

但是,JVM还有另外一种中立的特性:语言无关性。Java发展之初,设计者就曾考虑过并实现了让其他语言运行在Java虚拟机之上的可能性,也可以把Java虚拟机规范拆分为:

  • Java语言规范
  • Java虚拟机规范

到现在,也发展出一大批在JVM上运行的语言:如Clojure、Groovy、JRuby、Jpython、Scala。实现语言无关性的基础仍然是 [虚拟机]和[字节码存储格式]

  • 1)虚拟机并不会关心Class文件的来源是什么语言,任何语言的编译器,只要把程序代码编译成符合Class文件应有的结构,就可以在JVM上运行

  • 2)Java语言中各种变量、关键字、运算符的语义,最终都是由多条字节码命令组合而成,因此字节码命令所能提供的语义描述能力肯定比Java语言本身强大

  • 3)一些Java语言本身无法有效支持的语言特性,并不代表字节码本身无法有效支持

  • 4)这些为其他语言实现一些有别于java语言的特性提供了基础。

所以,就很有必要搞清楚Class字节码文件的结构与作用机制

2.Class类文件的结构


Class文件描述

1.Class文件是:

  • 1)一组8位字节为基础单位二进制流
  • 2)各数据项严格按照顺序紧凑排列,中间没有任何分割符
  • 3)当遇到需要占用8位以上空间的数据项时,按照高位在前的方式分割成若干个8位字节进行存储。

2.Class文件中的数据类型包括:

  • 1)无符号数:

    • 属于基本数据类型:以u1、u2、u4、u8来分别代表1、2、4、8个字节;
    • 可以用来描述数字、索引引用、数量值、按照UTF-8编码成的字符串值
  • 2)表

    • 由多个无符号数或其他表作为数据项,构成的复合数据类型;
    • 所有表习惯性以“_info”结尾;
    • 表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表;

集合的概念:

  • 无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常使用的形式是:一个前置的容量计数器,以及若干个连续的数据项——称这一系列连续的某一类型的数据为:某一类型的集合

3.Class文件的数据项构成:

在这里插入图片描述

无论是顺序还是数量,这些数据项都被严格限定,哪个字节代表什么含有,长度,先后顺序,都不允许改变!

以下是这些数据项的详细介绍。

1.Magic Number 和 文件版本

名称类型数量描述
magicu4[代表4个字节]1Magic Number
minor_versionu21次版本号
majornor_versionu21主版本号

1.Magic Number 魔数

Class字节文件的头4个字节: Magic Number。

唯一作用:确定这个文件是否是一个能被虚拟机接收的Class文件

为啥使用魔数识别文件格式,而不是扩展名来识别?

  • 处于对安全的考虑,因为文件扩展名可以很随意的改动

4个字节的魔数长啥样?

  • 使用十六进制表示:0xCAFEBABE(咖啡宝贝)

2.Class文件版本

紧接着的4个字节是:

  • minor_version:次版本号 第5、6个字节

  • majornor_version:主版本号 第7、8个字节

版本号特性:

  • 高版本的JDK能向下兼容以前版本的Class文件,但不能向上兼容。

2.常量池

名称类型数量描述
constant_pool_countu21常量池容量计数值
constant_poolcp_info[表]constant_pool_count-1常量池表

1.紧接着主次版本号之后,是常量池入口

  • 第9个字节;
  • 偏移地址:0x00000008;

常量池是一个Class文件结构中与其他项目关联最多的,并且是第一个出现的表类型数据项目

2.常量池的表达形式:

1)常量池容量计数值

  • 由于常量的数量不固定,所以要在常量池入口放置一项u2类型的数据——constant_pool_count,用来计数;

2)常量池容量特性:

  • 容量计数是从1开始,而不是0;[只有常量池的容量计数从1开始,对于其他集合类型,都是从0开始]
  • 常量池的容量如果是0x0016,即十进制22:代表常量池中有21项常量,索引为1~21[不是从0索引开始]

为啥将第0项常量空出?

  • 为了满足后面某些指向常量池的索引值的数据,在特定情况下需要表达“不引用任何一个常量池项目”的意思
  • 这种情况下就可以把索引值置为0来表示。

3)常量池中存储的常量分类:

  • 字面量 Literal

    • 比较接近Java语言层面的常量概念
    • 如:文本字符串、被声明为final的常量值等
  • 符号引用 Symbolic References:属于编译原理方法的概念,包括以下三类常量

    • ①类和接口的全限定名(Fully Qualified Name)
    • ②字段的名称和描述符(Desciptor)
    • ③方法的名称和描述符

4)常量池中的常量[11种结构]特性:

  • 每一项常量都是一个表,共有11种结构各不相同的表结构数据;
  • 共同点:表开始第一位是一个 u1 类型的标志位:tag
    • 取值1至12,缺少标志为2的数据类型;
    • 代表当前这个常量属于哪种常量类型;

常量池中的11种具体常量表,长啥样?

在这里插入图片描述

各常量表又有自己的对应的常量结构。

字节码存储案例:

常量池容量计数值 + 常量池1号位常量[tag + 其他数据元素] + 常量池2号位常量[tag + 其他数据元素] + …… + 常量池最后一位常量[tag + 其他数据元素]

  • tag:用于标记这个常量表属于什么类型的常量

  • 其他数据元素:根据该常量表对应的常量结构,可以是有特定意义的[无符号数]如长度标记,也可以是新的常量表 或 其地址标记。

☆为什么要有常量池,常量池的作用是什么?

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步操作,而是在虚拟机加载Class文件的时候进行动态连接

也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经转换的话是无法直接被虚拟机使用的。

也就是说——结论:

  • 当虚拟机运行时,需要①从常量池获得对应的符号引用[见后文],再在类创建时或运行时②解析并翻译到具体的内存地址之中 —— 可以满足动态连接的需求
  • 此外,Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达—— 便于字节码的存储

问题:涉及类的创建与动态连接内容

实例:Class文件字节码分析工具-javap

javap 在 JDK 的bin目录中

案例:输出TestClass.class文件的字节码内容

javap -verbose TestClass
package com.test.clazz

public class TestClass {

	private int m;
	
	public int inc() {
		return m + 1;
	}
}

j
在这里插入图片描述

3.访问标志

名称类型数量描述
access_flagsu21访问标志

常量池结束后,紧接着 2 个字节的访问标志 --> access_flags.

1)作用:

用于识别一些类或接口层次的访问信息[也就是该类的访问信息],包括:

  • 这个Class是 类还是接口;
  • 是否定义为 public类型;
  • 是否定义为 abstract类型;
  • 如果是类的话,是否被声明为final
  • ……

在这里插入图片描述

2)标记方法

access_flags:4个字节,一共有32个标志位可以使用

  • 对应标记为 1 代表该标志为真,0位假
  • 当前只定义了其中 8 个,没有使用到的标志位一律为 0

4.类索引、父类索引 与 接口索引集合

名称类型数量描述
this_classu21类索引
super_classu21父类索引
interfaces_countu21接口索引集合-接口计数器
interfacesu2interfaces_count接口索引集合-接口索引表

1)作用:

Class文件中由这三项数据来确定这个类的继承关系。

a.类索引:用于确定这个类的的全限定名;

b.父类索引:用于确定这个类的父类的全限定名

  • Java不允许多继承,父索引只能有一个;
  • 除了java.lang.Object 类,其没有父类,父索引为0,其余类的父索引均不为0;

c.接口索引集合:一组u2类型的数据集合

  • 用于描述这个类实现了哪些接口;
  • 接口排列顺序:按照 implements 语句后的接口顺序,如果是接口 则是extends后接口的顺序

2)how

三者都按顺序排列在 访问标志[access_flags] 之后.

a.类索引 和 父类索引

  • ①用两个u2类型的索引值 --> 各自指向一个类型为 CONSTANT_Class_info 的类描述常量

  • ②再通过 CONSTANT_Class_info 类型的常量中的索引值,找到定义在 CONSTANT_Utf8_info 中的全限定名字符串;

b.接口索引集合:

  • ①interfaces_count 接口计数器:入口第一项 u2类型的数据 --> 表示索引表容量
    • 如果没有实现的接口,计数器值为0,后面的接口索引表不占用任何字节
  • ②interfaces 接口表索引:n个u2类型的接口索引,n为接口计数器值
    • 索引指向 CONSTANT_Class_info
    • CONSTANT_Class_info 中的 index 常量指向定义在 CONSTANT_Utf8_info 中的全限定名字符串;

CONSTANT_Class_info:

  • tag:u1 --> 常量池数据类型标志
  • index:u2 --> 指向全限定名常量项的索引

描述:

this_class + super_class + interfaces_count + interfaces[0] + …… + interface[n-1]

5.字段表集合

字段表用于描述接口或类中声明的变量。

名称类型数量描述
fields_countu21字段表集合-字段表计数器
fieldsfield_info[表]fields_count字段表集合-字段表

1)fields_count 字段计数器

用于表示字段表的容量;

2)field_info 字段表

字段表用于描述接口或类中声明的变量;

  • 字段(field)包括:类级变量 或 实例级变量,但不包括在方法内部声明的变量。

Java要描述一个字段,需要包含什么信息?

  • 字段的权限[作用域]:public、private、protected 修饰符
  • 类级变量还是实例级变量:static 修饰符
  • 可变性:final
  • 并发可见性:volatile 修饰符、是否强制从主内存读写 [volatile /'vɒlətaɪl/ 不稳定的]
  • 可否序列化:transient 修饰符
  • 字段数据类型:基本数据类型、对象、数组
  • 字段名称

其中,修饰符信息都是布尔值,可以使用标志位来表示;

而字段名称、字段数据类型,这些无法固定的信息,只能使用常量池中的常量来描述。

3)field_info 字段表的最终格式:

在这里插入图片描述

a.字段修饰符 [access_flags:u2]

  • 字段访问标志 : 一些字段的修饰符信息;

b.字段简单名称 [name_index:u2]

c.字段和方法的描述符 [descriptor_index:u2]

  • 这里主要是指字段的描述符;

d.属性表集合 [attributes_count:u2 + attributes:attribute_info]

  • 用于存储一些额外信息

全限定名、简单名称、描述符 这三种特殊字符串的概念:

  • 全限定名:把全类名中的“.” 替换成 “/”
    • 如 java.lang.Object --> java/lang/Object
    • 为了使连续的多个限定名之间不产生混淆,在使用时最后一般会加入一个 “;”,表示全限定名结束
  • 简单名称:指没有类型和参数修饰的方法或字段名称
    • 如 getName() 方法 和 name 字段 : 简单名称分布为 getName 和 name
  • 字段和方法的描述符:描述符:是用来描述
    • 字段的数据类型方法的参数列表(包括数量、类型、顺序)和返回值

描述符:

  • 基本数据类型 及 无返回值void 类型:使用一个大写字符来表示
  • 对象类型:使用字符 L 加 对象的全限定名来表示
  • 数组类型:维度使用 “[” 表示(二维“[[”)、数据类型同上

案例1:字段数据类型

  • 定义一个 “java.lang.String[][]”类型二维数组:将被记录为 “[[Ljava/lang/String;”
  • “int[]” : “[I”

用描述符描述方法时:先参数列表,后返回值的顺序描述

  • 参数列表按照严格顺序,放在一组 “()” 小括号内

案例2:方法描述符

  • void inc() : ()V
  • java.lang.String toString() : ()Ljava/lang/String;
  • int indexOf(char[] source,int i,char[] target) : ([CI[[C)I

4)how:如何描述

field_count(n) + field_info[0] + field_info[1] + …… + field_info[n-1]

5)注意:

字段表集合中不会列出从超类或符接口中继承而来的字段;

但可能列出原本Java代码中不存在的字段,如内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

另外,Java语言中字段是无法重载的,不管字段的数据类型、修饰符是否相同,都必须使用不同名称;

但,对于字节码而言,如果两个字段的描述符[数据类型的描述]不一致,那字段重名就是合法的。

6.方法表

类似于字段表,Class文件存储格式中对方法的描述,与对字段的描述几乎采用完全一致的方式。仅在访问标志和属性表集合的可选项中有所区别。

名称类型数量描述
methods_countu21方法表集合-方法表计数器
methodsmethod_info[表]methods_count方法表集合-方法表

1)method_info 方法表的结构:

在这里插入图片描述

问题:方法的定义可以通过访问标志[public final static abstract …]、名称索引[getAge]、描述符索引[int (int i,int j)]表达清楚,但方法里的代码去哪里了?

方法里的Java代码,经过编译器编译成字节码文件之后,存放在方法属性表集合中一个名为**“Code”的属性**里面品[见下一节]。

2)描述

methods_count + method_info[0] + … + method_info[n-1]

3)注意:

a.如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息;

但有可能出现由编译器自动添加的方法,最典型的就是类构造器“”方法 和 实例构造器“”方法;

b.在Java语言中,要重载(Overload)一个方法,除了要与原有方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。

  • 特征签名:是一个方法中各个参数在常量池中的字段符号引用集合,也就是因为返回值不会包含在特征签名中;因此Java语言中是无法仅仅依靠返回值的不同来对一个已有方法进行重载

但在Class文件格式中,特征签名范围更大一些:只要描述符不是完全一致的方法也可以共存;也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中。

7.属性表集合

在Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景专有的信息

名称类型数量描述
attributes_countu21属性表集合-属性表计数器
attributesattribute_info[表]attributes_count属性表集合-属性表

在这里插入图片描述

1)属性表的结构

  • 对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示;

  • 而属性值的结构,则是完全自定义的,只需要说明属性值所占用的位数长度即可。

一个符合规则的属性表,应该满足下表所定义结构:

在这里插入图片描述

2)描述:

attributes_count属性表个数 + attributes[0]如Code属性表 + …

3.小结

Class文件是虚拟机执行引擎的数据入口,也是Java技术体系的基础支柱之一。了解Class文件的结构,对后面进一步了解虚拟机执行引擎有很重要的意义。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值