类文件结构详解下篇
# 前言
在类文件结构详解上篇一文中,我们介绍了class文件的构成,整个class文件一共包含3部分共16个属性:
- 3个描述文件属性的数据项:魔数和主次版本号
- 11个描述类属性的数据项:类、字段、方法等信息
- 2个描述代码属性的数据项:属性表,描述方法体内的具体内容
其中文件属性和类属性在上一篇中已经有过介绍,本文将主要介绍一下属性表。
后续的讲解我们会使用下面的类作为样例,附上代码:
package com.hukai.demo.bytecode;
import java.beans.Transient;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 属性测试
*
* @author hukai
*/
public class AttributeDemo {
// 被final修饰并且直接赋值,数据类型为基本类型或字符串类型,生成ConstantValue属性进行初始化
private final int anInt = 3;
// 被final修饰并且直接赋值,数据类型不为基础类型,在实例构造器`init`方法里初始化
private final Date anDate = new Date();
// 在`clinit`方法(静态代码块)里初始化
public static final int bnInt = 4;
@Deprecated
public static final Date bnDate = new Date();
@Transient
public String convertToString(String dateString) throws RuntimeException, ParseException {
try {
if (dateString == null) {
throw new NullPointerException("字符串为空");
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse(dateString);
return String.valueOf(date.getTime());
} catch (NullPointerException | ArrayIndexOutOfBoundsException e){
throw new RuntimeException("出现异常");
}
}
class Sms {
private String phone;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
}
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
# 属性表结构
属性表(attribute_info)在前面的讲解之中已经出现过数次,在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于推述某些场景专有的信息。其结构如下图:

对于每个属性(attribute_info),它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占的位数即可。一个符合规则的属性表应该满足下表中所定义的结构:
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | attribute_name_index | 1 |
| u4 | attribute_length | 1 |
| u1 | info | attribute_length |
为了能正确解析Class文件,Java虚拟机规范中预定义了21项虛拟机实现应当能识别的属性,下文中将对其中一些属性中的关键常用的部分进行讲解。
| 属性名称 | 使用位置 | 含义 |
|---|---|---|
| Code | 方法表 | Java代码编译成的字节码指令 |
| ConstantValue | 字段表 | final关键字定义的常量值 |
| Deprecated | 类文件、字段表、方法表 | 被声明为deprecated的方法和字段 |
| Exceptions | 方法表 | 方法抛出的异常 |
| InnerClasses | 类文件 | 内部类列表 |
| LineNumberTale | Code属性 | Java源码的行号与字节码指令的对应关系 |
| LocalVariableTable | Code属性 | 方法的局部变量描述 |
| SourceFile | 类文件 | 源文件名称 |
| Synthetic | 类文件、方法表、字段表 | 标识方法或字段是由编译器自动生成的 |
# Code属性
# 属性详解
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。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。代表了该属性的属性名称
- attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节
- max_stack表示操作数栈的最大深度,jvm运行时会根据这个值来分配栈帧中的操作数栈深度
- max_locals表示局部变量表所需要的存储空间,单位为slot。并不是所有局部变量占用的slot之和,当一个局部变量的生命周期结束后,其所占用的slot将分配给其它依然存活的局部变量使用,按此方式计算出方法运行时局部变量表所需的存储空间
- code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。那么每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个宇节码代表的是什么指令.并且可以知道这条指令后面是否需要跟随参数,以及参数该如何理解
- exception_table_length表示异常表占用的字节数
- exception_table表示具体的异常表
- Code属性本身还有自己的一些属性表,包括LineNumberTable、LocalVariableTable和StackMapTable,这些属性不是必须的,如果有的话,会在attributes_count和attributes中体现出来
tips:Slot,虚拟机为局部变量分配内存所使用的最小单位,长度不超过32位的数据类型占用1个Slot,64位的数据类型(long和double)占用2个Slot
# 实例解析
使用javap -v命令解析字节码内容,查看convertToString方法的解析结果如下:

stack=3, locals=4分别代表了max_stack=3和max_locals=4,即操作数栈的最大深度为3,局部变量表所需要的存储空间为4 slot。
arg_size代表参数个数。需要注意的是如果方法不是static修饰的,会自带一个this参数,此时arg_size=入参个数+1。虚拟机在解析class文件中的方法时,会判断参数数量args_size是否大于MAX_ARGS_SIZE,如果大于则就会报错了。MAX_ARGS_SIZE为255。
后续为编译出的字节码指令,在此不做详解。
最后为exception_table属性,很明显他对应着try catch这种东西,如果你的代码里没有try catch代码块,是不会生成exception_table的,它的结构如下:
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | start_pc | 1 |
| u2 | end_pc | 1 |
| u2 | handler_pc | 1 |
| u2 | catch_type | 1 |
start_pc和end_pc划分了try{},其对应字节码指令的行号,而catch_type代表了catch(exception)里面的那个参数exception,如果抓到异常就转到handler_pc处理。
# Exceptions属性
# 属性详解
这里的Exceptions属性是在方法表中与Code属性平级的一项属性,不是前面讲的异常表。Exceptions属性的作用是列举出方法中可能抛出的受査异常,也就是方法描述时在throws关键字后面列举的异常。结构如下:
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | attribute_name_index | 1 |
| u4 | attribute_length | 1 |
| u2 | number_of_exceptions | 1 |
| u2 | exception_index_table | number_of_exceptions |
- number_of_exceptions表示可能抛出number_of_exceptions种 受查异常
- exception_index_table为异常索引集合,一组u2类型 exception_index的集合,每一个exception_index为一个指向常量池中一CONSTANT_Class_info型常量的索引,代表该受查异常的类型
# 实例解析
使用IDEA的jclasslib插件查看字节码信息,可以看到convertToString方法的Exceptions属性分析结果如下:

可以看到声明了RuntimeException和ParseException两个异常,皆为throws关键字后面列举的异常。
# LineNumberTable属性
# 属性详解
LineNumberTable属性用于描述Java源码行号与字节码行号之间的对应关系,它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。
如果选择不生成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_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info结构如下:
| 类型 | 名称 | 数量 | 含义 |
|---|---|---|---|
| u2 | start_pc | 1 | 字节码偏移量 |
| u2 | line_number | 1 | java源文件行号 |
# 实例分析
使用jclasslib插件查看convertToString方法的Code属性下的LineNumberTable属性如下:

# LocalVariableTable属性
# 属性详解
LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。
如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE将会使用诸如arg0、argl之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。其结构如下表:
| 类型 | 名称 | 数量 |
|---|---|---|
| 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 | 变量在栈帧中slot的位置 |
# 实例分析
查看convertToString方法的Code属性下的LocalVariableTable属性如下:

可以看到虽然我们方法体中没有用到this关键字,但是LocalVariableTable中还是有this这个局部变量,其作用范围是整个方法体。当然,静态方法是不会有this这个局部变量的。
# SourceFile属性
SourceFile属性用于记录生成这个class文件的源码文件名称。这个属性也是可选的,可以分别使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息。
对大多数文件,类名和文件名是一致的,少数特殊类除外(如:内部类),如果不生成这项属性,抛出异常时,堆找中将不会显示出错代码所属的文件名。这是一个定长的属性,结构如下表:
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | attribute_name_index | 1 |
| u4 | attribute_length | 1 |
| u2 | sourcefile_index | 1 |
sourcefile_index是指向常量池中一CONSTANT_Utf8_info类型常量的索引,常量的值为源码文件的文件名。实例如下:

# ConstantValue属性
# 属性详解
ConstantValue属性的作用是通知虚拟机自动为常量赋值。我个人对这个属性的理解是:当某个类变量被final修饰并且直接赋值,并且该变量的数据类型为基本类型或字符串类型,就生成ConstantValue属性进行初始化。如果变量不是基本类型或字符串类型,则在实例构造器init方法里初始化,若变量还被static修饰,则在静态初始化方法clinit里初始化。属性结构如下表:
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | attribute_name_index | 1 |
| u4 | attribute_length | 1 |
| u2 | constantvalue_index | 1 |
constantvalue_index数据项代表了常量池中一个字面量常量的引用,它指向的应该是一个基本数据类型常量或者CONSTANT_String_info常量中的一种。
# 实例分析
AttributeDemo类中定义了四个字段,查看字节码分析结果如下:

可以看到只有两个字段(anInt、bnInt)生成了ConstantValue属性,它们的共同特点是都被final修饰且为基本类型。
# 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 |
数据项number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一个inner_classes_info表进行描述。inner_classes_info表的结构如下表:
| 类型 | 名称 | 数量 | 含义 |
|---|---|---|---|
| u2 | inner_class_info_index | 1 | 指向内部类CONSTANT_Class_info类型常量索引 |
| u4 | outer_class_info_index | 1 | 指向宿主类CONSTANT_Class_info类型常量索引 |
| u2 | inner_name_index | 1 | 索引值。指向内部类名称,如果为匿名内部类,则该值为0 |
| u2 | inner_name_access_flags | 1 | 类似于access_flags,是内部类的访问标志 |
实例分析如下:

# Deprecated及Synthetic属性
Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没存的区别,没有属性值的概念。
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@dcprecated注释进行设置。
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。在JDK1.5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的ACC_SYNTHETIC标志位。
Deprecated和Synthetic属性的结构非常简单,如下表:
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | attribute_name_index | 1 |
| u4 | attribute_length | 1 |