JVM:类文件结构1

类文件结构1

1、无关性的基石

无关性包括:平台无关性和语言无关性

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码是构成平台无关性的基石

实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,Class文件包含了Java虚拟机指令集和符号表以及若干其他辅助信息。基于安全方面的考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,但任何一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件

在这里插入图片描述

2、Class类文件的结构

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储

Class文件格式采用一种类似于C语言结构体的伪结构存储数据,这种伪结构中只有两种数据类型:无符号数和表

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

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

类型 名称 描述 数量
u4 magic 魔数,表明当前文件是.class文件,固定0xCAFEBABE 1
u2 minor_version Class文件的副版本 1
u2 major_version Class文件的主版本 1
u2 constant_pool_count 常量池计数 1
cp_info constant_pool 常量池内容 constant_pool_count - 1
u2 access_flags 类访问标识 1
u2 this_class 当前类 1
u2 super_class 父类 1
u2 interfaces_count 实现的接口数 1
u2 interfaces 实现接口信息 interfaces_count
u2 fields_count 字段数量 1
field_info fields 包含的字段信息 fields_count
u2 methods_count 方法数量 1
method_info methods 包含的方法信息 methods_count
u2 attribute_count 属性数量 1
attribute_info attributes 各种属性 attributes_count

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

1)、魔数与Class文件的版本

每个Class文件的头4个字节成为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,值为0xCAFEBABE

紧接着魔术的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号,第7和第8个字节是主版本号。Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件

JDK1.1能支持版本号为45.045.65535的Class文件,无法执行版本号为46.0以上的Class文件,而JDK1.2则能支持45.046.65535的Class文件。JDK1.8可生成的Class文件主版本号最大值为52.0

测试程序(本章后面的内容都将以这段程序使用JDK1.8编译输出的Class文件为基础来讲解):

package com.hand.jvm04;

public class TestClass {
	private int m;

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

在这里插入图片描述

0xCAFEBABE:魔数

0x0000:次版本号

0x0034:主版本号,十进制的52,说明这个文件是可以被JDK1.8或以上版本虚拟机执行的Class文件

2)、常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数器。这个容量计数是从1而不是0开始的

在这里插入图片描述

图中,常量池容量为十六进制数0x0013,即十进制的19,这就代表常量池中有18项常量,索引值范围为1~18

在Class文件格式规范制定时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目的含义,这种情况就可以把索引值置为0来表示

Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的

常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有连接这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中

常量池中每一项常量都是一个表,在JDK1.7之前共有11中结构各不相同的表结构数据,在JDK1.7中为了更好地支持动态语言调用,又额外增加了3种(CONSTANT_MothodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info)

类型 标志 描述
CONSTANT_utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 标志方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

在这里插入图片描述

常量池中的第一项常量,它的标志位是0x0A,查表发现这个常量属于CONSTANT_Methodref_info类型

在这里插入图片描述

tag是标志位,用于区分常量类型,第一个index指向声明方法的类描述符CONSTANT_Class_info的索引项,这里index的值为0x0004,即指向了常量池中的第4项常量,第二个index指向名称及类型描述符CONSTANT_NameAndType_info的索引项,这里index的值为0x000F,即指向了常量池中的第15项常量

常量池中的第二项常量,它的标志位是0x09,查表发现这个常量属于CONSTANT_Fieldref_info类型

在这里插入图片描述

tag是标志位,用于区分常量类型,第一个index指向声明字段的类或接口描述符CONSTANT_Class_info的索引项,这里index的值为0x0003,即指向了常量池中的第3项常量,第二个index指向字段描述符CONSTANT_NameAndType_info的索引项,这里index的值为0x0010,即指向了常量池中的第16项常量

补充:

由于Class文件中方法、字段等都需要引用CONSTANT_utf8_info型常量来描述名称,所以CONSTANT_utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大长度,即u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译

使用javap分析Class文件字节码,使用-verbose参数输出TestClass.class文件字节码内容

Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // com/hand/jvm04/TestClass.m:I
   #3 = Class              #17            // com/hand/jvm04/TestClass
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // m:I
  #17 = Utf8               com/hand/jvm04/TestClass
  #18 = Utf8               java/lang/Object

常量池中的14种常量项的结构总表:

在这里插入图片描述

3)、访问标志

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义abstract类型;如果是类的话,是否被声明为final等

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为Public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可以设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义.
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举

access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0

在这里插入图片描述

TestClass是一个被public关键字修饰的普通Java类,因此它的ACC_PUBLIC、ACC_SUPER标志为真,access_flags标志为0x0021

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

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所以的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_utf8_info类型的常量中的全限定名字符串

对于接口索引集合,入口的第一项——u2类型的数据为接口计数器表示索引表的容量。如果该类没有实现任何接口,则该技术其值为0,后面接口的索引表不再占用任何字节

在这里插入图片描述

0x0003:类索引为3

0x0004:父类索引为4

0x0000:接口索引集合大小为0

Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // com/hand/jvm04/TestClass.m:I
   #3 = Class              #17            // com/hand/jvm04/TestClass
   #4 = Class              #18            // java/lang/Object

5)、字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
类型 名称 数量
u2 attributes_count 1
attribute_info attributes attributes_count

字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否为public
ACC_PRIVATE 0x0002 字段是否为private
ACC_PROTECTED 0x0004 字段是否为protected
ACC_STATIC 0x0008 字段是否为static
ACC_FINAL 0x0010 字段是否为final
ACC_VOLATILE 0x0040 字段是否为volatile
ACC_TRANSTENT 0x0080 字段是否为transient
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ENUM 0x4000 字段是否为enum

由于Java本身的语言规则,在实际情况下有如下规则:

  • ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志中只能选其一
  • ACC_FINAL、ACC_VOLATILE不能同时选择
  • 接口之中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志

跟随access_flags标志的是两项索引值,name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符

补充:

“简单名称”、“描述符”、“全限定名”解释

A.全限定名:"com/hand/jvm04/TestClass"是这个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了是连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束

B.简单名称:指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别为“inc”和“m”

C.作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述规则,基本数据类型(byte、char、float、double、short、int、long、boolean)以及代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象的全限定名来表示

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,如Ljava/lang/Object

对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组"int[]"将被记录为“[I”

用描述符来描述方法时,按照先参列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为"()Ljava/lang/String;",方法int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”

在这里插入图片描述

字段表集合第一个u2类型的数据为容量计数器fields_count,其值为0x0001,说明这个类只有一个字段表数据。接下来紧跟着容量计数器的是access_flags标志,值为0x0002,代表private修饰符的ACC_PRIVATE标志位为真,其他修饰符为假。代表字段名称的name_index的值为0x0005,第5项常量是一个CONSTANT_Utf8_info类型的字符串,其值为“m”,代表字段描述符的descriptor_index的值为0x0006,指向常量池的字符串“I”,根据这些信息,可以推断出源码定义的字段为“private int m;”

在descriptor_index之后跟随者一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。对于本例中的字段m,它的属性表计数器为0,也就是没有需要额外描述的信息,但是,如果字段m的声明改为“final static int m=123;”,那就可能会存在一项名称为ConstantValue的属性,其值指向常量123

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的

类文件结构2:https://blog.csdn.net/qq_40378034/article/details/86664661

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/86664632