且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Java Class字节码知识点回顾

更新时间:2022-08-14 10:07:00

把之前的笔记重新整理了一下,发上来供对java Class文件结构的有兴趣的同学参考一下,也算对以前知识的回顾。

Java Class文件打破了C或者C++等语言所遵循的传统,用这些传统语言写的程序通常首先被编译,然后被连接成单独的、专门支持特定硬件平台和操作系统的二进制文件。通常情况下,一个平台上的二进制可执行文件不能在其他平台上工作。

Java Class文件是可以运行在任何支持Java虚拟机的硬件平台和操作系统上的二进制文件,Class文件中包含了java虚拟机指令集和符号表以及若干其他辅助信息。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将java虚拟机作为语言的产品交付媒介。例如,使用java编译器可以把java代码编译成存储字节码的class文件,使用grooy等其他语言的编译器一样可以把程序代码编译成class文件,虚拟机并不关心class的来源是何种语言。

Java Class字节码知识点回顾

class类文件的结构

class文件是一组以8位字节位基础单位的二进制流,采用一种类似c语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数。无符号数属于基本的数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或utf-8编码构成的字符串值。表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性地以_info结尾。

每一个 Class 文件对应于一个如下所示的 ClassFile 结构体:

ClassFile {
    u4 magic;                    //魔数
    u2 minor_version;            //副版本号    
    u2 major_version;            //主版本号
    u2 constant_pool_count;        //常量池计数器,
    cp_info constant_pool[constant_pool_count-1]; //常量池列表    
    u2 access_flags;            //访问标志    
    u2 this_class;                //类索引,表示这个Class文件所定义的类或接口    
    u2 super_class;                //父类索引
    u2 interfaces_count;        //接口计数器    
    u2 interfaces[interfaces_count];    //接口表,接口顺序和源代码顺序一致    
    u2 fields_count;                    //字段计数器    
    field_info fields[fields_count];    //字段表    
    u2 methods_count;                    //方法计数器    
    method_info methods[methods_count];    //方法表
    u2 attributes_count;                //属性计数器    
    attribute_info attributes[attributes_count];    //属性表
}

借用下之前同学的图能更清晰地表达:
Java Class字节码知识点回顾

引例

本文以TestClassCode.class源码为例展开,TestClassCode.java对应的java代码如下:


package com.test.doc.exp;

public class TestClassCode {

    private String    attribute_1;

    protected Integer attribute_2;

    public void testInterface_1() {

    }

    public String testInterface_2(String param) {
        return param;
    }

}

TestClassCode.class对应的字节码如下:

2f54 6573 7443 6c61 7373 436f 6465 0700 0401 0010 6a61 7661 2f6c 616e 672f 4f62
6a65 6374 0100 0b61 7474 7269 6275 7465 5f31 0100 124c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b01 000b 6174 7472 6962 7574 655f 3201 0013 4c6a 6176 612f 6c61
6e67 2f49 6e74 6567 6572 3b01 0006 3c69 6e69 743e 0100 0328 2956 0100 0443 6f64
650a 0003 000d 0c00 0900 0a01 000f 4c69 6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162 6c65 0100 0474 6869 7301 0020 4c63 6f6d
2f74 6573 742f 646f 632f 6578 702f 5465 7374 436c 6173 7343 6f64 653b 0100 0f74
6573 7449 6e74 6572 6661 6365 5f31 0100 0f74 6573 7449 6e74 6572 6661 6365 5f32
0100 2628 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 294c 6a61 7661 2f6c 616e
672f 5374 7269 6e67 3b01 0005 7061 7261 6d01 000a 536f 7572 6365 4669 6c65 0100
1254 6573 7443 6c61 7373 436f 6465 2e6a 6176 6100 2100 0100 0300 0000 0200 0200
0500 0600 0000 0400 0700 0800 0000 0300 0100 0900 0a00 0100 0b00 0000 2f00 0100
0100 0000 052a b700 0cb1 0000 0002 000e 0000 0006 0001 0000 0003 000f 0000 000c
0001 0000 0005 0010 0011 0000 0001 0012 000a 0001 000b 0000 002b 0000 0001 0000
0001 b100 0000 0200 0e00 0000 0600 0100 0000 0b00 0f00 0000 0c00 0100 0000 0100
1000 1100 0000 0100 1300 1400 0100 0b00 0000 3600 0100 0200 0000 022b b000 0000
0200 0e00 0000 0600 0100 0000 0e00 0f00 0000 1600 0200 0000 0200 1000 1100 0000
0000 0200 1500 0600 0100 0100 1600 0000 0200 17```

## class文件结构分析字节码

### 魔数

class文件对应的字节码:```cafe babe```,每个class文件的头4个字节称为```魔数```(cafe babe),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件。

### 版本号

class文件对应的字节码:```0000 0032```,第5、6个字节是次版本号(minor_version),第7、8个字节是主版本号(major_version)。java的版本号是从45开始的,jdk1.1之后的每个jdk大版本发布主版本号向上+1,高版本的jdk能向下兼容以前版本的class文件,但不能运行以后版本的class文件,即使文件格式发生任何变化,虚拟机也拒绝执行超过其版本号的class文件。
本例中次版本号:```0x0000```,主版本号:```0x0032```,代表编译器使用jdk1.6.0_1版本。

### 常量池计数器、数据区

常量池是class文件中非常重要的结构,它描述着整个class文件的字面量信息,可以理解为class文件之中的资源仓库。常量池是由一组 constant_pool 结构体数组组成的,而数组的大小则由常量池计数器指定。常量池计数器 constant_pool_count 的值 =constant_pool表中的成员数 + 1。constant_pool表的索引值只有在大于 0 且小于 constant_pool_count时才会被认为是有效的。

本例中常量池容量为```0x0018```,代表十进制数24,常量池中有23项常量,所以范围为1-23。在class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以把索引值置为0来表示。** class文件结构中只有常量池的容量计数是从1开始 **。

接下来存储的是常量池数据区,主要存放两大类常量:字面量和符号引用,常量池中每一个常量都是一个表,共有14中不同结构的表结构数据:

| 类型 | 标志 | 描述 |
| --- | --- | --- |
| 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_Fielddef_info | 9 | 字段的符号引用 | 
| CONSTANT_Methoddef_info | 10 | 类中方法的符号引用 | 
| CONSTANT_InterfaceMethoddef_info | 11 | 接口中方法的符号引用 | 
| CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 | 
| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 | 
| CONSTANT_MethodType_info | 16 | 表示方法类型 | 
| CONSTANT_InvokeDynamic_info | 18 | 表示方法句柄 | 

之所以说常量池是最繁琐的数据,是因为常见的11个常量各自均有自己的结构,见下图:

![3](http://img4.tbcdn.cn/L1/461/1/dd451bd9356e24945cd06dc7b2eed9a372714184)

以TestClassCode.java为例,格式化展示对应的常量区字节码:
![_](http://img3.tbcdn.cn/L1/461/1/3aae9c8c5ac72e932002ac4d05fc14f1c15dde8f)

第一项常量值:```0x07 0002```,它的标志位是```0x07```,属于CONSTANT_Class_info,此类型代表一个类或接口的符号引用。```0x0002```,是它的符号引用,指向常量池的第二项常量。

第二项常量值:```0x01 001e 636f 6d2f 7465 7374 2f64 6f63 2f65 7870 2f54 6573 7443 6c61 7373 436f 6465``` ,它的标志位是```0x01```,属于CONSTANT_Utf8_info,此类型代表一个UTF-8编码的字符串,长度为```001e```即十进制数30,字符串值为```0x636f 6d2f 7465 7374 2f64 6f63 2f65 7870 2f54 6573 7443 6c61 7373 436f 6465```,对应为:```com/test/doc/exp/TestClassCode```。

以此方法,我们可以依次得到剩下的常量池数据区,当然也可以使用javap -verbose TestClassCode命令,输出TestClassCode.class对应的常量池区域:

Constant pool:
#1 = Class #2 // com/test/doc/exp/TestClassCode
#2 = Utf8 com/test/doc/exp/TestClassCode
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 attribute_1
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 attribute_2
#8 = Utf8 Ljava/lang/Integer;
#9 = Utf8
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Methodref #3.#13 // java/lang/Object."":()V
#13 = NameAndType #9:#10 // "":()V
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/test/doc/exp/TestClassCode;
#18 = Utf8 testInterface_1
#19 = Utf8 testInterface_2
#20 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#21 = Utf8 param
#22 = Utf8 SourceFile
#23 = Utf8 TestClassCode.java


### 访问标志

常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或接口层次的访问信息。具体的标志位以及标志含义见下表:
![类访问标志](http://img1.tbcdn.cn/L1/461/1/b998fa8ec85c473970e1e376ff2fbe9de2456957)

本例中访问标志为```0x0021```,它的ACC_PUBLIC,ACC_SUPER标志应当为真,而其他标志为假。

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

类索引和父类索引都是一个u2类型的数据,而结构索引集合是一组u2类型的数据集合,class文件由这三项数据来确定这个类的父类的全限定名。对于接口索引集合的第一项是接口计数器,如果没有实现任何接口,该计数器为0。

本例中,类索引对应```0x0001```,即类索引指向常量池第一个变量,常量池对应为CONSTANT_Class_info的类描述符常量,再通过该变量的索引值找到定义在CONSTANT_Utf8_info类型中的全限定名字符串```com/test/doc/exp/TestClassCode```。父类索引为```0x0003```,值为```java/lang/Object```。接口索引集合为```0x0000```,即当前不实现任何接口。

### 字段表集合

字段表集合用于描述接口或类中声明的变量,是指由若干个字段表(field_info)组成的集合。对于在类中定义的若干个字段,经过JVM编译成class文件后,会将相应的字段信息组织到一个叫做字段表集合的结构中,字段表集合是一个类数组结构,会将类中定义的字段个数设到```字段计数器```中,然后每个字段信息组成一个```field_info```结构,依次存储到字段计数器之后。

field_info结构如下:

struct field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes;
}


access_flags是一个u2的数据类型,字段访问标志包括:ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED、ACC_STATIC、ACC_FINAL、ACC_VOLATILE、ACC_TRANSIENT、ACC_SYNTHETIC、ACC_ENUM。
![字段访问标志](http://img2.tbcdn.cn/L1/461/1/4c2af4e3dbaa7450307da52d3f277193f82a86a9)

跟随access_flags标志的是两个索引值:```name_index```和```descriptor_index```。他们都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。本例中字段表集合值对应```0x0002 0002 0005 0006 0000 0004 0007 0008 0000```,字段表计数器```0x0002```,代表当前类包含2个字段。access_flags对应```0x0002```,代表字段为private属性。name_index为```0x0005```,指向常量池第五个常量,即attribute_1。descriptor_index为```0x0006```,指向常量池第六个常量,即```Ljava/lang/String;``` 。随后紧跟的```0x0000```代表该字段没有对应的属性。

字段表集合中不会列出从超类或父接口中继承而来的字段,但由可能列出原本java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

### 方法表与属性表集合
方法表集合是指由若干个方法表(method_info)组成的集合。对于在类中定义的若干个,经过JVM编译成class文件后,会将相应的method方法信息组织到一个叫做方法表集合的结构中,字段表集合是一个类数组结构,如下:

struct method {
u2 method_counts;
method_info[] method_infos;
}


方法表结构method_info如下:

struct method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes;
}


这个结构跟field_info结构几乎完全一致,但因为volatile关键字和transient关键字不能修饰方法,所以方法表中的访问标志没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的方法表中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP、ACC_ABSTRACT。

| 标志名称 | 标志值 | 含义 |
| --- | --- | --- |
| 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_STRICTFP | 0x0800 | 是否为strictfp |
| ACC_SYNTHETIC | 0x1000 | 是否为编译器自动产生 |

方法的名称索引、描述索引与字段类似,方法里的代码经过编译器编译成字节码指令后,存放在方法属性表集合中。在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与Class文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

属性表的通用结构如下:

struct attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}


目前jvm常见的预定义属性包括:
![预定义属性](http://img1.tbcdn.cn/L1/461/1/cf5b7ed6d932862da032542f09ffc518a2408003)

下面详细说明下Code、LineNumberTable、LocalVariableTable属性。

### Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变成字节码指令存储在code属性内。Code属性表的结果如下:

struct Code {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
exception_info exception_table;
u2 attribute_count;
attribute_info attributes;
}


```attribute_name_index```是一项指向常量表的索引,常量值固定为“Code”,它代表了该属性的属性名称,```attribute_length```指示了属性值的长度,由于属性名称索引与属性长度一共是6个字节,所以属性值的长度固定为整个属性表的长度减去6个字节。```max_stack```代表了操作数栈(Operand Stacks)的最大深度。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Frame)中的操作数栈深度。```max_locals```代表了局部变量表所需的存储空间。在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量表分配内存所使用的最小单位。

对于byte,char,float,int,shot,boolean,reference和returnAddress等长度不超过32位的数据类型,每个局部变量占1个Slot,而double与long这两种64位的数据类型而需要2个Slot来存放。** 方法参数(包括实例方法中的隐藏参数“this”),显示异常处理器的参数(Exception Handler Parameter,即try-catch语句中catch块所定义的异常),方法体中定义的局部变量都需要使用局部表来存放 ** 。

另外,** 并不是在方法中使用了多个局部变量,就把这些局部变量所占的Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所在的Slot就可以被其他局部变量所使用,编译器会根据变量的作用域来分类Slot并分配给各个变量使用,然后计算出max_locals的大小** 。 

```code_length```和```code```用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。Code属性是Class文件中最重要的一个属性,如果表一个Java程序中的信息分为代码(Code,方法体里的Java代码)和元数据(Metadata,包括类、字段、方法定义及其它信息)两部分,那么在整个Class文件里,Code属性用于描述代码,其它的所有数据项目就都用于描述元数据。

### LineNumberTable属性
LineNumberTable属性用于描述Java源代码行号与字节码行号(字节码偏移量)之间的对应关系。它并不是运行时必须的属性。如果选择不生成LineNumberTable属性表,对程序运行产生的最主要的影响就是在抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候无法按照源码来设置断点。

struct LineNumberTable {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
line_number_info line_number_table;
}


其中,```line_number_table```是一个数量为line_number_table_length,类型为line_number_info的集合,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。

### LocalVariableTable属性
LocalVariableTable属性表用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。LocalVariableTable属性表结构如下:

![LocalVariableTable](http://img1.tbcdn.cn/L1/461/1/c6eeddc91207f6fa8950529bc1b998b90edd0182)

### 例

说完那么多概念,我们回到方法表字节码:

```0003 0001 0009 000a 0001 000b 0000 002f 0001 0001 0000``` 

```0005 2ab7 000c b100 0000 0200 0e00 0000 0600 0100 0000 0300```

```0f00 0000 0c00 0100 0000 0500 1000 1100 0000 0100 1200 0a```

我们从头开始说明这块字节码的意思:```0x0003```代表方法表中包含3个方法,接下来的```0x0001 0009 000a```依次代表第一个方法的访问属性```public```、名称索引对应```<init>```、描述符索引```()V```。

```0x0001```代表属性表中有一个属性,```0x000b```对应常量池中的字符串```Code```,代表该属性为Code类型,```0x0000 002f```代表下面的code属性表长度为47个字节。```0x0001```栈深,```0x0001```局部变量表大小,```0x0000 0005```代码长度,```0x2ab7 000c b1```代码区域,```0x0000```异常表,```0x0002```属性表个数,```0x000e```指向常量池字符串LineNumberTable,```0x0000 0006```属性长度6个字节,```0x0001```行号表长度为1,```0x0000 0003```代表字节码行号为0,对应的java源码行号为3。