# 类文件结构
我们都知道Java虚拟机执行的是class文件中的字节码(Byte Code),但为什么要存在这样一个中间语言呢?不能直接把Java语言编译成本地机器码(Native Code)吗?
答案就是平台无关性,Java在诞生之初就提出要“一次编写,到处运行(Write Once,Run Anywhere)”,这句话充分表达了当时软件开发人员对跨平台开发的需求之渴望,各种不同的硬件体系结构,各种不同的操作系统,都可以运行着Java虚拟机,上面载入并执行同一种平台无关的字节码
甚至,设计者们曾考虑到虚拟机的语言无关性,让其他语言也能够运行在Java虚拟机上,他们在发布规范文档的时候,特地把规范拆分成《Java语言规范》(The Java Language Specification)和《Java虚拟机规范》(The Java Virtual Machine Specification),如今,Kotlin、Clojure、Groovy、JRuby、JPython、Scala等语言,也的确得益于这种高瞻远瞩的设计构想而诞生了
而实现语言无关性的基础,仍然是虚拟机和字节码存储格式,Java虚拟机不与任何程序语言做绑定(包括Java),它只与class文件这种特定的二进制格式所关联
# Class类文件的结构
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储
根据《Java虚拟机规范》的规定,Class文件采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:“无符号数”和“表”
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有的表的命名都习惯性以“_info”结尾。表用于描述有层次关系的复合结构数据,整个Class文件也可以视作一张表,详见下表
Class文件格式
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 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 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会用一个前置的容量计数器加若干个连续数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的集合
# 魔数与Class文件的版本
- magic
- minor_version
- major_version
魔数在每个Class文件的最开始4个字节,它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件,不仅仅Class文件,很多文件格式标准都有使用魔数来标识的习惯,使用魔数而非文件后缀名,主要出于安全考虑,因为文件后缀名可被随意改动,Class文件的魔数值为0xCAFEBABE(咖啡宝贝)
紧接着魔数的4个字节分别是Class文件的次版本号(Minor Version)和主版本号(Major Version),Java的版本号是从45开始的,JDK1.1之后每个JDK大版本往上加一,高版本的JDK能向下兼容之前版本的Class文件,但不兼容之后版本的Class文件,因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件
# 常量池
- constant_pool_count
- constant_pool
紧接着主次版本号之后,就是常量池了,它通常是Class文件中占用空间最大的数据项目之一
由于常量池中常量的数量并不固定,所以需要先放置一项constant_pool_count来计数,值得注意的是,这个容量计数是从1开始而非0,也就是说constant_pool_count为0x0016,即十进制的22,那么代表常量池中有21项常量,这个设计是为了表达“不引用任何一个常量池项目”的Class文件,那么它的constant_pool_count就为0,Class文件结构中只有常量池的容量计数从1开始,对于其它集合类型,都是从0开始
常量池中主要存放了两种常量:
- 字面量(Literal):比较接近通常说的Java语言层面的概念,比如文本字符串,final类型常量值等
- 符号引用(Symbolic References):属于编译原理方面的概念
常量主要包含以下几类:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle,Method Type,Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site,Dynamically-Computed Constant)
在编译Java代码时,并没有和C和C++那样的连接过程,而是在虚拟机加载Class文件时候进行动态连接,会在这个阶段从常量池获得对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址中
常量池中每一项都是一个表,它们都有一个共同特点,就是表结构起始第一个都是一个u1类型的标志位,代表当前常量属于哪种类型,具体如下:
常量池中的17种数据类型的结构总表
常量 | 项目 | 类型 | 描述 |
---|---|---|---|
CONSTANT_Utf8_info | tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | |
CONSTANT_Float_info | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | |
CONSTANT_Long_info | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | |
CONSTANT_Double_info | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | |
CONSTANT_Class_info | tag | u1 | 值为7 |
index | u2 | 指向全限定名常量项的索引 | |
CONSTANT_String_info | tag | u1 | 值为8 |
index | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为10 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_Interfaceref_info | tag | u1 | 值为11 |
index | u2 | 指向声明方法的接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType_info的索引项 | |
CONSTANT_NameAndType_info | tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | |
index | u2 | 指向该字段或方法描述符常量项的索引 | |
CONSTANT_MethodHandle_info | tag | u1 | 值为15 |
reference_kind | u1 | 值必须在1至9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为 | |
reference_index | u2 | 值必须是对常量池的有效索引 | |
CONSTANT_MethodType_info | tag | u1 | 值为16 |
descriptor_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符 | |
CONSTANT_Dynamic_info | tag | u1 | 值为17 |
bootstrap_method_attr_index | u2 | 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 | |
name_and_type_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 | |
CONSTANT_InvokeDynamic_info | tag | u1 | 值为18 |
bootstrap_method_attr_index | u2 | 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 | |
name_and_type_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 | |
CONSTANT_Module_info | tag | u1 | 值为19 |
name_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示模块名字 | |
CONSTANT_Package_info | tag | u1 | 值为20 |
name_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示包名称 |
对Class文件的常量池信息,可以使用JDK中的javap工具配合-v选项来得到,这里不再赘述
# 访问标志
- access_flags
在常量池之后,紧接着的2个字节就是访问标志了,它用于标识比如这个Class是类还是接口,以及它是public还是其它权限,等等
访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义允许在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类型值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
access_flags占用了u2即2个字节,其中共有16个标志位可以使用,当前只定义了9个,没有使用到的标志位要求一律为零
假设一个类是public类型,且使用了JDK1.0.2之后的编译器进行编译,那么它的access_flags应为:0x0001 | 0x0020 = 0x0021
# 类索引,父类索引与接口索引集合
- this_class
- super_class
- interfaces_count
- interfaces
在访问标志之后,就是类索引,父类索引与接口索引集合
Class文件由这三项数据来确定该类型的继承关系,类索引用于确认这个类的全限定名,父类索引用于确定这个类的父类的全限定名
由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类
接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中
类索引,父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过它里边的index,可以继续找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
# 字段表集合
- fields_count
- fields
字段表用于描述接口或者类中声明的变量,它包含了类级变量以及实例级变量,但不包含在方法内部声明的局部变量,包括的修饰符如下:
- 作用域修饰符(public,private,protected)
- 是实例变量还是类变量修饰符(static)
- 可变性修饰符(final)
- 并发可见性修饰符(volatile,是否强制从主内存读写)
- 可否被序列化修饰符(transient)
以及字段数据类型和名称信息等,以上这三类信息中,修饰符可以用标志位来表示,而后两者信息都是无法固定的,只能用常量池中的常量来描述
字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本代码中不存在的字段,比如在内部类中为了保持对外部类的访问性,编译器会自动添加指向外部类实例的字段
在Java语言层面,字段是无法被重载的,无论字段的数据类型,修饰符是否相同,但是在Class文件中,只要两个字段的描述符不完全相同,重名就是合法的
字段表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
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_TRANSIENT | 0x0080 | 字段是否transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否enum |
描述符标识字符含义
标识字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,如Ljava/lang/Object; |
# 方法表集合
- methods_count
- methods
方法表基本上和字段表使用了完全一致的描述方式
看到这里,可能会疑惑,方法里的代码去哪里了呢?答案是在下面讲到的属性表集合的Code里,后面会细说
和字段表集合相对应,如果父类方法在子类中没有被重写(Override),那么方法表集合里就不会出现来自父类的方法信息,同样,有可能会出现编译器自动添加的方法,比如类构造器“<clinit>()”方法和实例构造器“<init>()”方法
在Java语言层面,重载方法需要不同的特征签名,包括了
- 方法名称
- 参数顺序
- 参数类型
但是,在字节码层面,重载方法还额外包括了:
- 方法返回值
- 受查异常表
所以,如果两个方法有相同的名称和特征签名,但返回值不同,也是可以合法共存于同一个Class文件中的
方法表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
方法访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是不是由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICT | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生 |
那么,方法内部的代码都存放在哪里呢?
在经过Javac编译器编译成字节码指令后,方法内部代码都被存放在了方法属性表集合中一个名为”Code“的属性里面,属性表是Class文件格式中最具拓展性的一种数据项
# 属性表集合
- attributes_count
- attributes
属性表集合是Class文件格式中,最具扩展性的一种数据项目
虚拟机规范预定义的属性
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | 由final关键字定义的常量值 |
Deprecated | 类,方法表,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常列表 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标示这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类,方法表,字段表 | JDK5中新增的属性,用于支持泛型情况下的方法签名。在Java语言中,任何类,接口,初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | JDK5中新增的属性,用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR45提案为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用该属性就可以用于存储这个标准所新加入的调试信息 |
Synthetic | 类,方法表,字段表 | 标识方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | JDK5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | JDK5中新增的属性,为动态注解提供支持。该属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimeInvisibleAnnotations | 类,方法表,字段表 | JDK5中新增的属性,与RuntimeVisibleAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotations | 方法表 | JDK5中新增的属性,作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数 |
RuntimeInvisibleParameterAnnotations | 方法表 | JDK5中新增的属性,作用与RuntimeInvisibleAnnotations属性类似,只不过作用对象为方法参数 |
AnnotationsDefault | 方法表 | JDK5中新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |
RuntimeVisibleTypeAnnotations | 类,方法表,字段表,Code属性 | JDK8中新增的属性,为实现JSR308中新增的类型注解提供的支持,用于指明哪些类注解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimeInvisibleTypeAnnotations | 类,方法表,字段表,Code属性 | JDK8中新增的属性,为实现JSR308中新增的类型注解提供的支持,与RuntimeVisibleTypeAnnotations属性作用刚好相反,用于指明哪些类注解是运行时不可见的 |
MethodParameters | 方法表 | JDK8中新增的属性,用于支持(编译时加上-parameters参数)将方法名称编译进Class文件中,并可运行时获取。此前要获取方法名称(典型的如IDE的代码提示)只能通过JavaDoc中得到 |
Module | 类 | JDK9中新增的属性,用于记录一个Module的名称以及相关信息(requires,exports,opens,uses,provides) |
ModulePackages | 类 | JDK9中新增的属性,用于指定一个模块的主类 |
NestHost | 类 | JDK11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个内部类通过该属性得知自己的宿主类 |
NestMembers | 类 | JDK11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个宿主类通过该属性得知自己有哪些内部类 |
属性表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
# Code属性
Java程序方法体里的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内
Code属性表的结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index是一项指向CONSTANT_Utf8_info类型常量的索引,常量值固定为“Code”,代表了该属性的名称
max_stack代表了操作数栈(Operand Stack)深度的最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度
max_locals代表了局部变量表所需的存储空间,它的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存的最小单位。方法参数(包括实例方法中的隐藏参数“this”),显式异常处理程序的参数(Exception Handler Parameter,就是try-catch语句中catch块中所定义的异常),方法体中定义的局部变量都需要依赖局部变量表来存放,但是需要注意的是,并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数之和作为max_locals的值,操作数栈和局部变量表字节决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费,虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals的大小
code_length和code用来存储Java源程序编译后生成的字节码指令,当虚拟机读到code中的一个字节码时,就可以对应找出这个字节码代表的指令,并且可以知道这个指令后面是否需要跟随参数,以及后续的参数应当如何解析,Code属性是u1类型,也就是可以表达2^8=256条指令,可通过查虚拟机字节码指令表来学习各个指令的作用
Code属性是Class文件中最重要的一个属性,如果Java程序中的信息分为以下两类:
- 代码(Code,方法体里边的Java代码)
- 元数据(Metadata,包括类,字段,方法定义以及其他信息)
那么在Class文件里,Code属性用来描述代码,其他所有信息都用来描述元数据
这里还有个有意思的地方,那就是关于Java语言中this关键字的实现,在任何实例方法里,都可以通过this关键字访问到此方法所属的对象,它的原理其实非常简单,只是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法的时候自动传入此参数而已,所以局部变量表中会预留出第一个变量槽位来存放对象实例的引用,这个处理只对实例方法有效,以及实例方法的Args_size都会是比Java语言中肉眼看到的参数个数多1
异常表属性表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
异常表实际上是Java代码的一部分,尽管字节码中有最初为处理异常而设计的跳转指令,但《Java虚拟机规范》中明确要求Java语言的编译器应当选择使用异常表而不是通过跳转指令来实现Java异常及finally处理机制
当字节码从第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其他之类的异常(catch_type为指向一个CONSTANT_Class_info类型常量的索引),则跳转到第handler_pc行继续进行处理,当catch_type值为0时,代表任意异常情况都需要跳转到handler_pc处进行处理
# Exceptions属性
Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是方法描述时在throws关键字后面列举出的异常
Exceptions属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
# LineNumberTable属性
LineNumberTable属性用于描述Java源代码与字节码行号(字节码的偏移量)之间的对应关系,并不是运行时必需的属性,但默认会生成到Class文件中,用于当抛出异常时,在堆栈信息中显示出错的行号,并且在调试程序的时候,按照源码行来设置断点
LineNumberTable属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
# LocalVariableTable及LocalVariableTypeTable属性
LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,也不是运行时必需的属性,但默认会生成到Class文件中,用于其他人引用这个方法时,能保留参数名称,在调试期间根据参数名从上下文中获得参数值
LocalVariableTypeTable属性是JDK5引入泛型后,因为描述符中泛型的参数化类型被擦除了,用来完成泛型的描述
LocalVariableTable属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
local_variable_info项目结构
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
# SourceFile及SourceDebugExtension属性
SourceFile属性用于记录生成的这个Class文件的源码文件名称,对大多数类来说,类名和文件名都是一致的,但是有些特殊情况下(如内部类),需要这个属性来在堆栈中显示出错代码所属的文件名
SourceDebugExtension属性是为了方便在编译器和动态生成的Class中加入供程序员使用的自定义内容而存在的,典型的场景是在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR45提案为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制
SourceFile属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
SourceDebugExtension属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | debug_extension[attribute_length] | 1 |
# ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用这项属性
- 非static类型的变量:在实例构造器<init>()方法中进行赋值
- static类型的变量:
- 在类构造器<clinit>()方法中进行赋值
- 使用ConstantValue属性进行赋值
ConstantValue属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
# InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联
InnerClasses属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_classes | number_of_classes |
inner_classes_info表的结构
类型 | 名称 | 数量 |
---|---|---|
u2 | inner_class_info_index | 1 |
u2 | outer_class_info_index | 1 |
u2 | inner_name_index | 1 |
u2 | inner_class_access_flags | 1 |
inner_class_access_flags标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 内部类是否为public |
ACC_PRIVATE | 0x0002 | 内部类是否为private |
ACC_PROTECTED | 0x0004 | 内部类是否为protected |
ACC_STATIC | 0x0008 | 内部类是否为static |
ACC_FINAL | 0x0010 | 内部类是否为final |
ACC_INTERFACE | 0x0020 | 内部类是否为接口 |
ACC_ABSTRACT | 0x0400 | 内部类是否为abstract |
ACC_SYNTHETIC | 0x1000 | 内部类是否并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 内部类是不是一个注解 |
ACC_ENUM | 0x4000 | 内部类是不是一个枚举 |
# Deprecated及Synthetic属性
Deprecated及Synthetic属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念
Deprecated属性用于表示某个类,字段或者方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用@Deprecated注解进行设置
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,典型例子如枚举类中自动生成的枚举元素数组和嵌套类的桥接方法(Bridge Method)
Deprecated及Synthetic属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
# StackMapTable属性
StackMapTable属性在JDK6中增加,会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器
新的验证器在同样能保证Class文件合法性的前提下,省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而在编译阶段将一系列的验证类型(Verification Type)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能
StackMapTable属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_entries | 1 |
stack_map_frame | stack_map_frame_entries | number_of_entries |
# Signature属性
Signature属性在JDK5被增加到Class文件规范中,它大幅增强了Java语言的语法,在此之后,任何类,接口,初始化方法或者成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会为它记录泛型签名信息
之所以需要专门使用这样一个属性来记录泛型类型,是因为Java语言的泛型采用了擦除法实现的伪泛型,字节码(Code属性)中所有的泛型信息编译(类型变量,参数化类型)在编译之后都通通被擦除掉,使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动),非常容易实现Backport,运行期也能够节省一些类型所占用的内存空间,但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得反省信息,现在Java的反射API能够获取到的泛型信息,最终的数据来源也是这个属性
Signature属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | signature_index | 1 |
# BootstrapMethods属性
BootstrapMethods属性在JDK7被增加到Class文件规范中,用于保存invokedynamic指令引用的引导方法限定符
BootstrapMethods属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | num_bootstrap_methods | 1 |
bootstrap_method | bootstrap_methods | num_bootstrap_methods |
bootstrap_method结构
类型 | 名称 | 数量 |
---|---|---|
u2 | bootstrap_method_ref | 1 |
u2 | num_bootstrap_arguments | 1 |
u2 | bootstrap_arguments | num_bootstrap_arguments |
# MethodParameters属性
BootstrapMethods属性在JDK8被增加到Class文件规范中,用于记录方法的各个形参名称和信息,最初,基于存储空间的考虑,Class文件默认时不存储方法参数名称的,因为对计算机执行程序来说没有任何区别,后来在IDE中编辑使用包里边的方法时,如果只有单独的包而没有附加的JavaDoc的话,IDE是无法获得方法调用的智能提示的,这就阻碍了JAR包的传播
MethodParameters属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | parameters_count | 1 |
parameter | parameters | parameters_count |
parameter属性结构:
类型 | 名称 | 数量 |
---|---|---|
u2 | name_index | 1 |
u2 | access_flags | 1 |
# 模块化相关属性
JDK9中提供,由于模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以Class文件格式拓展了Module,ModulePackages,ModuleMainClass这三个属性用于支持Java模块化相关功能
Module属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | module_name_index | 1 |
u2 | module_flags | 1 |
u2 | module_version_index | 1 |
u2 | requires_count | 1 |
require | requires | requires_count |
u2 | exports_count | 1 |
export | exports | exports_count |
u2 | opens_count | 1 |
open | opens | opens_count |
u2 | uses_count | 1 |
use | uses_index | uses_count |
u2 | provides_count | 1 |
provide | provides | provides_count |
exports属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | exports_index | 1 |
u2 | exports_flags | 1 |
u2 | exports_to_count | 1 |
export | exports_to_index | exports_to_count |
ModulePackages属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | package_count | 1 |
u2 | package_index | package_count |
ModuleMainClass属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | main_class_index | 1 |
# 运行时注解相关属性
早在JDK5时期,Java语言的语法进行了多项增强,其中之一就是提供了对注解(Annotation)的支持,到了JDK8时期,进一步加强了Java语言的注解适用范围
RuntimeVisibleAnnotations属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | num_annotations | 1 |
annotation | annotations | num_annotations |
annotation属性结构
类型 | 名称 | 数量 |
---|---|---|
u2 | type_index | 1 |
u2 | num_element_value_pairs | 1 |
element_value_pair | element_value_pairs | num_element_value_pairs |
# Class类文件结构实战
我们来通过一段简单的代码片段,尽可能多的包含了上面介绍的各种结构体,来进一步深入了解Class类文件的结构
代码清单
package org.example.helloworld.calculator;
public class Calculator implements Dividable {
private final String errorMessage;
public Calculator(String errorMessage) {
this.errorMessage = errorMessage;
}
public double divide(double d1, double d2) {
if (d2 == 0D) {
throw new ArithmeticException(errorMessage);
}
double d;
try {
d = d1 / d2;
} catch (Exception e) {
d = Double.NaN;
} finally {
System.out.println("do nothing");
}
return d;
}
private class SomeInnerClass {
private String nothing;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
javap -verbose
D:\Data\repository\local\ij\helloworld>javap -verbose target\classes\org\example\helloworld\calculator\Calculator.class
Classfile /D:/Data/repository/local/ij/helloworld/target/classes/org/example/helloworld/calculator/Calculator.class
Last modified 2021年9月21日; size 1200 bytes
SHA-256 checksum 065a779e61369867bb10e7ec1e84bd339f45b5a6b9bb205722b64903e73ded84
Compiled from "Calculator.java"
public class org.example.helloworld.calculator.Calculator implements org.example.helloworld.calculator.Dividable
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #12 // org/example/helloworld/calculator/Calculator
super_class: #13 // java/lang/Object
interfaces: 1, fields: 1, methods: 2, attributes: 2
Constant pool:
#1 = Methodref #13.#40 // java/lang/Object."<init>":()V
#2 = Fieldref #12.#41 // org/example/helloworld/calculator/Calculator.errorMessage:Ljava/lang/String;
#3 = Class #42 // java/lang/ArithmeticException
#4 = Methodref #3.#43 // java/lang/ArithmeticException."<init>":(Ljava/lang/String;)V
#5 = Fieldref #44.#45 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #46 // do nothing
#7 = Methodref #47.#48 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #49 // java/lang/Exception
#9 = Class #50 // java/lang/Double
#10 = Double NaNd
#12 = Class #51 // org/example/helloworld/calculator/Calculator
#13 = Class #52 // java/lang/Object
#14 = Class #53 // org/example/helloworld/calculator/Dividable
#15 = Class #54 // org/example/helloworld/calculator/Calculator$SomeInnerClass
#16 = Utf8 SomeInnerClass
#17 = Utf8 InnerClasses
#18 = Utf8 errorMessage
#19 = Utf8 Ljava/lang/String;
#20 = Utf8 <init>
#21 = Utf8 (Ljava/lang/String;)V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 LocalVariableTable
#25 = Utf8 this
#26 = Utf8 Lorg/example/helloworld/calculator/Calculator;
#27 = Utf8 divide
#28 = Utf8 (DD)D
#29 = Utf8 d
#30 = Utf8 D
#31 = Utf8 e
#32 = Utf8 Ljava/lang/Exception;
#33 = Utf8 d1
#34 = Utf8 d2
#35 = Utf8 StackMapTable
#36 = Class #49 // java/lang/Exception
#37 = Class #55 // java/lang/Throwable
#38 = Utf8 SourceFile
#39 = Utf8 Calculator.java
#40 = NameAndType #20:#56 // "<init>":()V
#41 = NameAndType #18:#19 // errorMessage:Ljava/lang/String;
#42 = Utf8 java/lang/ArithmeticException
#43 = NameAndType #20:#21 // "<init>":(Ljava/lang/String;)V
#44 = Class #57 // java/lang/System
#45 = NameAndType #58:#59 // out:Ljava/io/PrintStream;
#46 = Utf8 do nothing
#47 = Class #60 // java/io/PrintStream
#48 = NameAndType #61:#21 // println:(Ljava/lang/String;)V
#49 = Utf8 java/lang/Exception
#50 = Utf8 java/lang/Double
#51 = Utf8 org/example/helloworld/calculator/Calculator
#52 = Utf8 java/lang/Object
#53 = Utf8 org/example/helloworld/calculator/Dividable
#54 = Utf8 org/example/helloworld/calculator/Calculator$SomeInnerClass
#55 = Utf8 java/lang/Throwable
#56 = Utf8 ()V
#57 = Utf8 java/lang/System
#58 = Utf8 out
#59 = Utf8 Ljava/io/PrintStream;
#60 = Utf8 java/io/PrintStream
#61 = Utf8 println
{
public org.example.helloworld.calculator.Calculator(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #2 // Field errorMessage:Ljava/lang/String;
9: return
LineNumberTable:
line 7: 0
line 8: 4
line 9: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lorg/example/helloworld/calculator/Calculator;
0 10 1 errorMessage Ljava/lang/String;
public double divide(double, double);
descriptor: (DD)D
flags: (0x0001) ACC_PUBLIC
Code:
stack=4, locals=9, args_size=3
0: dload_3
1: dconst_0
2: dcmpl
3: ifne 18
6: new #3 // class java/lang/ArithmeticException
9: dup
10: aload_0
11: getfield #2 // Field errorMessage:Ljava/lang/String;
14: invokespecial #4 // Method java/lang/ArithmeticException."<init>":(Ljava/lang/String;)V
17: athrow
18: dload_1
19: dload_3
20: ddiv
21: dstore 5
23: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
26: ldc #6 // String do nothing
28: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: goto 65
34: astore 7
36: ldc2_w #10 // double NaNd
39: dstore 5
41: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
44: ldc #6 // String do nothing
46: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
49: goto 65
52: astore 8
54: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
57: ldc #6 // String do nothing
59: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
62: aload 8
64: athrow
65: dload 5
67: dreturn
Exception table:
from to target type
18 23 34 Class java/lang/Exception
18 23 52 any
34 41 52 any
52 54 52 any
LineNumberTable:
line 12: 0
line 13: 6
line 18: 18
line 22: 23
line 23: 31
line 19: 34
line 20: 36
line 22: 41
line 23: 49
line 22: 52
line 23: 62
line 25: 65
LocalVariableTable:
Start Length Slot Name Signature
23 11 5 d D
36 5 7 e Ljava/lang/Exception;
41 11 5 d D
0 68 0 this Lorg/example/helloworld/calculator/Calculator;
0 68 1 d1 D
0 68 3 d2 D
65 3 5 d D
StackMapTable: number_of_entries = 4
frame_type = 18 /* same */
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 81 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 252 /* append */
offset_delta = 12
locals = [ double ]
}
SourceFile: "Calculator.java"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
Calculator.class.hexdump
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 34 00 3E 0A 00 0D 00 28 09 J~:>...4.>....(.
00000010: 00 0C 00 29 07 00 2A 0A 00 03 00 2B 09 00 2C 00 ...)..*....+..,.
00000020: 2D 08 00 2E 0A 00 2F 00 30 07 00 31 07 00 32 06 -...../.0..1..2.
00000030: 7F F8 00 00 00 00 00 00 07 00 33 07 00 34 07 00 .x........3..4..
00000040: 35 07 00 36 01 00 0E 53 6F 6D 65 49 6E 6E 65 72 5..6...SomeInner
00000050: 43 6C 61 73 73 01 00 0C 49 6E 6E 65 72 43 6C 61 Class...InnerCla
00000060: 73 73 65 73 01 00 0C 65 72 72 6F 72 4D 65 73 73 sses...errorMess
00000070: 61 67 65 01 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 age...Ljava/lang
00000080: 2F 53 74 72 69 6E 67 3B 01 00 06 3C 69 6E 69 74 /String;...<init
00000090: 3E 01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F >...(Ljava/lang/
000000a0: 53 74 72 69 6E 67 3B 29 56 01 00 04 43 6F 64 65 String;)V...Code
000000b0: 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 ...LineNumberTab
000000c0: 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 le...LocalVariab
000000d0: 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00 leTable...this..
000000e0: 2E 4C 6F 72 67 2F 65 78 61 6D 70 6C 65 2F 68 65 .Lorg/example/he
000000f0: 6C 6C 6F 77 6F 72 6C 64 2F 63 61 6C 63 75 6C 61 lloworld/calcula
00000100: 74 6F 72 2F 43 61 6C 63 75 6C 61 74 6F 72 3B 01 tor/Calculator;.
00000110: 00 06 64 69 76 69 64 65 01 00 05 28 44 44 29 44 ..divide...(DD)D
00000120: 01 00 01 64 01 00 01 44 01 00 01 65 01 00 15 4C ...d...D...e...L
00000130: 6A 61 76 61 2F 6C 61 6E 67 2F 45 78 63 65 70 74 java/lang/Except
00000140: 69 6F 6E 3B 01 00 02 64 31 01 00 02 64 32 01 00 ion;...d1...d2..
00000150: 0D 53 74 61 63 6B 4D 61 70 54 61 62 6C 65 07 00 .StackMapTable..
00000160: 31 07 00 37 01 00 0A 53 6F 75 72 63 65 46 69 6C 1..7...SourceFil
00000170: 65 01 00 0F 43 61 6C 63 75 6C 61 74 6F 72 2E 6A e...Calculator.j
00000180: 61 76 61 0C 00 14 00 38 0C 00 12 00 13 01 00 1D ava....8........
00000190: 6A 61 76 61 2F 6C 61 6E 67 2F 41 72 69 74 68 6D java/lang/Arithm
000001a0: 65 74 69 63 45 78 63 65 70 74 69 6F 6E 0C 00 14 eticException...
000001b0: 00 15 07 00 39 0C 00 3A 00 3B 01 00 0A 64 6F 20 ....9..:.;...do.
000001c0: 6E 6F 74 68 69 6E 67 07 00 3C 0C 00 3D 00 15 01 nothing..<..=...
000001d0: 00 13 6A 61 76 61 2F 6C 61 6E 67 2F 45 78 63 65 ..java/lang/Exce
000001e0: 70 74 69 6F 6E 01 00 10 6A 61 76 61 2F 6C 61 6E ption...java/lan
000001f0: 67 2F 44 6F 75 62 6C 65 01 00 2C 6F 72 67 2F 65 g/Double..,org/e
00000200: 78 61 6D 70 6C 65 2F 68 65 6C 6C 6F 77 6F 72 6C xample/helloworl
00000210: 64 2F 63 61 6C 63 75 6C 61 74 6F 72 2F 43 61 6C d/calculator/Cal
00000220: 63 75 6C 61 74 6F 72 01 00 10 6A 61 76 61 2F 6C culator...java/l
00000230: 61 6E 67 2F 4F 62 6A 65 63 74 01 00 2B 6F 72 67 ang/Object..+org
00000240: 2F 65 78 61 6D 70 6C 65 2F 68 65 6C 6C 6F 77 6F /example/hellowo
00000250: 72 6C 64 2F 63 61 6C 63 75 6C 61 74 6F 72 2F 44 rld/calculator/D
00000260: 69 76 69 64 61 62 6C 65 01 00 3B 6F 72 67 2F 65 ividable..;org/e
00000270: 78 61 6D 70 6C 65 2F 68 65 6C 6C 6F 77 6F 72 6C xample/helloworl
00000280: 64 2F 63 61 6C 63 75 6C 61 74 6F 72 2F 43 61 6C d/calculator/Cal
00000290: 63 75 6C 61 74 6F 72 24 53 6F 6D 65 49 6E 6E 65 culator$SomeInne
000002a0: 72 43 6C 61 73 73 01 00 13 6A 61 76 61 2F 6C 61 rClass...java/la
000002b0: 6E 67 2F 54 68 72 6F 77 61 62 6C 65 01 00 03 28 ng/Throwable...(
000002c0: 29 56 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 53 )V...java/lang/S
000002d0: 79 73 74 65 6D 01 00 03 6F 75 74 01 00 15 4C 6A ystem...out...Lj
000002e0: 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 ava/io/PrintStre
000002f0: 61 6D 3B 01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 am;...java/io/Pr
00000300: 69 6E 74 53 74 72 65 61 6D 01 00 07 70 72 69 6E intStream...prin
00000310: 74 6C 6E 00 21 00 0C 00 0D 00 01 00 0E 00 01 00 tln.!...........
00000320: 12 00 12 00 13 00 00 00 02 00 01 00 14 00 15 00 ................
00000330: 01 00 16 00 00 00 46 00 02 00 02 00 00 00 0A 2A ......F........*
00000340: B7 00 01 2A 2B B5 00 02 B1 00 00 00 02 00 17 00 7..*+5..1.......
00000350: 00 00 0E 00 03 00 00 00 07 00 04 00 08 00 09 00 ................
00000360: 09 00 18 00 00 00 16 00 02 00 00 00 0A 00 19 00 ................
00000370: 1A 00 00 00 00 00 0A 00 12 00 13 00 01 00 01 00 ................
00000380: 1B 00 1C 00 01 00 16 00 00 01 0B 00 04 00 09 00 ................
00000390: 00 00 44 29 0E 97 9A 00 0F BB 00 03 59 2A B4 00 ..D).....;..Y*4.
000003a0: 02 B7 00 04 BF 27 29 6F 39 05 B2 00 05 12 06 B6 .7..?')o9.2....6
000003b0: 00 07 A7 00 22 3A 07 14 00 0A 39 05 B2 00 05 12 ..'.":....9.2...
000003c0: 06 B6 00 07 A7 00 10 3A 08 B2 00 05 12 06 B6 00 .6..'..:.2....6.
000003d0: 07 19 08 BF 18 05 AF 00 04 00 12 00 17 00 22 00 ...?../.......".
000003e0: 08 00 12 00 17 00 34 00 00 00 22 00 29 00 34 00 ......4...".).4.
000003f0: 00 00 34 00 36 00 34 00 00 00 03 00 17 00 00 00 ..4.6.4.........
00000400: 32 00 0C 00 00 00 0C 00 06 00 0D 00 12 00 12 00 2...............
00000410: 17 00 16 00 1F 00 17 00 22 00 13 00 24 00 14 00 ........"...$...
00000420: 29 00 16 00 31 00 17 00 34 00 16 00 3E 00 17 00 )...1...4...>...
00000430: 41 00 19 00 18 00 00 00 48 00 07 00 17 00 0B 00 A.......H.......
00000440: 1D 00 1E 00 05 00 24 00 05 00 1F 00 20 00 07 00 ......$.........
00000450: 29 00 0B 00 1D 00 1E 00 05 00 00 00 44 00 19 00 )...........D...
00000460: 1A 00 00 00 00 00 44 00 21 00 1E 00 01 00 00 00 ......D.!.......
00000470: 44 00 22 00 1E 00 03 00 41 00 03 00 1D 00 1E 00 D.".....A.......
00000480: 05 00 23 00 00 00 0F 00 04 12 4F 07 00 24 51 07 ..#.......O..$Q.
00000490: 00 25 FC 00 0C 03 00 02 00 26 00 00 00 02 00 27 .%|......&.....'
000004a0: 00 11 00 00 00 0A 00 01 00 0F 00 0C 00 10 00 02 ................
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 魔数magic(u4,0x00000000-0x00000003)
首先登场的是u4的magic,值为0xCA 0xFE 0xBA 0xBE(咖啡宝贝?)
# 次版本号minor_version(u2,0x00000004-0x00000005)
紧接着的是u2的minor_version,值为0x00 0x00,表示次版本号为0.0
# 主版本号major_version(u2,0x00000006-0x00000007)
紧接着的是u2的major_version,值为0x00 0x34,表示主版本号为52.0,对应着JDK1.8
# 常量池容量constant_pool_count(u2,0x00000008-0x00000009)
紧接着的是u2的constant_pool_count,值为0x00 0x3E,表示常量池容量为62-1=61,含61个常量,原因前面有提及
# ...
以上,从二进制Class文件的hexdump中解析出信息,其实都是重复工作了,我们可以从javap -verbose的输出中看到一些较为直观的信息
大体上我们已经能够揭开Class文件结构的神秘面纱了,想要继续了解的话,可以找到字节码指令的表,并参照Java源码本身来对照学习
# 字节码指令简介
Java虚拟机的指令为一个字节长度的,由以下两部分构成
- 代表着某种特定操作含义的数字(称为操作码,Opcode)
- 跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)
由于Java虚拟机采用面向操作数栈而非面向寄存器的架构,所以大多数指令不包含操作数,只有一个操作码,指令的参数都存放在操作数栈中
因为限制了Java虚拟机操作码长度为一个字节,所以指令集的操作码总数不能超过2^8=256条
又因为Class文件格式放弃了编译后代码的操作数长度对齐,所以虚拟机在处理那些超过一个字节的数据时,不得不在运行时从字节中重建出具体数据的结构
比如要将一个16位长度的无符号整数使用两个无符号字节存储,它们的值应该是这样的:(byte1 << 8 ) | byte2
这样虽然导致解释字节码时会损失一些性能,但是带来的优点是可以获得尽可能精简的编译代码,以及更高的传输效率,这是因为当初Java语言设计之初主要面向网络,智能家电的技术背景所决定的
不考虑异常处理的话,Java虚拟机的解释器大致就是按照以下的伪代码执行模型来处理字节码的
do {
自动计算PC寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作码;
if (字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
} while (字节码流长度 > 0);
2
3
4
5
6
# 字节码与数据类型
在Java虚拟机指令集中,大多数指令都包含其操作所对应的数据类型信息,比如iload指令用于从局部变量表中加载int类型数据到操作数栈中,而fload指令加载的则是float类型数据,这两条指令的操作在虚拟机内部可能是由同一段代码来实现的,但是在Class文件中它们必须拥有各自独立的操作码
下表列举了Java虚拟机所支持的与数据类型相关的字节码指令,通过使用数据类型列所代表的特殊字符替换opcode列的指令模板中的T,就可以得到具体的字节码指令
opcode | byte | short | int | long | float | double | char | reference |
---|---|---|---|---|---|---|---|---|
Tipush | bipush | sipush | ||||||
Tconst | iconst | lconst | fconst | dconst | aconst | |||
Tload | iload | lload | fload | dload | aload | |||
Tstore | istore | lstore | fstore | dstore | astore | |||
Tinc | iinc | |||||||
Taload | baload | saload | iaload | laload | faload | daload | caload | aaload |
Tastore | bastore | sastore | iastore | lastore | fastore | dastore | castore | aastore |
Tadd | iadd | ladd | fadd | dadd | ||||
Tsub | isub | lsub | fsub | dsub | ||||
Tmul | imul | lmul | fmul | dmul | ||||
Tdiv | idiv | ldiv | fdiv | ddiv | ||||
Trem | irem | lrem | frem | drem | ||||
Tneg | ineg | lneg | fneg | dneg | ||||
Tshl | ishl | lshl | ||||||
Tshr | ishr | lshr | ||||||
Tushr | iushr | lushr | ||||||
Tand | iand | land | ||||||
Tor | ior | lor | ||||||
Txor | ixor | lxor | ||||||
i2T | i2b | i2s | i2l | i2f | i2d | |||
l2T | l2i | l2f | l2d | |||||
f2T | f2i | f2l | f2d | |||||
d2T | d2i | d2l | d2f | |||||
Tcmp | lcmp | |||||||
Tcmpl | fcmpl | dcmpl | ||||||
Tcmpg | fcmpg | dcmpg | ||||||
if_TcmpOP | if_icmpOP | if_acmpOP | ||||||
Treturn | ireturn | lreturn | freturn | dreturn | areturn |
# 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,包括:
- 将一个局部变量加载到操作栈:Tload
- 将一个数值从操作数栈存储到局部变量表:Tstore
- 将一个常量加载到操作数栈:Tipush,ldc*,Tconst
- 扩充局部变量表的访问索引的指令:wide
# 运算指令
运算指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶
- 加法指令:Tadd
- 减法指令:Tsub
- 乘法指令:Tmul
- 除法指令:Tdiv
- 求余指令:Trem
- 取反指令:Tneg
- 位移指令:Tshl,Tshr
- 按位或指令:Tor
- 按位与指令:Tand
- 按位异或指令:Txor
- 局部变量自增指令:Tinc
- 比较指令:Tcmp,Tcmpl,Tcmpg
Java虚拟机在实现处理整数时,只有除法指令(idiv和ldiv)以及求余指令(irem和lrem)中当出现除数为零时会导致虚拟机抛出ArithmeticException,其余任何整型运算场景都不应该抛出运行时异常。在实现处理浮点数时,严格遵循IEEE 754中的规范,所有的运算结果都必须舍入到适当的精度
# 类型转换指令
类型转换指令可以将两种不同的数值类型互相转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题
Java虚拟机直接支持以下数字类型的宽化类型转换(Widening Numeric Conversion,即小范围类型向大范围类型的安全转换)
- int类型到long,float,double类型
- long类型到float,double类型
- float类型到double类型
与之相对的,处理窄化类型转换(Narrowing Numeric Conversion)时,就必须显式地使用转换指令来完成
- int转:i2T
- long转:l2T
- float转:f2T
- double转:d2T
窄化类型转换可能会导致转换结果产生不同的正负号,不同的数量级的情况,也可能导致数值的精度丢失,但却永远不可能导致虚拟机抛出运行时异常
# 对象创建与访问指令
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令,这些指令包括:
- 创建类实例的指令:new
- 创建数组的指令:newarray,anewarray,multianewarray
- 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield,putfield,getstatic,putstatic
- 把一个数组元素加载到操作数栈的指令:Taload
- 将一个操作数栈的值存储到数组元素中的指令:Tastore
- 取数组长度的指令:arraylength
- 检查类实例类型的指令:instanceof,checkcast
# 操作数栈管理指令
如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:
- 将操作数栈的栈顶一个或两个元素出栈:pop,pop2
- 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2
- 将栈最顶端的两个数值互换:swap
# 控制转移指令
控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令)的下一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值,指令包括:
- 条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,if_icmpeq,if_icmpne,if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne
- 复合条件分支:tableswitch,lookupswitch
- 无条件分支:goto,goto_w,jsr,jsr_w,ret
# 方法调用和返回指令
指令包括:
- invokevirtual指令:用于调用对象的实例方法
- invokeinterface指令:用于调用接口方法
- invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
- invokestatic指令:用于调用类静态方法
- invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面四条指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的
# 异常处理指令
在Java程序中显式抛出异常的操作(throw语句)都是由athrow指令来实现,而处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经废弃了),而是采用异常表来完成
# 同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的
方法级的同步是隐式的,无需通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法
当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程Monitor,然后才能执行方法,最后当方法完成(无论正常完成还是非正常完成)时释放管程,在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程
如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有两条指令来支持synchronized关键字的语义:
- monitorenter
- monitorexit
编译器必须确保无论方法通过何种方式完成,方法中调用的每条monitorenter指令都必须有其对应的monitorexit指令,无论这个方法是正常结束还是异常结束