Java字节码.class文件案例分析

时间:2023-01-28 17:20:53

javac编译过后的字节码(16进制)

下边的截图就是.class文件的内容

Java字节码.class文件案例分析

它对应的源代码部分的内容为
Java字节码.class文件案例分析


核心概念

Java虚拟机规范中规定,Class文件格式采用一种类似C语言结构体的伪结构来存储,它只有两种数据类型

  • 无符号数(基本数据类型)
    主要用于描述数字、索引引用、数量值、或UTF-8编码构成的字符串;
    u1 – 1个字节
    u2 – 2个字节
    u4 – 4个字节
    u8 – 8个字节
  • 表(符合数据类型)
    用于描述有层次关系的符合结构的数据;
    习惯性以“_info”结尾

1.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

2.Class文件版本号

2.1.Java中javac的参数-source-target
  • -source
    表示当前源代码使用什么版本下的JDK进行编译,例如:javac -source 1.4 TestClass.java表示使用JDK 1.4的语法对当前.java源文件进行编译(我机器安装的JDK为1.8),估计从JDK 1.9开始就不支持1.5/5以及更早版本了;
  • -target
    表示编译器生成特定版本的Java类文件格式,可指定Class文件格式,例如:javac -source 1.4 -target 1.4 TestClass.java表示使用-source 1.4的语法源代码,生成的最终Class文件格式也是1.4的格式;

-target在使用的时候需要加上-source,否则会产生错误,下边两种错误都是不能正确生成.class字节码文件的:

错误1:(不带-source)默认的-source是1.8,但在输出类文件格式的时候尝试使用1.5的文件格式输出

Java字节码.class文件案例分析
错误2:(带-source)带上了-source的1.8参数过后尝试用1.5的文件格式输出
Java字节码.class文件案例分析
警告:(带-source)带上了-source的1.5参数过后尝试用1.7的文件格式输出(编译可通过)
Java字节码.class文件案例分析

综上所述,-source的版本号必须小于或等于-target的版本号,一旦大于了过后可能导致编译不通过,但这里会有一个问题,从下边的表看来,直接使用低版本输出-target 的方式应该是可行的,但这个用法似乎只适合特定版本的JDK,例如:1.6.0_01可直接使用-target 1.5输出JDK 1.5的字节码文件,我在本机使用1.8的版本输出时就会报错。( -source <= -target )

2.2.Class文件版本号
编译器版本 -target参数 十六进制版本 十进制版本
JDK 1.1.8 不能带target参数 00 03 00 2D 45.3
JDK 1.2.2 不带(默认 -target 1.1) 00 03 00 2D 45.3
JDK 1.2.2 -target 1.2 00 00 00 2E 46.0
JDK 1.3.1_19 不带(默认 -target 1.1) 00 00 00 2D 45.3
JDK 1.3.1_19 -target 1.3 00 00 00 2F 47.0
JDK 1.4.2_10 不带(默认 -target 1.2) 00 00 00 2E 46.0
JDK 1.4.2_10 -target 1.4 00 00 00 30 48.0
JDK 1.5.0_11 不带(默认 -target 1.5) 00 00 00 31 49.0
JDK 1.5.0_11 -target 1.4 -source 1.4 00 00 00 30 48.0
JDK 1.6.0_01 不带(默认 -target 1.6) 00 00 00 32 50.0
JDK 1.6.0_01 -target 1.5 00 00 00 31 49.0
JDK 1.6.0_01 -target 1.4 -source 1.4 00 00 00 30 48.0
JDK 1.7.0 不带(默认 -target 1.7) 00 00 00 33 51.0
JDK 1.7.0 -target 1.6 00 00 00 32 50.0
JDK 1.7.0 -target 1.4 -source 1.4 00 00 00 30 48.0
JDK 1.8.0 不带(默认 -target 1.8) 00 00 00 34 52.0

注意:

  • -target 1.1中包含了次版本号,之后就没有次版本号了;
  • 从1.1到1.4的语法差异很小,默认的-target使用的都不是自身对应版本;
  • 1.5开始过后默认的-target是1.5,所以如果要生成1.4的文件格式则需要加上-source 1.4,之后的JDK使用也如此;

最后:某个JVM能接受的class文件的最大主版本号不能超过对应JDK带相应-target参数编译出来的class文件的版本号。例:1.4的JVM能接受最大的class文件的主版本号不能超过1.4 JDK使用-target 1.4输出的主版本号,即48。因为JDK 1.5默认编译输出-target为1.5,则最终class字节码是49.0,所以1.4的JVM是无法执行和支持JDK 1.5编译输出的字节码的,只有抛出错误。

3.关于常量池

3.1.基础知识

常量池中主要存放两大类型常量。

  • 字面量【Literal】
  • 符号引用【Symbolic References】(详细内容可参考编译原理)
    符号引用主要包含三种:
    1)类和接口的全限定名(Fully Qualified Name);
    2)字段的名称和描述符(Descriptor);
    3)方法的名称和描述符;

Java和C/C++语言有一点不同,它没有Link【链接】的步骤。

  • C/C++语言一般是把源文件编译成.obj的目标文件,然后“链接”成可执行程序;
  • Java则会使用JVM加载.class文件,在加载的时候使用动态链接,也就是说Class文件不会保存方法和字段的最终内存信息,这些符号引用如果不经过转化的话是无法直接被虚拟机使用的。
3.2.常量项目类型

常量池中每一项常量都是一个表,共有11种结构【除去JDK 1.7之后的CONSTANT_InvokeDynamic和CONSTANT_InvokeDynamicTrans两个】,这11种表的第一位都是一个u1类型的标志位(Tag,1 ~ 12,缺少标志为2的数据类型),表示当前常量的类型。

类型 标志 描述
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 字段或方法的部分符号引用
3.3.使用javap输出常量

JDK中提供了javap工具,该工具主要用于分析字节码,使用下边命令可输出当前字节码文件中的所有常量(例子中35项):

javap -verbose TestClass.class

输出(为了方便截图使用PowerShell输出,路径切换成.\TestClass.class,其他的没有变化):
Java字节码.class文件案例分析
Java字节码.class文件案例分析

3.4.常量类型的结构总表

上边提到了11种常量池的结构信息,那么这里再提供11种常量类型的结构总表,细化到前边提到的数据类型(Tag对应3.2中的表)。

常量 项目 类型 描述
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的索引项
CONSTANT_Methodref_info tag u1 值为10
index u2 指向声明方法的类或接口描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型CONSTANT_NameAndType的索引项
CONSTANT_InterfaceMethodref_info tag u1 值为11
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型CONSTANT_NameAndType的索引项
CONSTANT_NameAndType_info tag u1 值为12
index u2 指向该字段或方法名称常量项的索引
index u2 指向该字段或方法描述符常量项的索引

4.访问标志

当常量池结束过后,接着的2个字节就表示访问标记(access_flags),这个标记用于标识类或者接口层次的访问信息,例如:

  1. 这个Class是类还是接口?
  2. 是否定义为public类型?
  3. 是否定义为abstract类型?
  4. 如果是类,有没有被声明为final?
4.1.访问标志表
标志名称 标志值 二进制值 含义
ACC_PUBLIC 0x0001 0000 0000 0000 0001 是否为public类型
ACC_FINAL 0x0010 0000 0000 0001 0000 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 0000 0000 0010 0000 是否允许使用invokespecial字节码指令,JDK 1.2之后编译出来的类的这个标志为真
ACC_INTERFACE 0x0200 0000 0010 0000 0000 标识这个是一个接口
ACC_ABSTRACT 0x0400 0000 0100 0000 0000 是否为abstract类型,对于接口或抽象类来说,此标记值为真,其他类值为假
ACC_SYNTHETIC 0x1000 0001 0000 0000 0000 (JDK 1.5之后定义)标识这个类并非由用户代码生成
ACC_ANNOTATION 0x2000 0010 0000 0000 0000 (JDK 1.5之后定义)标识这是一个注解
ACC_ENUM 0x4000 0100 0000 0000 0000 (JDK 1.5之后定义)标识这是一个枚举
4.2.注意事项和访问标志计算

其他标记比较容易理解,这里解释一下ACC_SYNTHETIC标记,ACC_SYNTHETIC标记等价的属性称为Synthetic Attribute,它用于指示当前类、接口、方法或字段由编译器生成,而不在源代码中存在(不包含类初始化函数和实例初始化函数),相同的功能还有一种方式就是在类、接口、方法或字段的访问权限中设置ACC_SYNTHETIC标记。Synthetic Attribute是从JDK 1.1中引入的,主要用于支持内嵌类和接口(Nested classes && Interfaces),这些功能目前都可以使用ACC_SYNTHETIC来表达。ACC_SYNTHETIChe Synthetic Attribute功能相同,但不是同一个东西。

access_flags的计算公式为:access_flags = flagA | flagB | flagB ...

比如:一个访问访问标志是0x0021 = 0x0020 | 0x0001 = ACC_SUPER | ACC_PUBLIC

5.字段表集合

字段表(field_info)用于描述类或接口中声明的变量,它包含类变量、实例变量,但不包括方法内的局部变量和块变量。和cp_info部分不一样,cp_info因为常量类型的不一样其数据结构有11种,但field_info的结构只有一种。

5.1.字段表结构
类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count
5.2.字段访问标志

字段访问标志和类的访问标志算法是一样,但因为修饰字段的标志和修饰类的标志不太一样,看看下边的字段访问标志(上表结构中的access_flags)。

标志名称 标志值 二进制值 含义
ACC_PUBLIC 0x0001 0000 0000 0000 0001 是否public
ACC_PRIVATE 0x0002 0000 0000 0000 0010 是否private
ACC_PROTECTED 0x0004 0000 0000 0000 0100 是否protected
ACC_STATIC 0x0008 0000 0000 0000 1000 是否static
ACC_FINAL 0x0010 0000 0000 0001 0000 是否final
ACC_VOLATILE 0x0040 0000 0000 0100 0000 是否volatile
ACC_TRANSIENT 0x0080 0000 0000 1000 0000 是否transient
ACC_SYNTHETIC 0x1000 0001 0000 0000 0000 是否由编译器自动产生
ACC_ENUM 0x4000 0100 0000 0000 0000 是否enum
5.3.简单名称、描述符、全限定名

在access_flags标志之后的有两部分:

  • name_index:表示字段的简单名称;
  • descriptor_index:表示字段和方法的描述符;

这里区分三个概念,本文中反复提到:全限定名、简单名称、描述符:

  1. 全限定名
    全限定名格式如:com/sco/core/TestClass,仅仅是把类全名中的.替换成了/而已,为了连续多个全限定名不混淆,结尾会使用一个;表示全限定名结束。
  2. 简单名称
    简单名称则是没有类型、参数修饰的方法或字段名,比如TestClass类中的agename字段名,inc方法名。
  3. 描述符
    方法和字段的描述符主要用来描述字段的数据类型、方法的参数列表(包括数量、类型、顺序)和返回值。
5.4.字段描述符表
标识字符 十六进制值 含义
B 42 基本类型byte
C 43 基本类型char
D 44 基本类型double
F 46 基本类型float
I 49 基本类型int
J 4A 基本类型long
S 53 基本类型short
Z 5A 基本类型boolean
V 56 基本类型void
L 4C 对象类型,如:Ljava/lang/Object;

除了上述的基本类型和对象类型描述符以外,Java中还有其他数据类型的描述符:

数组类型:对于数组类型,每一维度使用一个前置的“[”字符来描述,例:java.lang.String[][] => [[Ljava/lang/String; int[] => [I;

6.方法表集合

方法表集合(method_info)和字段表集合的结构是一致的,只是访问标志不同。

6.1.方法访问标志
标志名称 标志值 二进制值 含义
ACC_PUBLIC 0x0001 0000 0000 0000 0001 是否public
ACC_PRIVATE 0x0002 0000 0000 0000 0010 是否private
ACC_PROTECTED 0x0004 0000 0000 0000 0100 是否protected
ACC_STATIC 0x0008 0000 0000 0000 1000 是否static
ACC_FINAL 0x0010 0000 0000 0001 0000 是否final
ACC_SYNCHRONIZED 0x0020 0000 0000 0010 0000 是否synchronized
ACC_BRIDGE 0x0040 0000 0000 0100 0000 是否由编译器产生的桥接方法
ACC_VARARGS 0x0080 0000 0000 1000 0000 方法是否接受不定参数
ACC_NATIVE 0x0100 0000 0001 0000 0000 是否native
ACC_ABSTRACT 0x0400 0000 0100 0000 0000 是否abstract
ACC_STRICT 0x0800 0000 1000 0000 0000 是否strictfp
ACC_SYNTHETIC 0x1000 0001 0000 0000 0000 是否由编译器自动产生

7.属性表集合

属性表集合(attribute_info)在前边大部分分析中一直没有遇到,直到TestClass中的方法1中才第一次出现attribute_info的结构。它用于在Class文件、字段表、方发表中携带自己的属性表集合,以用于描述某些场景的专有信息。先看看下边

JVM虚拟机规范预定的属性

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 源文件名称
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的
7.1.Code属性

属性表结构

对于每个属性,名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构是完全自定义的,但符合规则的属性表应该满足下边的表结构。

类型 名称 数量
u2 attribute_name_index 1
u2 attribute_length 1
u2 info attribute_length

Code属性表结构

Java程序方法体里面的代码经过javac编译器处理过后将最终字节码存储在Code属性内。*:抽象类或接口中的方法不存在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”,即属性的属性名为固定值,长度为4;
  • attribute_length:表示该属性的长度,属性长度 = 属性表长度 - 6字节(attribute_name_index占2字节,attribute_length占4字节);
  • max_stack:操作数栈(Operand Stack)最大深度,任何时候操作数栈都不会超过这个深度,JVM根据这个值分配栈帧(Frame);
  • max_locals:局部变量表所需的存储空间,max_locals单位是Slot,Slot是JVM为局部变量分配内存所使用的最小单位;
    1)对于byte、char、float、int、short、boolean、reference、returnAddress长度不超过32位的数据类型,每个局部变量占用1个Slot;
    2)而double和long这两种64位的数据类型则需要占用2个Slot存放
    3)另外方法参数(包括实例方法中隐藏参数“this”)、显示异常处理器参数(Exception Handler Parameter,即try-catch语句中catch定义的异常)、方法中定义的局部变量也需要使用局部变量表来存。(Slot中存储的变量是可以重用的,max_locals的大小并不是Slot之和。)
  • code_length和code:存储了Java源程序编译后生成的字节码指令,code_length是字节码长度,code是用于存储字节码指令的字节流;每一个字节码指令是u1类型,范围从0x00 ~ 0xFF,包含256条字节容量,而JVM中只有200条编码值对应。code_length是u4类型,理论上最大值可以到(2^32 - 1),但JVM中限制了一个方法不允许超过65535条字节码指令,否则javac会拒绝编译;

Class中核心的两部分:

  1. Code:代码区,方法体里的Java代码;
  2. Metadata:元数据,类、字段、方法定义以及其他部分信息;
7.2.Exception属性

异常表结构

类型 名称 数量
u2 start_pc 1
u2 end_pc 1
u2 handler_pc 1
u2 catch_type 1

他的基本含义是:如果字节码从start_pc行(这里的行非源代码行)到第end_pc行(不包含end_pc行)之间出现了类型为catch_type或其子类异常,则转到第handler_pc行继续处理,当catch_type为0时,代表任何异常情况都需要转到handler_pc行处进行处理。

Exception属性结构

和前边的异常表结构不同,这里的Exception属性表是在方法中和Code属性平级,它的作用是列举出方法中可能抛出的受检查异常(Checked Exception),也就是方法描述时在throws关键字后边列举的异常。*:只针对Checked Exception

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

7.3.LineNumberTable属性

LineNumberTable属性用于描述Java源代码行号和字节码行号(字节码的偏移量)之间的对应关系,它不是运行时必须属性,但默认会生成到Class文件中。也可以在javac中使用-g:none-g:lines选项来取消或显示生成这一部分信息。

若不生成LineNumberTable的影响就是抛出Exception异常信息的时候不会在堆栈信息中显示行号,并且调试的时候无法按照源代码设置断点。

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

这里只有一点需要说明:line_number_info = start_pc + line_number,两个变量都是u2类型的变量

  • start_pc:表示字节码行号;
  • line_number:表示Java源代码行号;
7.4.LocalVariableTable属性

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源代码定义的变量之间的关系,但是这种关系并非运行时必须,默认也不会生成到Class文件中,可以通过javac中使用-g:none-g:vars选项取消或者生成这项信息。

如果没有这项信息,最大的影响就是其他人使用这个方法时,所有参数名会丢失,IDE可能使用arg0、arg1占位符替代原来的参数,这对程序运行没有影响,但会给写代码带来很大的不方便。

LocalVariableTable属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_veriable_info local_variable_table local_variable_table_length

*:这里的特殊变量是local_variable_info类型,它描述了一个栈帧与源代码中局部变量的关联,有单独的结构。

local_variable_info项目结构

类型 名称 数量
u2 start_pc 1
u2 length 1
u2 name_index 1
u2 descriptor_index 1
u2 index 1

解释一下五个属性:

  • start_pc:表示这个局部变量的生命周期开始的字节码偏移量;
  • length:表示这个局部变量的作用范围覆盖长度,和start_pc一起就表示局部变量在字节码中的作用范围;
  • name_index:局部变量的名称对应常量池的符号引用;
  • descriptor_index:局部变量的类型对应的常量池的符号引用;
  • index:局部变量在栈帧局部变量中Slot的位置,如果数据类型是long或double(64bit),Slot为index和index + 1两个位置;

JDK 1.5引入了泛型之后,LocalVeriableTable属性添加了一个“姐妹属性”:LocalVariableTypeTable,这个属性的结构和LocalVariableTable相似,仅仅把记录的字段描述符的descriptor_index替换成字段特征签名(Signature)。

7.5.SourceFile属性

SourceFile属性主要记录生成这个Class文件的源代码名称,也属于可选属性,可以使用javac的-g:none-g:source选项来关闭或要求生成这些信息。

SourceFile结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

7.6.ConstantValue属性

字节码中的ConstantValue的主要作用是虚拟机自动为静态变量赋值,只有被修饰了static关键字的变量(类变量)才可以使用这项属性。JVM对static类变量的赋值方式有两种:

  • 在类构造器<cinit>中进行——在ConstantValue属性基础之上如果没有final修饰,并且不属于基本类型或java.lang.String,则使用<cinit>;
  • 使用ConstantValue属性进行赋值——如果同时使用static和final,并且这个变量数据类型是8种基本类型或java.lang.String,则使用ConstantValue属性初始化;

ConstantValue结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 constantvalue_index 1

7.7.InnerClass属性

InnerClass属性结构

类型 名称 数量
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_name_index:如果是匿名内部类,则这个值为0

inner_class_access_flags标志

标志名称 标志值 二进制值 含义
ACC_PUBLIC 0x0001 0000 0000 0000 0001 是否public
ACC_PRIVATE 0x0002 0000 0000 0000 0010 是否private
ACC_PROTECTED 0x0004 0000 0000 0000 0100 是否protected
ACC_STATIC 0x0008 0000 0000 0000 1000 是否static
ACC_FINAL 0x0010 0000 0000 0001 0000 是否final
ACC_SYNCHRONIZED 0x0020 0000 0000 0010 0000 是否synchronized
ACC_ABSTRACT 0x0400 0000 0100 0000 0000 是否abstract
ACC_SYNTHETIC 0x1000 0001 0000 0000 0000 是否由编译器自动产生
ACC_ANNOTATION 0x2000 0010 0000 0000 0000 (JDK 1.5之后定义)标识这是一个注解
ACC_ENUM 0x4000 0100 0000 0000 0000 (JDK 1.5之后定义)标识这是一个枚举

7.8.Deprecated, Synthetic属性

Deprecated和Synthetic两个属性都是boolean标记,只存在有和没有的区别,没有属性值的概念,它们结构很简单

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1

8.JDK 1.5和JDK 1.6中的新属性

属性名称 使用位置 含义
StackMapTable Code属性 JDK 1.6中添加,为了加快Class文件校验,把类型校验时需要用的相关信息直接写入class文件,以前这些信息通过代码数据流分析得到。
EnclosingMethod JDK 1.5中添加的属性,当一个类为局部类或匿名类时,可通过此属性声明其访问范围。
Signature 类、方法表、字段表 JDK 1.5中添加的属性,存储类、方法、字段的特征签名。JDK 1.5引入泛型是Java语言的进步,虽然使用了类型擦除避免在字节码级别产生冲突,但元数据中的泛型信息需要保留,这种情况下描述符无法精确描述泛型信息,所以添加这个特征签名属性。
SourceDebugExtension JDK 1.6中添加的属性,SourceDebugExtension属性用于存储额外调试信息,譬如JSP调试无法通过Java堆栈定位JSP的行号,JSR-45中为了非Java语言编写却需要编译成字节码运行在JVM中的程序提供了可进行调试的标准机制,使用SourceDebugExtension可存储这个标准新加入的调试信息。
LocalVariableTypeTable JDK 1.5中添加的属性,使用特征签名代替描述符,为了引入泛型语法之后能描述泛型参数化类型而添加。
RuntimeVisibleAnnotations 类、方法表、字段表 JDK 1.5添加的属性,为动态注解提供支持,RuntimeVisibleAnnoations属性用于指明那些注解是运行时可见的。
RuntimeInvisibleAnnotations 类、方法表、字段表 JDK 1.5添加的属性,作用和上边作用刚好相反。
RuntimeVisibleParameterAnnotations 方法表 JDK 1.5添加的属性,作用和RuntimeVisiableAnnotations类似,只不过作用对象是参数。
RuntimeInvisibleParameterAnnotations 方法表 JDK 1.5添加的属性,不解释。
AnnotationDefault 方法表 JDK 1.5添加的属性,用于记录注解类元素的默认值

解析com.sco.core.TestClass的字节码

1.魔数段(magic)

CA FE BA BE

Class字节码文件的头四个字节称为魔数(Magic Number),唯一的作用是用于确定这个文件是否为一个虚拟机可接受的Class文件,Java字节码文件的魔数段是固定的,就是“咖啡宝贝”。

2.Class文件版本(minor_version major_version)

00 00 00 34

紧跟魔数的4个字节是Class文件的版本号,第5和第6字节是次版本号(Minor Version,这里是00 00),第7和第8字节是主版本号(Major Version,这里是00 34),34是十六进制,对应十进制的52,即JDK 1.8的字节码。参考2.2章节的版本号详细内容,JDK版本从45开始到52,低版本的JVM是不能执行高版本的字节码的,范围是Version.0到Version.65535,比如JDK 1.7可执行的是51.0 ~ 51.65535。

3.常量池(constant_pool)

3.1.常量池入口
00 24

常量池入口是一个u2类型的数据,表示常量池容量计数(constant_pool_count),从1开始计数,24是十六进制,十进制为36,则表示常量池中有35项常量,索引为1 ~ 35,索引0的位置为预留,可表示“不引用任何一个常量池项目”。只有常量池的容量计数是从1开始!!

3.2.常量池内容

索引值和常量的标号对应,从1 ~ 35总共35个常量

常量1:

0A 00 07 00 14 // java/lang/Object.”<init>”:()V
  • 0A——tag值为10,表示第一个常量类型是CONSTANT_Methodref_info
  • 00 07——#7 声明当前方法类描述符索引值为7;
  • 00 14——#20 当前方法的名称和类型索引值为20;

常量2:

09 00 06 00 15 // com/sco/core/TestClass.age:I
  • 09——tag值为9,类型为CONSTANT_Fieldref_info
  • 00 06——#6 声明当前方法类描述符索引值为6;
  • 00 15——#21 字段描述符的名称和类型索引值为21;

常量3:

09 00 06 00 16 // com/sco/core/TestClass.name:Ljava/lang/String;
  • 09——tag值为9,类型为CONSTANT_Fieldref_info
  • 00 06——#6 声明当前方法类描述符索引值为6;
  • 00 16——#22 字段描述符的名称和类型索引值为22;

除了索引值,和第二个常量的其他内容都一致,也属于字段定义信息。

常量4:

09 00 17 00 18 // java/lang/System.out:Ljava/io/PrintStream;
  • 09——tag值为9,类型为CONSTANT_Fieldref_info
  • 00 17——#23 声明当前方法类型描述符索引为23;
  • 00 18——#24 字段描述符的名称和类型索引值为24;

常量5:

0A 00 19 00 1A // java/io/PrintStream.out:Ljava/io/PrintStream;
  • 0A——tag值为10,类型为CONSTANT_Methodref_info
  • 00 19——#25 声明当前方法类描述符索引值为25;
  • 00 1A——#26 当前方法的名称和类型索引值为26;

常量6:

07 00 1B // com/sco/core/TestClass
  • 07——tag值为7,类型为CONSTANT_Class_info
  • 00 1B——#27 类型为“类或接口符号引用”,所以全限定名常量索引为27;

常量7:

07 00 1C // java/lang/Object
  • 07——tag值为7,类型为CONSTANT_Class_info
  • 00 1C——#28 类型为“类或接口符号引用”,所以全限定名常量索引为28;

常量8:

01 00 03 61 67 65
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 03——这个UTF-8编码的常量字符串长度为3,也就是说随后3个字节表示这个字符串常量;
  • 61 67 65——随后3个字节分别表示(字符串“age”)
    61 -> 97 -> a
    67 -> 103 -> g
    65 -> 101 -> e

    age

常量9:

01 00 01 49
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 01——这个UTF-8编码的常量字符串长度为1;
  • 49——随后1个字节表示
    49 -> 73 -> I
    I

常量10:

01 00 04 6E 61 6D 65
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 04——这个UTF-8编码的常量字符串长度为4;
  • 6E 61 6D 65——随后四个字节表示(字符串“name”)
    6E -> 110 -> n
    61 -> 97 -> a
    6D -> 109 -> m
    65 -> 101 -> e

    name

常量11:

01 00 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 12——这个UTF-8编码的常量字符串长度为18;
  • 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B——18个字节的字符串,对应:Ljava/lang/String;

常量12:

01 00 06 3C 69 6E 69 74 3E
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 06——这个UTF-8编码的常量字符串长度为6;
  • 3C 69 6E 69 74 3E——6个字节的字符串,对应:<init>

常量13:

01 00 16 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 49 29 56
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 16——这个UTF-8编码的常量字符串长度为22;
  • 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 49 29 56——22个字节的字符串,对应:(Ljava/lang/String;I)V

常量14:

01 00 04 43 6F 64 65
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 04——这个UTF-8编码的常量字符串长度为4;
  • 43 6F 64 65——4个字节的字符串,对应:Code

常量15:

01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 0F——这个UTF-8编码的常量字符串长度为15;
  • 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65——15个字节的字符串,对应:LineNumberTable

常量16:

01 00 03 69 6E 63
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 03——这个UTF-8编码的常量字符串长度为3;
  • 69 6E 63——3个字节的字符串,对应:inc

常量17:

01 00 03 28 29 49
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 03——这个UTF-8编码的常量字符串长度为3;
  • 28 29 49——3个字节的字符串,对应:()I

常量18:

01 00 0A 53 6F 75 72 63 65 46 69 6C 65
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 0A——这个UTF-8编码的常量字符串长度为10;
  • 53 6F 75 72 63 65 46 69 6C 65——10字节的字符串,对应:SourceFile

常量19:

01 00 0E 54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 0E——这个UTF-8编码的常量字符串长度为14;
  • 54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61——14字节的字符串,对应:TestClass.java

常量20:

0C 00 0C 00 1D // “lt;initgt;”:()V
  • 0C——tag值为12,类型为CONSTANT_NameAndType_info
  • 00 0C——#12 该字段或方法名称常量索引为12;
  • 00 1D——#29 该字段或方法描述符常量索引为29;

常量21:

0C 00 08 00 09 // age:I
  • 0C——tag值为12,类型为CONSTANT_NameAndType_info
  • 00 08——#8 该字段或方法名称常量索引为8;
  • 00 09——#9 该字段或方法描述符常量索引为9;

常量22:

0C 00 0A 00 0B // name:Ljava/lang/String;
  • 0C——tag值为12,类型为CONSTANT_NameAndType_info
  • 00 0A——#10 该字段或方法名称常量索引为10;
  • 00 0B——#11 该字段或方法描述符常量索引为11;

常量23:

07 00 1E // java/lang/System
  • 07——tag值为7,类型为CONSTANT_Class_info
  • 00 1E——#30 类型为“类或接口符号引用”,所以全限定名常量索引为30;

常量24:

0C 00 1F 00 20 // out:Ljava/io/PrintStream;
  • 0C——tag值为12,类型为CONSTANT_NameAndType_info
  • 00 1F——#31 该字段或方法名称常量索引为31;
  • 00 20——#32 该字段或方法描述符常量索引为32;

常量25:

07 00 21 // java/io/PrintStream
  • 07——tag值为7,类型为CONSTANT_Class_info
  • 00 21——#33 类型为“类或接口符号引用”,所以全限定名常量索引为33;

常量26:

0C 00 22 00 23 // println:(Ljava/lang/System;)V
  • 0C——tag值为12,类型为CONSTANT_NameAndType_info
  • 00 22——#34 该字段或方法名称常量索引为34;
  • 00 23——#35 该字段或方法描述符常量索引为35;

常量27:

01 00 16 63 6F 6D 2F 73 63 6F 2F 63 6F 72 65 2F 54 65 73 74 43 6C 61 73 73
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 16——这个UTF-8编码的常量字符串长度为22;
  • 63 6F 6D 2F 73 63 6F 2F 63 6F 72 65 2F 54 65 73 74 43 6C 61 73 73——22字节的字符串,对应:com/sco/core/TestClass

常量28:

01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 10——这个UTF-8编码的常量字符串长度为16;
  • 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74——16字节的字符串,对应:java/lang/Object

常量29:

01 00 03 28 29 56
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 03——这个UTF-8编码的常量字符串长度为3;
  • 28 29 56——3个字节的字符串,对应:()V

常量30:

01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 10——这个UTF-8编码的常量字符串长度为16;
  • 6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D——16字节的字符串,对应:java/lang/System

常量31:

01 00 03 6F 75 74
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 03——这个UTF-8编码的常量字符串长度为3;
  • 6F 75 74——3个字节的字符串,对应:out

常量32:

01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 15——这个UTF-8编码的常量字符串长度为21;
  • 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B——21个字节的字符串,对应:Ljava/io/PrintStream;

常量33:

01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 13——这个UTF-8编码的常量字符串长度为19;
  • 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D——19个字节的字符串,对应:java/io/PrintStream

常量34:

01 00 07 70 72 69 6E 74 6C 6E
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 07——这个UTF-8编码的常量字符串长度为7;
  • 70 72 69 6E 74 6C 6E——7个字节的字符串,对应:println

常量35

01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
  • 01——tag值为1,类型为CONSTANT_Utf8_info
  • 00 15——这个UTF-8编码的常量字符串长度为21;
  • 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56——21个字节的字符串,对应:(L/java/lang/String;)V

上边列举了例子中35个常量的字节码内容,可仔细去对照Class字节文件内容看看常量池的定义信息。常量池的详细信息相对比较繁琐因为每一种常量类型都对应了自己的一种结构,对照上边的详细内容结构表可解析每一个常量的类型、长度、详细内容是什么。

4.访问标志(access_flags)

访问标志:

00 21

该类的访问标志为:0x0021 = 0x0020 | 0x0001 = ACC_SUPER | ACC_PUBLIC

5.类索引、父类索引、接口索引

  • 类索引:引用于确定这个类的全限定名;
  • 父类索引:引用于确定这个类的父类的全限定名(因为Java语言不支持多继承,所有的类都继承于java.lang.Object,除了java.lang.Object类,所有类的父索引都不为0);
  • 接口索引集:接口索引的格式一般格式是:interfaces_count ( u2 ) + interfaces ( u2 ) * n;( n – interfaces_count ),这里interfaces_count表示当前类继承了多少接口,是接口计数器,后边每一个u2类型的正数就是每一个接口的接口索引;

TestClass示例:

00 06 00 07 00 00
  • 00 06:类索引为#6,值:#6 -> #27 -> com/sco/core/TestClass
  • 00 07:父类索引为#7,值:#7 -> #28 -> java/lang/Object
  • 00 00:因为TestClass类没有实现任何接口,所以接口索引集部分为00 00,并且紧随其后也没有任何字节描述

6.字段表集合

字段计数器:(当前类中有2个字段)

00 02

字段1:

00 82 00 08 00 09 00 00
  • 00 82:access_flags = 0x0080 | 0x0002 = ACC_TRANSIENT | ACC_PRIVATE
  • 00 08:name_index = #8,#8 -> age
  • 00 09:descriptor_index = #9,#9 -> I
  • 00 00:attributes_count:值为0,因为值为0,所以之后自然没有attribute_info部分

字段2:

00 02 00 0A 00 0B 00 00
  • 00 02:access_flags = 0x0002 = ACC_PRIVATE
  • 00 0A:name_index = #10,#10 -> name
  • 00 0B:descriptor_index = #11,#11 -> Ljava/lang/String;
  • 00 00:attributes_count:值为0,所以之后就没有attribute_info部分

7.方法表、Code属性

方法计数器:(当前类中有2个方法)

00 02
7.1.第一个方法

方法1(构造方法):

00 01 00 0C 00 0D 00 01
  • 00 01:access_flags = 0x0001 = ACC_PUBLIC
  • 00 0C:name_index = #12, #12 -> <init>
  • 00 0D:descriptor_index = #13,#13 -> (Ljava/lang/String;I)V
  • 00 01:attributes_count:值为1,所以紧随其后的就是attribute_info部分

方法1的Code(非指令部分):

00 0E 00 00 00 33 00 02 00 03 00 00 00 0F // 非指令部分
  • 00 0E:attribute_name_index = #14,#14 -> Code
  • 00 00 00 33:attribute_length = 33 -> 51,所以整个属性表的长度为51 + 6 = 57字节长度
  • 00 02:max_stack = 2
  • 00 03:max_locals = 3
  • 00 00 00 0F:code_length = 15

方法1的Code(指令部分):

2A B7 00 01 2A 1C B5 00 02 2A 2B B5 00 03 B1 // 指令部分
  • 2A B7 00 01
    2A -> aload_0:调用aload_0指令将第一个Reference类型的本地变量推送至栈顶,存储在第0个Slot中;
    B7 00 01-> invokespecial #1:调用超类构造方法、实例初始化方法、私有方法,invokespecial之后有一个u2类型的参数,对应<init>的符号引用
  • 2A 1C B5 00 02
    2A -> aload_0:调用aload_0指令将第一个Reference类型的本地变量推送至栈顶
    1C -> iload_2:调用iload_2将第三个int整型本地变量推送到栈顶;
    B5 00 02 -> putfield #2:调用putfield为指定的实例域赋值,00 02的常量为age的符号引用;
  • 2A 2B B5 00 03
    2A -> aload_0:调用aload_0指令将第一个Reference类型的本地变量推送至栈顶
    2B -> aload_1:调用aload_1指令将第二个Reference类型的本地变量推送至栈顶;
    B5 00 03 -> putfield #3:调用putfield为指定的实例域赋值,00 03的常量为name的符号引用;
  • B1:最后一个B1指令为:B1 -> return表示当前方法返回void,到这里构造函数就调用完成了;

方法1的Exception:

00 00 // 该方法没有throws部分的定义

方法1的Attribute Count:

00 01 // 方法1最后一部分有一个属性块

方法1的LineNumberTable:

00 0F 00 00 00 12 00 04 
00 00 00 07 00 04 00 08 00 09 00 09 00 0E 00 0A
  • 00 0F:attribute_name_index = #15,#15 -> LineNumberTable
  • 00 00 00 12:attribute_length = 14
  • 00 04:line_number_table_length = 4,表示这个LineNumberTable中有4条记录
  • 00 00 00 07 00 04 00 08 00 09 00 09 00 0E 00 0A:Source File -> Byte Code
    00 00 00 07 -> Source File( 7 ) : Byte Code ( 0 )
    00 04 00 08 -> Source File( 8 ) : Byte Code ( 4 )
    00 09 00 09 -> Source File( 9 ) : Byte Code ( 9 )
    00 0E 00 0A -> Source File( 14 ) : Byte Code ( 10 )

到这里构造函数的方法1部分的字节码就全部解析完了,接下来看看剩余部分的方法2的字节码。

7.2.第二个方法

方法2:

00 01 00 10 00 11 00 01
  • 00 01:access_flags = 0x0001 = ACC_PUBLIC
  • 00 10:name_index = #16, #16 -> inc
  • 00 11:descriptor_index = #17, #17 -> ()I
  • 00 01:attributes_count:值为1,紧随其后就是attribute_info

方法2的Code(非指令部分):

00 0E 00 00 00 2D 00 02 00 01 00 00 00 11
  • 00 0E:attribute_name_index = #14,#14 -> Code
  • 00 00 00 2D:attribute_length = 2D -> 45,所以整个属性表的长度为45 + 6 = 51字节长度
  • 00 02:max_stack = 2
  • 00 01:max_locals = 1
  • 00 00 00 11:code_length = 17

方法2的Code(指令部分):

B2 00 04 2A B4 00 03 B6 00 05 2A B4 00 02 04 60 AC // 指令部分
  • B2 00 04
    B2 00 04 -> getstatic #4:获取指定类的静态域,并且压入到栈顶,这里#4表示指定类的符号引用,为:java/lang/System.out:Ljava/io/PrintStream;
  • 2A B4 00 03
    2A -> aload_0:调用aload_0指令将第一个Reference类型的本地变量推送到栈顶

    B4 00 03 -> getfield #3:获取指定类的实例域,并且将其压入到栈顶,#3的符号引用为:com/sco/core/TestClass.name:Ljava/lang/String;,即TestClass的实例变量name;
  • B6 00 05
    B6 00 05 -> invokevirtual #5:调用实例方法,#5的符号引用为:java/io/PrintStream.println:(Ljava/lang/String;)V
  • 2A B4 00 02
    2A -> aload_0:调用aload_0指令将第一个Reference类型的本地变量推送到栈顶

    B4 00 02 -> getfield #2:获取指定类的实例域,并且将其压入到栈顶,#2的符号引用为:com/sco/core/TestClass.age:I
  • 04 60 AC
    04 -> iconst_1:将int类型的1推送到栈顶
    60 -> iadd:将栈顶两个int类型的值相加,返回结果重新推送到栈顶
    AC -> ireturn:从当前方法返回int值

方法2的Exception:

00 00 // 该方法没有throws部分的定义

方法2的Attribute Count:

00 01 // 方法1最后一部分有一个属性块

方法2的LineNumberTable:

00 0F 00 00 00 0A 00 02
00 00 00 0D 00 0A 00 0E
  • 00 0F:attribute_name_index = #15,#15 -> LineNumberTable
  • 00 00 00 0A:attribute_length = 10
  • 00 02:line_number_table_length = 2,表示这个LineNumberTable中有2条记录
  • 00 00 00 0D 00 0A 00 0E:Source File -> Byte Code

    00 00 00 0D -> Source File( 13 ) : Byte Code ( 0 )

    00 0A 00 0E -> Source File( 14 ) : Byte Code ( 10 )

8.SourceFile属性

00 01 // 方法区过后当前Class文件也会包含attribute属性信息,当前Class文件还有1个属性
00 12 00 00 00 02 00 13
  • 00 12:attribute_name_index = #18,#18 -> SourceFile
  • 00 00 00 02:attribute_length = 2
  • 00 13:sourcefile_index = #19, #19 -> TestClass.java

到这里com.sco.core.TestClass这个类的字节码文件就全部解析完成了。

参考书籍:《深入理解Java虚拟机》,作者:周志明

Java字节码.class文件案例分析