字节码详解

源代码经过javac编译器编译之后便会生成字节码文件,字节码以二进制的方式存储,无法直接用记事本打开阅读。

1. 字节码查看工具

1.1 安装jclasslib工具

查看二进制字节码,不推荐使用java自带的反解析命令行工具javap,推荐可视化更好的jclasslib。打开IDEA,在plugins搜索jclasslib即可完成安装:
Alt text 需要查看字节码文件,先选中文件,然后点击View->Show Bytecode With Jclasslib:
Alt text

1.2 使用arthas工具

如果在生产上,需要查看字节码,可以使用arthas工具的jad 全类名命令:
Alt text

2. 字节码的组成

Alt text

3. 基础信息

Alt text

3.1 Magic魔数

软件使用文件的头几个字节(文件头)去校验文件的类型,如果软件不支持该种类型就会出错。Java字节码文件中,将文件头称为magic魔数。 Alt text

提示

使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

3.2 文件版本号

紧接着魔数的4个字节存储的是Class文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version。

主版本(十进制) 副版本(十进制) 编译器版本
45 3 1.1
46 0 1.2
47 0 1.3
48 0 1.4
49 0 1.5
50 0 1.6
51 0 1.7
52 0 1.8
53 0 1.9
54 0 1.10
55 0 1.11

提示

jdk版本=主版本号 – 44(比如主版本号52就是JDK8)

4. 常量池

常量池(constant_pool)的作用:避免相同的内容重复定义,节省空间。常量池中存放编译时期生成的各种字面量和符号引用

4.1 常量池计数器(constant_pool_count)

常量池中常量数量不是一个固定数量,为了方便管理,引入了常量池计数器(u2类型):从1开始,比如constant_pool_count=1表示常量池中有0个常量项。查找常量的时候,常量池类似数组结构,可以使用常量池计数器作为数组下标索引表示。
Alt text

为何常量池计数器不是从0开始的?

常量池计数器从1开始,因为它把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的含义,这种情况可用索引值0来表示。

4.2 常量池存储内容

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

常量 具体的常量
字面量 文本字符串
声明为final的常量值
符号引用 类和接口的全限定名
字段的名称和描述符
方法的名称和描述符

两大类里面也分具体的常量类型和结构(详细一点👀),可以分为class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。如下表:

类型 标志(或标识) 描述
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 表示一个动态方法调用点

5. 字段(表)

字段表(fields)用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量(local variables)。字段叫什么名字、字段被定义为什么数据类型,这些并没有直接保存到字段里面,而是引用常量池中的常量来表示(问就是它是JVM规范规定的)。

5.1 字段计数器

字段计数器(fields_count)的值表示当前class文件fields表的成员个数。使用两个字节来表示。 Alt text

5.2 字段表存储内容

一个字段的信息通过索引指向常量池,包括如下这些信息。这些信息中,各个修饰符不一定都有:

  • 作用域(public、private、protected修饰符)
  • 是实例变量还是类变量(static修饰符)
  • 可变性(final)
  • 并发可见性(volatile修饰符,是否强制从主内存读写)
  • 可否序列化(transient修饰符)
  • 字段数据类型(基本数据类型、对象、数组)
  • 字段名称

6. 方法(表)

方法表(methods)指向常量池索引集合,它完整描述了每个方法的信息,方法的信息包括方法的访问修饰符(public、private或protected),方法的返回值类型以及方法的参数信息等。如果方法是抽象或者native,则不会出现在方法表中,也不不包括从父类或父接口继承的方法。此外编译器额外产生两个方法:类(接口)初始化方法<clinit>()和实例初始化方法<init>()。

6.1 方法计数器

methods_count的值表示当前class文件methods表的成员个数。使用两个字节来表示。 Alt text

6.2 方法存储内容

method_info不仅存储某个方法的完整信息,而且包含字节码指令。方法表结构如下:

类型 名称 含义 数量
u2 access_flags 访问标志 1
u2 name_index 方法名索引 1
u2 descriptor_index 描述符索引 1
u2 attributes_count 属性计数器 1
attribute_info attributes 属性集合 attributes_count

6.3 字节码指令

字节码指令的内容存放在方法的Code属性中,也就是说用到了方法表结构里面属性表(attribute_info),其属性类型是Code, 它是虚拟机中预定义的属性。Code属性代表的就是存放方法体里面的代码。具体内容可以参见[]

7. 属性表

指的是class文件所携带的辅助信息,比如该class文件的源文件的名称。属性表集合的限制没有那么严格,任何编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。

7.1 属性计数器

attributes_count的值表示当前class文件属性表的成员个数。属性表中每一项都是一个attribute_info结构。

7.2 属性表存储内容

属性的具体内容可以去自定义。虚拟机中预定义的属性如下:

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量池
Deprecated 类,方法,字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数所需要的类是否匹配

评论