【深入理解 Java 虚拟机笔记】类文件结构

xiaoxiao2025-06-11  38

5.类文件结构

由于最近十年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

无关性的基石

Java 刚诞生的宣传口号:一次编写,到处运行(Write Once, Run Anywhere)。其最终实现在操作系统的应用层:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码。

字节码(ByteCode)是构成平台无关的基石。虚拟机的语言无关性也越来越被开发者所重视,JVM 设计者在最初就考虑过实现让其他语言运行在Java虚拟机之上的可能性,如今已发展出一大批在 JVM 上运行的语言,比如 Clojure、Groovy、JRuby、Jython、Scala。

实现语言无关性的基础仍是虚拟机和字节码存储格式,Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与 Class 文件这种特定的二进制文件格式所关联,这使得任何语言的都可以使用特定的编译器将其源码编译成 Class 文件,从而在虚拟机上运行。

Class 类文件的结构

Class 文件是一组以 8 个字节为基础单位的二进制流(可能是磁盘文件,也可能是类加载器直接生成的),各个数据项目严格按照顺序紧凑地排列,中间没有任何分隔符。

Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,其中只有两种数据类型:

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

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

Class 文件格式:

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

写一个简单的程序来分析 Class 文件结构:

package com.chen; public class TestClass { private int m; public int inc() { return m + 1; } }

通过 WinHex 打开 Class 文件:

魔数与 Class 文件的版本

Class文件的头 4 个字节被称为魔数 (Magic Number),唯一作用是确定文件是否为一个可被虚拟机接受的 Class 文件,固定为“0xCAFEBABE”。

第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version),Java 版本号是从 45 开始的,JDK 1.1 之后每个大版本发布,主版本向上加1(JDK 1.0~1.1使用了 45.0~45.3)。Java 能向下兼容之前的版本,无法运行后续的版本。我们举的例子中,0x0034 即 52,对应 JDK 1.8。

常量池

紧接着就是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,是 Class 文件结构中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项之一。

由于常量池中的常量数量不固定,因此需要在常量池前放置一项 u2 类型的数据,来表示常量池容量计数值(constant_pool_count)。该值是从 1 开始的,例子的 0x0016 为十进制的 22,代表常量池中有 21 项常量,索引值范围为1~21。设计者将第 0 项常量空出来,目的是满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。

常量池主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References):

字面量比较接近 Java 语言层次中的常量概念,如文本字符串,声明为 final 的常量值等符号引用则属于编译原理方面的概念,包括三类常量: 类和接口的全限定名(Fully Qualified Name)字段的名称和描述符(Descriptor)方法的名称和描述符

Java 代码在 javac 编译时不会有“连接”这一步骤,而是在虚拟机加载 Class 文件的时候进行动态连接。所以 Class 文件不会保存各个方法、字段和最终内存布局信息,必须经过运行期转换,才能得到真正内存入口地址。当虚拟机运行时需要从常量池获取对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。

常量池中每一项常量都是一个表,JDK 1.7 中常量池共有 14 种不同的表结构数据,这些表结构开始的第一位是一个 u1 类型的标志位,代表当前常量的类型,具体如下图所示:

结合下图中各个表结构的说明和之前使用 javap 解析的文件内容:

例子中,第一个常量是 0x0A,属于 CONSTANT_Methodref_info 类型,0x0004,指向第四项 CONSTANT_Class_info 常量,而0x0012 指向第18项 CONSTANT_NameAndType_info 常量。

第二个常量是 0x09,对应的是 CONSTANT_Fieldref_info 类型,0x0003,指向第三项 CONSTANT_Class_info 常量,而0x0013指向19项 CONSTANT_NameAndType_info 常量。

其余的常量都可以通过类似的方法计算出来,计算机可以帮我们完成这一步,通过 javap,使用带 -verbose 参数输出的字节码内容:

从图中可以看出,有一些常量从来没有在代码出现过,如“I”、“V”、“”、“LineNumberTable”、“LocalVariableTable”等,这些自动生成的常量,会在字段表(field_info)、方法表(method_info)、属性表(attribute_info)引用到,用来描述一些不方便使用“固定字节”进行表述的内容。

访问标志

在常量池结束之后,接着就是两个字节的访问标志(acess_flags),用于识别一些类或者接口层次的信息。access_flags 中一共有16个标志位可以使用,当前只定义了 8 个,没有使用的一律为 0。

具体的标志位:

标志名称标志值含义ACC_PUBLIC0x00 01是否为 Public 类型ACC_FINAL0x00 10是否被声明为 final,只有类可以设置ACC_SUPER0x00 20是否允许使用 invokespecial 字节码指令的新语意,invokespecial 在JDK 1.0.2 发生过改变,在 JDK 1.0.2 之后编译出来的类这个标志都必须为真ACC_INTERFACE0x02 00标志这是一个接口ACC_ABSTRACT0x04 00是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其他类型为假ACC_SYNTHETIC0x10 00标志这个类并非由用户代码产生ACC_ANNOTATION0x20 00标志这是一个注解ACC_ENUM0x40 00标志这是一个枚举

我们例子中的 TestClass 是一个普通的 Java 类,不是接口,注解或枚举,被 public 修饰但不是 final 和 abstract,所以它的 ACC_PUBLIC、ACC_SUPER 为真,而 ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM六个标志位都应该为假,所以它的 acess_flags 应该为 0x0001|0x0020=0x0021,如图,偏移地址:000000D9

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

这三个索引用来确定这个类的继承关系:

类索引:u2 类型的数据,用于确定类的全限定名。本例子中为 0x0003,指向常量池中第3项;父类索引:u2 类型的数据,用于确定父类的全限定名。本例子中为 0x0004,指向常量池中第4项;接口索引集合:一组 u2 类型的数据的集合,用于确定实现的接口(对于接口来说就是 extend 的接口)。第一项为接口索引计数器,u2 类型的数据,用于表示索引集合的容量。本例子中为 0x0000,说明没有实现接口。

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量,包括类级变量和实例级变量,但不包括**方法内部声明的局部变量,**它不会列出从父类和超类继承而来的字段,但有可能列出原本 Java 代码中不存在的字段,如在内部类中为了保持对外部类的访问性所添加指向外部类实例的字段。并且在 Java 语言中字段是无法重载的,但对于字节码来说,字段的描述符不同,字段的重名是可行的。

字段表的结构:

类型名称数量u2access_flags1u2name_index1u2descriptor_index1u2attributes_count1attribute_infoattributesattributes_count

其中字段修饰符放在 access_flags 项目中,与类中 access_flags 类似,都是 u2 数据类型,可以设置的标志位和含义:

标志名称标志值含义ACC_PUBLIC0x0001字段是否为publicACC_PRIVATE0x0002字段是否为privateACC_PROTECTED0x0004字段是否为protectedACC_STATIC0x0008字段是否为staticACC_FINAL0x0010字段是否为finalACC_VOLATILE0x0040字段是否为volatileACC_TRANSTENT0x0080字段是否为transientACC_SYNCHETIC0x1000字段是否为由编译器自动产生ACC_ENUM0x4000字段是否为enum

ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED 最多只能三选一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口中字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。

access_flags 之后是 name_index 和 descriptor_index 。它们是对常量池的引用,分别代表着字段的简单名称以及字段方法和方法的描述符。

描述符是用来描述字段的数据类型、方法的参数列表和返回值。描述符规则:

标志符含义B基本数据类型 byteC基本数据类型 charD基本数据类型 doubleF基本数据类型 floatI基本数据类型 intJ基本数据类型 longS基本数据类型 shortZ基本数据类型 booleanV基本数据类型 voidL对象类型,如 Ljava/lang/Object

对于数组类型,每一维度将使用一个前置的“[”字符来描述.如一个定义为"java.lang.Stirng[][]"类型的二维数组,将被记录为:“[[Ljava/lang/Stirng;”,一个整型数组“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,说明这类只有一个字段表数据接下来是 acesss_flags 标志,值为 0x0002,代表修饰符的 ACC_PRIVATE 标志为真接着是修饰字段名称的 name_index ,值为 0x0005,常量池中的第五个常量,也就是 CONSTANT_Utf8_info 类型的字符串,值为 “m”代表字段描述符的 descriptor_index 的值为 0x0006,指向常量池的字符串"I"所以原代码定义的字段为:private int m;

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

方法表集合

和字段表类似,方法表的结构依次包括访问标志(acess_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

volatile 和 transient 不能修饰方法,但 synchronized、native、strictfp 和 abstract 关键字可以修饰方法,所以方法表的标志位:

标志名称标志值含义ACC_PUBLIC0x0001方法是否为publicACC_PRIVATE0x0002方法是否为privateACC_PROTECTED0x0004方法是否为protectedACC_STATIC0x0008方法是否为staticACC_FINAL0x0010方法是否为finalACC_SYHCHRONRIZED0x0020方法是否为synchronizedACC_BRIDGE0x0040方法是否是有编译器产生的方法ACC_VARARGS0x0080方法是否接受参数ACC_NATIVE0x0100方法是否为nativeACC_ABSTRACT0x0400方法是否为abstractACC_STRICTFP0x0800方法是否为strictfpACC_SYNTHETIC0x1000方法是否是有编译器自动产生的

在例子中:

第一个 u2 类型的数据(计数器)为 0x0002,代表集合有两个方法(分别为编译器添加的实例构造器<init>和源码中的 inc())。

第一个方法的访问标志为 0x0001,即只有 ACC_PUBLIC 为真名称索引值为 0x0007,常量池第七项,即 “”描述符索引值为 0x0008,即 “()V”属性表计数器为 0x0001,表示此方法属性表集合有一项属性,属性名称索引为 0x0009,对应常量池第九项 “Code”,说明此属性是方法的字节码描述。

第二个方法,inc()方法:

与字段表集合对应的,如果父类方法没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样会出现由编译器自动添加的方法,最典型的是类构造器 <clinit> 方法和实例构造器 <init> 方法。

Java 语言中无法通过返回值不同来对一个已有方法进行重载的,但在 Class 文件结构中,只要描述符不同的两个方法也可以共存。也就是说,返回值不同,也可以 共存于同一个 Class 文件中。

属性表集合

在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。属性表集合不要求各个属性表的严格顺序,只要不要已有属性名重复,任何人实现的编译器都可以向属性表写入自己定义的属性信息,JVM 运行时会忽略不认识的属性。

虚拟机规范预定义的属性:

属性名称使用位置含义Code方法表Java代码编译成的字节码指令ConstantValue字段表final关键字定义的常量池Deprecated类,方法,字段表被声明为deprecated的方法和字段Exceptions方法表方法抛出的异常EnclosingMethod类文件仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法InnerClass类文件内部类列表LineNumberTableCode属性Java源码的行号与字节码指令的对应关系LocalVariableTableCode属性方法的局部变量描述StackMapTableCode属性JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类是否匹配Signature类,方法表,字段表用于支持泛型情况下的方法签名SourceFile类文件记录源文件名称SourceDebugExtension类文件用于存储额外的调试信息Synthetic类,方法表,字段表标志方法或字段为编译器自动生成的LocalVariableTypeTable类使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加RuntimeVisibleAnnotations类,方法表,字段表为动态注解提供支持RuntimeInvisibleAnnotations表,方法表,字段表用于指明哪些注解是运行时不可见的RuntimeVisibleParameterAnnotation方法表作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法RuntimeInvisibleParameterAnnotation方法表作用与RuntimeInvisibleAnnotations属性类似,作用对象为方法参数AnnotationDefault方法表用于记录注解类元素的默认值BootstrapMethods类文件用于保存invokeddynamic指令引用的引导方式限定符

属性表的结构:

类型名称数量u2attribute_name_index1u2attribute_length1u1infoattribute_length

Code 属性

Java 程序方法体中的代码经过 Javac 编译器处理后,变为字节码指令存储在 Code 属性内。并非所有方法表都存在这个属性,比如接口或抽象类中的方法。

Code 属性表结构:

类型名称数量u2attribute_name_index1u4attribute_length1u2max_stack1u2max_locals1u4code_length1u1codecode_lengthu2exception_table_length1exception_infoexception_tableexception_lengthu2attributes_count1attribute_infoattributesattributes_count

本例中的第一个方法 <init> ,对应字节码:

其中

0x0009 代表属性名称"Code"0x0000002f 是属性值长度,属性值长度固定为整个属性表长度减去 6 个字节。0x0001 代表操作数栈(Operad Stacks)深度的最大值0x0001 代表局部变量表所需的存储空间。单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。方法参数(包括实例方法中的"this")、显式异常处理器的参数(Exception Handler Parameter,try-catch语句中 catch 块中所定义的异常)、方法体中定义的局部变量需要局部变量表来存放。0x00000005 存储 Java 源程序编译后的字节码指令长度。虽然是 4 字节的长度值,理论上最大值可以达到 2^32 -1,但是虚拟机规范限制了一个方法不允许超过 65535 条字节码指令。0x2ab70001b1 是字节码指令,每个指令就是一个 u1 类型的单字节: 2a 对应指令为 aload_0,即将第 0 个 Slot 中为 reference 类型的本地变量推送到操作数栈顶b7 对应指令 invokespecial,以栈顶的 reference 类型的数据所指向对象作为方法接收者,调用此对象的实例构造器方法、private 方法或者父类方法。这个方法有一个 u2 类型的参数说明调用什么方法,它指向常量池中的一个 CONSTANT_Methodref_info 类型常量,即此方法的符号引用。0001,这是 invokespecial 的参数,常量池 0x0001对应的常量为实例构造器<init> 方法的符号引用b1 对应指令是 return,返回此方法,并且返回值为 void 。

通过 javap 得到另一个方法:

其中 args_size = 1 ,因为实例方法,都可以通过 “this” 关键字访问到此方法所属的对象。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个 Slot 位来存放对象实例的引用,方法参数值也会从 1 开始计算,这个处理只对实例方法有效。

在字节码指令之后是这个方法的显示异常处理表集合,异常表对于 Code 属性并不是必须存在的。

异常表格式:

类型名称数量u2start_pc1u2end_pc1u2handler_pc1u2catch_type1

这四个字段,如果当字节码在第 start_pc 行到 end_pc行(不含第 end_pc 行)出现类型为 catch_type 或者其子类的异常,则转到第 handler_pc 行继续处理,当 catch_type 的值为 0 ,代表任意异常情况都需要转到 handler_pc 处来进行处理。

编译器通过异常表来实现 Java 异常以及 finally 处理机制。

Exceptions 属性

Exceptions 属性列举出方法中基本抛出的受查异常(Checked Exception),也就是方法描述时 throws 关键字后面列举的异常,和 Code 属性里的异常表不同。

其属性表结构如下:

类型名称数量u2attribute_name_index1u2attribute_lrngth1u2attribute_of_exception1u2exception_index_tsblenumber_of_exceptions

Exceptions 属性中的 number_of_exceptions 项表示方法可能抛出 number_of_exceptions 种受查异常,每一种受查异常使用一个 exception_index_table 项表示,exception_index_table 是一个指向常量池中 CONSTANT_Class_info 型常量的索引,代表了该受查异常的类型。

LineNumberTable 属性

LineNumberTable 属性用于描述 Java 源代码行号与字节码行号(字节码偏移量)的对应关系。它默认会生成在 Class 文件中,可以在 Javac 中通过 -g:none 或 -g:lines 选项来取消或生成这项信息。如果没有这个属性,运行时抛异常不会显示出错的行号,在代码调试时无法按照源码行来设置断点。

结构:

类型名称数量u2attribute_name_index1u4attribute_length1u2line_number_table_length1line_number_infoline_number_tableline_number_table_length

LocalVariableTable 属性

它是用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它默认会生成在 Class 文件中,可以在 Javac 中通过 -g:none 或 -g:vars 选项来取消或生成这项信息。如果没有这个属性,所有的参数名称都会丢失,取之以 arg0、arg1 这样的占位符来替代。

LocalVariableTable 结构:

类型名称数量u2attribute_name_index1u4attribute_length1u2local_variable_table_length1local_variable_infolocal_variable_tablelocal_variable_table_length

其中 local_variable_info 代表一个栈帧与源码中的局部变量的关联,结构:

类型名称数量说明u2start_pc1局部变量的生命周期开始的字节码偏移量u2length1局部变量作用范围覆盖的长度u2name_index1指向常量池中 CONSTANT_Utf8_info 类型常量的索引,局部变量名称u2descriptor_index1指向常量池中 CONSTANT_Utf8_info 类型常量的索引,局部变量描述符u2index1局部变量在栈帧局部变量表中 Slot 的位置,如果这个变量的数据类型为 64 位类型(long或double),它占用的 Slot 为 index 和 index+1 这 2 个位置

SourceFile 属性

SourceFile 属性用于记录生成这个 Class 文件的源码文件名称。可以使用 Javac 的 -g:none 和 -g:source 来关闭或生成。对于大多数类来说,类名和文件名相同,但有些例外情况(如内部类)例外。如果不生成这项属性,当抛出异常,堆栈将不会显示出错代码所属的文件名。

类型名称数量u2attribute_name_index1u4attribute_length1u2sourcefile_index1

sourcefile_index 数据项指向常量池中 CONSTANT_Utf8_info 型常量的索引,常量值是源码文件的文件名。

ConstantValue 属性

ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值。

只有被 static 关键字修饰的变量才可以用这个属性。对于非 static 类型的变量的赋值是在实例构造器 <init> 方法中进行的。而对于类变量有两种方式:在类构造器方法中或者使用 ConstantValue 属性。

目前 Sun Javac 编译器的选择是:

同时使用 final 和 static 修饰的变量,并且为基本数据类型或 java.lang.String 类型使用 ConstantValue 属性初始化如果没有 final 修饰,或并非基本数据类型,则选择在 <clinit> 方法中初始化

虚拟机规范中并没有强制要求要求 ConstantValue 属性的字段必须设置 ACC_FINAL 标志,只是必须设置 ACC_STATIC 标志而已。对 final 关键字的要求是 Javac 编译器自己加入的限制。

类型名称数量u2attribute_name_index1u4attribute_length1u2constantvalue_index1

ConstantValue 属性是一个定长属性,其 attribute_length 数据项值必须固定为 2。constantvalue_index 代表常量池的一个字面量引用。

InnerClasses 属性

InnerClasses 属性用于记录内部类与宿主类之间的关联。

属性结构:

类型名称数量u2attribute_name_index1u4attribute_length1u2number_of_classes1inner_classes_infoinner_classnumber_of_classes

number_of_classes 记录多少个内部类信息,每一个内部类信息由一个 inner_classes_info 表描述,其结构:

类型名称数量作用u2inner_classes_info_index1指向常量池 CONSTANT_Utf8_info 类型常量的索引,内部类的符号引用u2outer_classes_info_index1指向常量池 CONSTANT_Utf8_info 类型常量的索引,宿主类的符号引用u2inner_name_index1指向常量池 CONSTANT_Utf8_info 类型常量的索引,内部类的名称,如果是匿名内部类,则为 0u2inner_class_access_flags1内部类的访问标志

inner_class_access_flags 标志:

标志名称标志值含义ACC_PUBLIC0x0001内部类是否为publicACC_PRIVATE0x0002内部类是否为privateACC_PROTECTED0x0004内部类是否为protectedACC_STATIC0x0008内部类是否为staticACC_FINAL0x0010内部类是否为finalACC_INTERFACE0x0020内部类是否为接口ACC_ABSTRACT0x0400内部类是否为抽象类ACC_SYNCHETIC0x1000字段是否为由编译器自动产生ACC_ANNOTATION0x2000内部类是否为注解ACC_ENUM0x4000内部类是否为枚举

Deprecated 和 Synthetic 属性

这两个都属于标志类型的布尔属性。

Deprecated 表示某个类、字段或方法,已经被程序作者定义为不再推荐使用,可以在代码通过 @deprecated 标识。

Synthetic 表示字段或方法不是由Java源码直接产生,而是编译器自行添加的,唯一例外是实例构造器 <init> 和类构造器 <clinit> 。

Deprecated 和 Synthetic 属性结构:

类型名称数量u2attribute_name_index1u4attribute_length1

attribute_length 数据项值必须为 0x00000000,因为不需要属性值设置

StackMapTable 属性

这是一个复杂的变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。

类型名称数量u2attribute_name_index1u4attribute_length1u2number_of_entries1stack_map_framestack_map_frame entriesnumber_of_entries

Signature 属性

一个可选的定长属性,在 JDK 1.5 发布后增加的,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或参数化类型,则 Signature 属性会为它记录泛型签名信息。这主要是因为 Java 的泛型采用的是擦除法实现的伪泛型,在字节码中泛型信息编译之后统统被擦除,在运行期无法将泛型类型与用户定义的普通类型同等对待。通过 Signature 属性,Java 的反射 API 能够获取泛型类型。

类型名称数量u2attribute_name_index1u4attribute_length1u2signature_index1

其中 signature_index 值必须是一个对常量池的有效索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示类签名、方法类型签名或字段类型签名。

BootstrapMethods 属性

在JDK 1.7 之后加入到 Class 文件规范中。它是一个复杂的变长属性,位于类文件的属性表中,用于保存 invokedynamic指令引用的引导方法限定符。

字节码指令简介

JVM 的指令由一个字节长度、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。

加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,指令包括:

iload/iload 等(加载局部变量到操作栈)istore/istore 等(从操作数栈存储到局部变量表)bipush/sipush/ldc/iconst_<i>(加载常量到操作数栈)wide(扩充局部变量表访问索引)

其中部分指令(如 iload_<n>),代表一组指令(如 iload_0、iload_1、iload_2、iload_3),它们都是带有一个操作数的通用指令,它们省略了显式的操作数,如 iload_0 与操作数为 0 的 iload 指令语义完全一致。

###运算指令

运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。没有直接支持 byte、short、char 和 boolean 类型的算术指令而采用 int 代替。

加减乘除:iadd/isub/imul/idiv求余:irem取反:ineg位移:ishl/ishr按位或:ior按位与:iand按位异或:ixor局部变量自增:iinc比较:dcmpg/dcmpl/fcmpg/fcmpl/lcmp

类型转换指令

类型转换指令可以将两种不同的数值类型进行互相转换,这些转换操作一般用于实现用户代码中的显式类型转换操作。JVM 直接支持宽化类型转换(Widening Numeric Conversions,小范围向大范围的安全转换):

int 到 long、float 或 double 类型long 到 float、double 类型float 到 double 类型

处理窄化类型转换(Narrowing Numberic Conversions)时,必须显式使用转换指令:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f 。窄化类型转换可能会导致转换结果产生不同的正负号、不同的数量级的情况,可能会导致精度丢失。

对象创建与访问指令

虽然类实例和数组都是对象,但是虚拟机创建类对象和数组的指令是不同的。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或数组元素,指令如下:

创建类实例的指令:new;创建数组的指令:newarray、anewarray、multianewarray;访问类字段和实例字段的指令:getfield、putfield、getstatic、putstatic;把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload;将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore;取数组长度的指令:arraylength;检查类实例类型的指令:instanceof、checkcast;

操作数栈管理指令

就像操作一个普通的栈一样,Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:

将操作数栈的栈顶一个或两个元素出栈:pop、pop2;复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2;将栈顶最顶端的两个数值互换:swap;

控制转移指令

控制转移指令可以让 JVM 有条件或无条件的从指定的位置指令而不是控制转移指令的下一条指令继续执行,可以理解为控制转移指令改变了 PC 寄存器的值。指令如下:

条件分支:ifeq、iflt、ifle、ifgt、ifge、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、、if_icmpge、if_acmpeq和if_acmpne;复合条件分支:tableswitch、lookupswitch;无条件分支:goto、goto_w、jsr、jsr_w、ret;

方法调用和返回指令

这里仅仅列出 5 条用于方法调用的指令:

invokevirtual 指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式;invokeinterface 指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用;invokespecial 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法;invokestatic 指令用于调用类方法(static方法);invokedynamic 指令用于在运行时动态解析出调用点限定符索引用的方法,并执行方法,前面 4 条指令的分派逻辑都固化在 Java 虚拟机内部,而 invokedynamic 指令的分派逻辑是由用户所设定的引导方法决定的;

方法调用指令与类型无关,但是方法返回指令是根据返回值的类型区分的,包括 ireturn、lreturn、freturn、dreturn 和 areturn,另外还有一个 return 指令供声明为 void 的方法、实例初始化方法以及类和接口的类初始化方法使用。

异常处理指令

在 Java 程序中显式抛出异常的操作(throw 语句)都是由 athrow 指令来实现的,除了用 throw 语句显式抛出异常外,Java 虚拟机规范还规定了许多运行时异常会在其他 Java 虚拟机指令检测到异常状况时自动抛出。

而在 Java 虚拟机中,处理异常(catch语句)不是由字节码指令来完成的,而是采用异常表来完成的。

同步指令

Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

方法级的同步是隐式的,即不需要通过字节码指令来控制,它实现在方法调用和返回操作中。虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令就会去检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置了,如果设置,执行线程就要求持有管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个方法在执行期间发生了异常,并在方法中无法处理次异常,那么这个同步方法所持有的管程将在异常抛出后自动释放。

同步一段指令集序列通常是由 Java 语言中的 synchronized 语句块表示的,Java 虚拟机的指令集中有 monitorenter和monitorexit指令来支持 synchronized 关键字的语义。正确实现 synchronized 关键字需要 Javac 编译器和 Java 虚拟机两者共同协作。编译器必须保证每个 monitorenter指令都有对应的 monitorexit指令。

公有设计和私有设计

JVM 规范描述了共同程序存储格式:Class 文件格式以及字节码指令集。这些内容与硬件、操作系统以及具体的 JVM 实现之间是完全独立的,虚拟机实现者更愿意把它们看做是程序在各种 Java 平台之间互相安全的交互的手段。

Java 虚拟机的实现必须能够读取 Class 文件并精确实现包含在其中的 Java 虚拟机代码的含义。但一个优秀的虚拟机实现,在满足虚拟机规范的约束下具体实现做出修改和优化也是完全可行,甚至是被鼓励的。虚拟机后台如何处理 Class 文件并不关心,只要外部接口的表现与规范描述的一致即可。

虚拟机实现的方式主要有两种:

将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集将输入的Java虚拟机代码在加载或执行时翻译宿主机 CPU 的本地指令集(即 JIT 代码生成技术)

Class 文件结构的发展

Class 文件结构一直比较稳定,主要的改进集中向访问标志、属性表这些可扩展的数据结构中添加内容。Class 文件格式所具备的平台中立、紧凑、稳定和可扩展的特点,是 Java 技术体系实现平台无关、语言无关两项特性的重要支柱。

小结

本章详细讲解了Class文件结构的各个部分,通过一个实例演示了Class的数据是如何存储和访问的,后面的章节将以动态的、运行时的角度去看看字节码在虚拟机执行引擎是怎样被解析执行的。

参考资料

周志明. 深入理解Java虚拟机 : JVM高级特性与最佳实践 : Understanding the JVM : advanced features and best practices[M]. 机械工业出版社, 2013.
转载请注明原文地址: https://www.6miu.com/read-5031677.html

最新回复(0)