03_解析class文件

解析class文件

Java 虚拟机规范中所指的 class 文件,并非特指位于磁盘中的 .class 文件,而是泛指任何格式符合规范的 class 数据。它实际上可以通过网络下载,从数据库加载,甚至是在运行中直接生成等方式来获取 class 文件。

  • 构成 class 文件的基本数据单位是字节,可以把整个 class 文件当成一个字节流来处理
  • 数据由连续多个字节构成,这些数据在 class 文件中以大端(big-endian)方式存储

为了描述 class 文件格式,Java 虚拟机规范定义了 u1、u2 和 u4 三种数据类型来表示1、2和4字节无符号整数。

  • 相同类型的多条数据一般按表(table)的形式存储在 class 文件中
  • 表由表头和表项(item)构成,表头是 u2 或 u4 整数

整个 class 文件被描述为一个 ClassFile 结构,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
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];
}

一、前期准备

1.1 自定义数据类型

Java 虚拟机规范定义了 u1、u2 和 u4 三种数据类型来表示1、2和4字节无符号整数。

但是 Java 中都是有符号整数,没有无符号整数,所以这里先定义几种无符号整数类型,实际上它们是由更大范围的整数值来保存的:

1
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
/**
* 8比特无符号整数
*/
public class Uint8 {

private int val;

public Uint8(int val) {
this.val = val;
}

}

/**
* 16比特无符号整数
*/
public class Uint16 {

private int val;

public Uint16(int val) {
this.val = val;
}
}

/**
* 32比特无符号整数
*/
public class Uint32 {

private long val;

public Uint32(long val) {
this.val = val;
}

}

使用自定义类型的原因是,方便在定义 ClassFile 类时,各个成员变量的类型能更清晰一些,不然都是 int、long 这些类型的话,都不知道 ClassFile 中实际保存的字节数量。

这样增加自定义类型后,下面的 ClassFile 结构就稍微好看一点了:

1
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
public class ClassFile {

/**
* 魔数
*/
private Uint32 magic;
/**
* 次版本号
*/
private Uint16 minorVersion;
/**
* 主版本号
*/
private Uint16 majorVersion;
/**
* 常量池常量数量
*/
private Uint16 constantCount;
/**
* 常量池
*/
private ConstantPoolInfo constantPool;
/**
* 类访问标志
*/
private Uint16 accessFlags;
/**
* 类名
*/
private Uint16 className;
/**
* 父类名
*/
private Uint16 superClassName;
/**
* 接口数量
*/
private Uint16 interfaceCount;
/**
* 接口定义
*/
private Uint16[] interfaces;
/**
* 字段数量
*/
private Uint16 fieldCount;
/**
* 字段定义
*/
private FieldInfo[] fields;
/**
* 方法数量
*/
private Uint16 methodCount;
/**
* 方法定义
*/
private MethodInfo[] methods;
/**
* 属性数量
*/
private Uint16 attributesCount;
/**
* 属性定义
*/
private AttributeInfo[] attributes;

}

1.2 类型数据读取

现在是把 class 文件当成字节流来处理,但是如果直接操作字节是很不方便的。而且前面增加了自定义数据类型,把数据读出来后,还要再转成对应的数据类型,相当麻烦。

所以定义了一个工具类 ClassReader 来帮助读取数据:

1
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
public class ClassReader {

private ByteBuffer buf;
private int offset;

ClassReader (byte[] bytes) {
buf = ByteBuffer.wrap(bytes);
buf.order(ByteOrder.BIG_ENDIAN); // 大端
offset = 0;
}

public int readInt() {
return buf.getInt();
}

public long readLong() {
return buf.getLong();
}

public float readFloat() {
return buf.getFloat();
}

public double readDouble() {
return buf.getDouble();
}

public Uint8 readUint8() {
int val = buf.get();
return new Uint8(val);
}

public Uint16 readUint16() {
short s = buf.getShort();
int val = 0x0FFFF & s;
return new Uint16(val);
}

public Uint32 readUint32() {
int i = buf.getInt();
long val = 0x0FFFFFFFFL & i;
return new Uint32(val);
}

public Uint16[] readUint16s(Uint16 length) {
Uint16[] table = new Uint16[length.value()];
for (int i = 0; i < table.length; i++) {
table[i] = readUint16();
}
return table;
}

public byte[] readBytes(int length) {
byte[] bytes = new byte[length];
buf.get(bytes);
offset += length;
return bytes;
}

}

工具类底层是用 ByteBuffer 来实现的,它提供了很多有用的方法来读取指定类型的数据,基本上可以直接使用。

二、类文件数据结构

2.1 魔数

很多文件格式都会规定满足该格式的文件必须以某几个固定字节开头,这几个字节主要起标识作用,叫作魔数(magic number)。

class 文件的魔数是 0xCAFEBABE

1
magic = reader.readUint32();

Java 虚拟机规范规定,如果加载的 class 文件不符合要求的格式,Java 虚拟机实现就抛出 java.lang.ClassFormatError 异常。

2.2 版本号

魔数之后是 class 文件的次版本号和主版本号,都是u2类型。

1
2
minorVersion = reader.readUint16();
majorVersion = reader.readUint16();

次版本号只在 J2SE 1.2 之前用过,从1.2开始基本上就没什么用了(都是0)。

主版本号在 J2SE 1.2 之前是45,从1.2开始,每次有大的 Java 版本发布,都会加1。

Java 版本 class 文件版本号
JDK 1.0.2 45.0 ~ 45.3
JDK 1.1 45.0 ~ 45.65535
J2SE 1.2 46.0
J2SE 1.3 47.0
J2SE 1.4 48.0
Java SE 5.0 49.0
Java SE 6 50.0
Java SE 7 51.0
Java SE 8 52.0

特定的 Java 虚拟机实现只能支持版本号在某个范围内的 class 文件。

例如,Java 8 支持版本号为 45.0 ~ 52.0 的 class 文件。

如果版本号不在支持的范围内,Java 虚拟机实现就抛出 java.lang.UnsupportedClassVersionError 异常。

2.3 类访问标志

类访问标志,这是一个16位的 bitmask,用于指出 class 文件定义的是类还是接口,访问级别是 public 还是 private 等。

1
accessFlags = reader.readUint16();

2.4 类和超类

类访问标志之后是两个u2类型的常量池索引,分别给出类名和超类名。

1
2
className = reader.readUint16();
superClassName = reader.readUint16();

class 文件存储的类名类似完全限定名,但是把点(.)换成了斜线(/),这种名字叫作二进制名(binary names)。

比如,java.lang.Object 在 class 文件中存储的名称为 java/lang/Object

每个类都要有名字,所以 className 必须是有效的常量池索引。

java.lang.Object 之外,其他类都有超类,所以除了 Object.class 以外,其他 class 文件中的 superClassName 必须是有效的常量池索引。

java.lang.Object 的 class 文件中,superClassName 的值是0。

2.5 类接口

类和超类索引后面是接口索引表,表中存放的也是常量池索引,给出该类实现的所有接口的名字。

1
2
interfaceCount = reader.readUint16();
interfaces = reader.readUint16s(interfaceCount);

2.6 字段、方法

接口索引表之后是字段表和方法表,分别存储字段和方法信息。字段和方法的基本结构大致相同,差别仅在于属性表。

1
2
3
4
5
6
7
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}

和类一样,字段和方法也有自己的访问标志。

访问标志之后是一个常量池索引,给出字段名或方法名。

然后又是一个常量池索引,给出字段或方法的描述符。

最后是属性表。

字段和方法的结构基本一致,所以它们的解析过程也差不多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 成员变量
fieldCount = reader.readUint16();
fields = new FieldInfo[fieldCount.value()];
for (int i = 0; i < fields.length; i++) {
fields[i] = new FieldInfo();
fields[i].readFrom(reader);
}

// 成员方法
methodCount = reader.readUint16();
methods = new MethodInfo[methodCount.value()];
for (int i = 0; i < methods.length; i++) {
methods[i] = new MethodInfo();
methods[i].readFrom(reader);
}

三、常量池

常量池里面存放着各式各样的常量信息,包括数字和字符串常量、类和接口名、字段和方法名等等。

常量池实际上也是一个表。表头给出的常量池大小比实际大1。

假设表头给出的值是n,那么常量池的实际大小是n–1。也就是说,常量池的有效的常量池索引是1~n–1。0是无效索引,表示不指向任何常量。

CONSTANT_Long_infoCONSTANT_Double_info 各占两个位置。也就是说,如果常量池中存在这两种常量,实际的常量数量比n–1还要少,而且1~n–1的某些数也会变成无效索引。

1
2
3
4
5
6
7
8
9
10
11
12
constantCount = reader.readUint16();
constantPool = new ConstantInfo[constantCount.value()];
// 常量池的索引从1开始
for (int i = 1; i < constantPool.length; i++) {
constantPool[i] = readConstantInfo(reader);

// 双字类型(即8字节)占2个位置
if (constantPool[i] instanceof LongConstantInfo
|| constantPool[i] instanceof DoubleConstantInfo) {
constantPool[++i] = null;
}
}

常量的第一个字节是 tag 值,用于指明常量的类型。

Java 虚拟机规范定义了14种常量,各个常量值对应的 tag 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Constant {

public static final int ConstantUtf8 = 1; // Java 1.0.2
public static final int ConstantInteger = 3; // Java 1.0.2
public static final int ConstantFloat = 4; // Java 1.0.2
public static final int ConstantLong = 5; // Java 1.0.2
public static final int ConstantDouble = 6; // Java 1.0.2
public static final int ConstantClass = 7; // Java 1.0.2
public static final int ConstantString = 8; // Java 1.0.2
public static final int ConstantFieldRef = 9; // Java 1.0.2
public static final int ConstantMethodRef = 10; // Java 1.0.2
public static final int ConstantInterfaceMethodRef = 11; // Java 1.0.2
public static final int ConstantNameAndType = 12; // Java 1.0.2
public static final int ConstantMethodHandle = 15; // Java 7
public static final int ConstantMethodType = 16; // Java 7
public static final int ConstantInvokeDynamic = 18; // Java 7
public static final int ConstantModule = 19; // Java 9
public static final int ConstantPackage = 20; // Java 9
public static final int ConstantDynamic = 17; // Java 11

}

按照结构,常量池的常量可以分为2种,一种是存放有数据的字面常量,一种是存放索引的引用常量。

像整数、浮点数、UTF8字节等,都属于字面常量;而像字符串、类型、方法等都是引用常量。

就比如,整数常量的结构是这样的:

1
2
3
4
CONSTANT_Integer_info {
3 (u1 tag);
101 (u4 Integer);
}

整数常量的 u4 Integer 的值 101 就是这个整数常量的值,也就是它是直接保存数据的。

而字符串常量的结构是这样的:

1
2
3
4
CONSTANT_String_info {
8 (u1 tag);
34 (u2 string_index);
}

字符串常量里面的 string_index 就只是一个索引,指向了另外的常量,并不直接保存数据。

下面分别详细介绍这些常量的结构定义。

3.1 字面常量

字面常量,和 Java 中的基本类型概念差不多,是实际拥有数据的常量。

3.1.1 CONSTANT_Utf8

CONSTANT_Utf8 是一个变长的数据结构,里面存放的是 MUTF-8 编码的字符串,结构如下:

1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

tag 等于 1。

length 是它的长度,u2 是2个字节,所以它的最大值为 65536。

这也就意味着,Java 中允许的最大字符串长度为 65536 字节。也就是能够放 65536 个 ASCII 字符,或者 65536/3 个中文字符。

它的读取比较直接,按照字节长度读取即可:

1
2
3
4
public void readFrom(ClassReader reader) {
length = reader.readUint16();
val = reader.readBytes(length);
}

3.1.2 CONSTANT_Integer

CONSTANT_Integer 是整型常量,使用4字节存储数值,结构如下:

1
2
3
4
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}

tag 等于 3。

整型常量,对应的就是 Java 中的 Integer 整型,所以直接按照读取 Integer 的方式读取即可。

刚好 ByteBuffer 也提供了相应的基本类型读取方法:

1
2
3
public int readInt() {
return buf.getInt();
}

所以在解析整型常量时,可以直接解析:

1
2
3
public void readFrom(ClassReader reader) {
val = reader.readInt();
}

3.1.3 CONSTANT_Float

CONSTANT_Float 是 IEEE754 单精度浮点数常量,使用4字节存储数值,结构如下:

1
2
3
4
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}

tag 等于 4。

单精度浮点数常量,对应的就是 Java 中的 Float 单精度浮点数。

ByteBuffer 也提供了相应的读取方法:

1
2
3
public float readFloat() {
return buf.getFloat();
}

然后就可以直接解析浮点数了:

1
2
3
public void readFrom(ClassReader reader) {
val = reader.readFloat();
}

3.1.4 CONSTANT_Long

CONSTANT_Long 是8字节长整型常量,结构如下:

1
2
3
4
5
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}

tag 等于 5。

注意,在长整型常量定义中,8字节是被拆分成高位4字节和低位4字节来存放的。

长整型常量,对应的就是 Java 中的 Long 长整型。

ByteBuffer 也提供了相应的读取方法:

1
2
3
public long readLong() {
return buf.getLong();
}

直接解析即可:

1
2
3
public void readFrom(ClassReader reader) {
val = reader.readLong();
}

3.1.5 CONSTANT_Double

CONSTANT_Double 是 IEEE754 双精度浮点数常量,使用8字节存储数值,结构如下:

1
2
3
4
5
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}

tag 等于 6。

注意,在双精度浮点数常量定义中,8字节也是被拆分成高位4字节和低位4字节来存放的。

双精度浮点数常量,对应的是 Java 中的 Double 双精度浮点数。

ByteBuffer 也提供了相应的读取方法:

1
2
3
public double readDouble() {
return buf.getDouble();
}

直接解析即可:

1
2
3
public void readFrom(ClassReader reader) {
val = reader.readDouble();
}

3.2 引用常量

引用常量比较简单,它们不直接保存数据,只保存了索引,所以结构上可能会比字面常量稍微复杂一些。

下面简单介绍其中几种常见的引用常量。

3.2.1 CONSTANT_String

CONSTANT_String_info 常量表示 java.lang.String 字面量,结构如下:

1
2
3
4
CONSTANT_String_info {
u1 tag;
u2 string_index;
}

tag 等于 8。

string_index 是常量池索引,指向一个 CONSTANT_Utf8_info 常量。

3.2.2 CONSTANT_Class

CONSTANT_Class 常量表示类或者接口的符号引用,结构如下:

1
2
3
4
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}

tag 等于 7。

name_index 是常量池索引,指向一个 CONSTANT_Utf8_info 常量。

3.2.3 CONSTANT_Fieldref

CONSTANT_Fieldref_info 表示字段符号引用,结构如下:

1
2
3
4
5
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

tag 等于 9。

class_index 是所在类索引,指向一个 CONSTANT_Class_info 常量。

name_and_type_index 是名称和类型定义索引,指向一个 CONSTANT_NameAndType_info 常量。

3.2.4 CONSTANT_Methodref

CONSTANT_Methodref_info 表示普通(非接口)方法符号引用,结构如下:

1
2
3
4
5
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

tag 等于 10。

class_index 是所在类索引,指向一个 CONSTANT_Class_info 常量。

name_and_type_index 是名称和类型定义索引,指向一个 CONSTANT_NameAndType_info 常量。

3.2.5 CONSTANT_InterfaceMethodref

CONSTANT_InterfaceMethodref_info 表示接口方法符号引用,结构如下:

1
2
3
4
5
CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

tag 等于 11。

class_index 是所在接口索引,指向一个 CONSTANT_Class_info 常量。

name_and_type_index 是名称和类型定义索引,指向一个 CONSTANT_NameAndType_info 常量。

3.2.6 CONSTANT_NameAndType

CONSTANT_NameAndType_info 给出字段或方法的名称和描述符,结构如下:

1
2
3
4
5
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}

tag 等于 12。

name_index 是名称索引,指向一个 CONSTANT_Utf8_info 常量。

descriptor_index 是字段或方法的描述符索引,也是指向一个 CONSTANT_Utf8_info 常量。

CONSTANT_Class_infoCONSTANT_NameAndType_info 加在一起可以唯一确定一个字段或者方法。

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

不过这里的描述符,不是常见的完整的字段或者函数定义,而是一种缩写形式,它的规则有以下几个:

  • 类型描述符
    • 基本类型byte、short、char、int、long、float和double的描述符是单个字母,分别对应B、S、C、I、J、F和D(注意,long的描述符是J而不是L)
    • 引用类型的描述符是“L + 类的完全限定名 + 分号”
    • 数组类型的描述符是“[ + 数组元素类型描述符”
  • 字段描述符
    • 字段描述符就是字段类型的描述符
  • 方法描述符
    • 方法描述符是“(分号分隔的参数类型描述符)+ 返回值类型描述符”,其中void返回值由单个字母V表示

这里直接举几个例子来说明:

描述符 类型
B byte
S short
Ljava/lang/Object; java.lang.Object
[I int[]
[[Ljava/lang/String; String[][]
()V void method()
(Ljava/lang/String;)Ljava/lang/String; String method(String)
([JJ)J long method(long[], long)
(Ljava/lang/String;Ljava/lang/String;)V void method(String, String)

为了减少描述符在 ClassFile 文件中占用的空间,它只保留了必要的属性,一些不必要的属性如方法名称,并没有直接保存在描述符中。

Java语言支持方法重载(override),不同的方法可以有相同的名字,只要参数列表不同即可。这就是为什么CONSTANT_NameAndType_info结构要同时包含名称和描述符的原因。

那么字段呢?Java是不能定义多个同名字段的,哪怕它们的类型各不相同。这只是Java语法的限制而已,从class文件的层面来看,是完全可以支持这点的。

小结

常量池中的常量分为两类:字面量(literal)和符号引用(symbolic reference)。

字面量包括数字常量和字符串常量,符号引用包括类和接口名、字段和方法信息等。

除了字面量,其他引用常量都是通过索引直接或间接指向 CONSTANT_Utf8_info 常量。

四、属性表

属性表可谓是个大杂烩,里面存储了各式各样的信息,如方法的字节码等。

常量是由Java虚拟机规范严格定义的,共有14种。但属性是可以扩展的,不同的虚拟机实现可以定义自己的属性类型。

由于这个原因,Java虚拟机规范没有使用tag,而是使用属性名来区别不同的属性。

属性数据放在属性名之后的u1表中,这样Java虚拟机实现就可以跳过自己无法识别的属性。

属性的结构定义如下:

1
2
3
4
5
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}

属性表中存放的属性名实际上并不是编码后的字符串,而是常量池索引 attribute_name_index,指向常量池中的 CONSTANT_Utf8_info 常量。

按照这个定义,定义了一个属性接口:

1
2
3
public interface AttributeInfo {
void readFrom(ClassReader reader);
}

具体属性只要实现 readFrom 方法即可。

4.1 属性类型

Java虚拟机规范预定义了23种属性,按照用途可以分为三组:

  • 实现Java虚拟机所必需的,共有5种
  • Java类库所必需的,共有12种
  • 提供给工具使用,共有6种,这组属性是可选的
属性名 所在位置 分组 增加版本
ConstantValue field_info 1 1.0.2
Code meghod_info 1 1.0.2
Exceptions method_info 1 1.0.2
SourceFile ClassFile 3 1.0.2
LineNumberTable Code 3 1.0.2
LocalVariableTable Code 3 1.0.2
InnerClasses ClassFile 2 1.1
Synthetic ClassFile,field_info,method_info 2 1.1
Deprecated ClassFile,field_info,method_info 3 1.1
EnclosingMethod ClassFile 2 5.0
Signature ClassFile,field_info,method_info 2 5.0
SourceDebugExtension ClassFile 3 5.0
LocalVariableTypeTable Code 3 5.0
RunttimeVisibleAnnotations ClassFile,field_info,method_info 2 5.0
RunttimeInvisibleAnnotations ClassFile,field_info,method_info 2 5.0
RunttimeVisibleParameterAnnotations method_info 2 5.0
RunttimeInvisibleParameterAnnotations method_info 2 5.0
AnnotationDefault method_info 2 5.0
StackMapTable Code 1 6
BootstrapMethods ClassFile 1 7
RunttimeVisibleTypeAnnotations ClassFile,field_info,method_info,Code 2 8
RunttimeInvisibleTypeAnnotations ClassFile,field_info,method_info,Code 2 8
MethodParameters method_info 2 8

4.2 属性定义

4.2.1 Deprecated和Synthetic属性

Deprecated 是最简单的两种属性之一,仅起标记作用,不包含任何数据。

它的结构定义如下:

1
2
3
4
Deprecated_attribute {
u2 attribute_name_index;
u4 attribute_length;
}

由于不包含任何数据,所以attribute_length的值必须是0。

Deprecated 属性用于指出类、接口、字段或方法已经不建议使用,编译器等工具可以根据Deprecated属性输出警告信息。

J2SE 5.0之前,可以使用 Javadoc 提供的 @deprecated 标签指示编译器给类、接口、字段或方法添加 Deprecated 属性。

从J2SE 5.0开始,可以使用 @Deprecated 注解。

Deprecated 属性不包含数据,所以它的 readFrom 实现为空就行了:

1
2
3
@Override
public void readFrom(ClassReader reader) {
}

4.2.2 Synthetic

Synthetic 是最简单的两种属性之一,仅起标记作用,不包含任何数据。

它的结构定义如下:

1
2
3
4
Synthetic_attribute {
u2 attribute_name_index;
u4 attribute_length;
}

Synthetic 属性用来标记源文件中不存在、由编译器生成的类成员,引入 Synthetic 属性主要是为了支持嵌套类和嵌套接口。

Synthetic 属性不包含数据,所以它的 readFrom 也是为空:

1
2
3
@Override
public void readFrom(ClassReader reader) {
}

4.2.3 SourceFile

SourceFile 是可选定长属性,只会出现在 ClassFile 结构中,用于指出源文件名。

其结构定义如下:

1
2
3
4
5
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}

attribute_length 的值必须是2。

sourcefile_index 是常量池索引,指向一个 CONSTANT_Utf8_info 常量。

SourceFile 属性读取很简单,直接读就行了:

1
2
3
4
5
@Override
public void readFrom(ClassReader reader) {
length = reader.readUint32();
nameIndex = reader.readUint16();
}

4.2.4 ConstantValue

ConstantValue 是定长属性,只会出现在 field_info 结构中,用于表示常量表达式的值。

其结构定义如下:

1
2
3
4
5
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}

attribute_length 的值必须是2。

constantvalue_index 是常量池索引,指向某一个类型定义,但具体指向哪种常量因字段类型而异。

ConstantValue 属性读取也很简单,也是直接读就行了:

1
2
3
4
5
@Override
public void readFrom(ClassReader reader) {
length = reader.readUint32();
constantIndex = reader.readUint16();
}

4.2.5 Code

Code 是变长属性,只存在于 method_info 结构中。Code 属性中存放字节码等方法相关信息。

其结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

max_stack 给出操作数栈的最大深度;max_locals给出局部变量表大小;

接着是字节码,存在u1表中;最后是异常处理表和属性表。

Code 属性结构相对复杂一些,有几层结构,读取起来有点麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void readFrom(ClassReader reader) {
length = reader.readUint32();
maxStack = reader.readUint16();
maxLocals = reader.readUint16();
codeLength = reader.readUint32();
codes = reader.readBytes(codeLength);
exceptionLength = reader.readUint16();
exceptionEntries = new ExceptionTableEntry[exceptionLength.value()];
for (int i = 0; i < exceptionEntries.length; i++) {
exceptionEntries[i] = readExceptionEntry(reader);
}
attributeInfoTable = new AttributeInfoTable();
attributeInfoTable.readFrom(reader);
}

private ExceptionTableEntry readExceptionEntry(ClassReader reader) {
ExceptionTableEntry entry = new ExceptionTableEntry();
entry.setStartPC(reader.readUint16());
entry.setEndPC(reader.readUint16());
entry.setHandlerPC(reader.readUint16());
entry.setCatchPC(reader.readUint16());
return entry;
}

4.2.6 Exceptions

Exceptions 是变长属性,记录方法抛出的异常表。

其结构定义如下:

1
2
3
4
5
6
Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}

Exceptions 属性简单,直接读取即可:

1
2
3
4
5
6
@Override
public void readFrom(ClassReader reader) {
length = reader.readUint32();
numberOfExceptions = reader.readUint16();
exceptionIndexTable = reader.readUint16s(numberOfExceptions);
}

4.2.7 LineNumberTable

LineNumberTable 属性表存放方法的行号信息。

结构定义如下:

1
2
3
4
5
6
7
8
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}

LineNumberTable 属性表不算复杂,可以直接读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 @Override
public void readFrom(ClassReader reader) {
length = reader.readUint32();
lineNumberLength = reader.readUint16();
lineNumberEntries = new LineNumberTableEntry[lineNumberLength.value()];
for (int i = 0; i < lineNumberEntries.length; i++) {
lineNumberEntries[i] = readLineNumberTableEntry(reader);
}
}

private LineNumberTableEntry readLineNumberTableEntry(ClassReader reader) {
LineNumberTableEntry entry = new LineNumberTableEntry();
entry.setStartPC(reader.readUint16());
entry.setLineNumber(reader.readUint16());
return entry;
}

小结

相对于常量来说,属性的结构要稍微复杂一些,毕竟是可以扩展的,不像常量池常量那样基本都是固定的。

虽然属性的机构稍微复杂一些,但是层次还是比较清晰的。

五、单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ClassFileStructureTest {

private byte bt = 1;
private boolean b = false;
private char c = 'A';
private short s = 2;
private int i = 3;
private long l = 4L;
private float f = 5.0F;
private double d = 6.0;
private String str = "test";

public static void main(String[] args) {
short s = -10;
int i = s;
int ii = s & 0xFFFF;
long ss = 0L;
long si = ss | i;
for (int j = 0; j < 4; j++) {
}
System.out.println("Class File Test" + i + " " + ii + " " + si);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ClassFileTest {

@Test
public void getClassName() throws IOException {
String jreOption = null;
String cpOption = "C:\\IdeaProjects\\self-jvm\\target;C:\\IdeaProjects\\self-jvm\\target\\classes";
String bootClassName = "java\\lang\\Object";
String userClassName = "com\\wjd\\classfile\\ClassFileStructureTest";
Classpath classpath = new Classpath(jreOption, cpOption);

byte[] bootClassBytes = classpath.readClass(bootClassName);
ClassReader bootReader = new ClassReader(bootClassBytes);
ClassFile bootClassFile = ClassFile.parse(bootReader);
assertEquals("Class name error", "java/lang/Object", bootClassFile.getClassName());

byte[] userClassBytes = classpath.readClass(userClassName);
ClassReader userReader = new ClassReader(userClassBytes);
ClassFile userClassFile = ClassFile.parse(userReader);
assertEquals("Class name error", "com/wjd/classfile/ClassFileStructureTest", userClassFile.getClassName());
}
}

总结

class 文件:

  • Java 虚拟机规范中所指的 class 文件,并非特指位于磁盘中的 .class 文件,而是泛指任何格式符合规范的 class 数据。它实际上可以通过网络下载,从数据库加载,甚至是在运行中直接生成等方式来获取 class 文件
    • 构成 class 文件的基本数据单位是字节,可以把整个 class 文件当成一个字节流来处理
    • 数据由连续多个字节构成,这些数据在 class 文件中以大端(big-endian)方式存储
  • 为了描述 class 文件格式,Java 虚拟机规范定义了 u1、u2 和 u4 三种数据类型来表示1、2和4字节无符号整数
  • 相同类型的多条数据一般按表(table)的形式存储在 class 文件中
  • 表由表头和表项(item)构成,表头是 u2 或 u4 整数

常量池:

  • 常量池里面存放着各式各样的常量信息,包括数字和字符串常量、类和接口名、字段和方法名等等
  • 常量池实际上也是一个表。表头给出的常量池大小比实际大1
    • 假设表头给出的值是n,那么常量池的实际大小是n–1。也就是说,常量池的有效的常量池索引是1~n–1。0是无效索引,表示不指向任何常量
    • longdouble 各占两个位置。也就是说,如果常量池中存在这两种常量,实际的常量数量比n–1还要少,而且1~n–1的某些数也会变成无效索引
  • 按照结构,常量池的常量可以分为2种,一种是存放有数据的字面常量(literal),一种是存放索引的符号引用常量(symbolic reference)
    • 字面量包括数字常量和字符串常量,符号引用包括类和接口名、字段和方法信息等
    • 像整数、浮点数、UTF8字节等,都属于字面常量,直接存放数据
    • 而像字符串、类型、方法等都是引用常量,不直接存放数据,只保存指向数据的索引
    • 除了字面量,其他引用常量都是通过索引直接或间接指向 CONSTANT_Utf8_info 常量

属性:

  • Java虚拟机规范没有使用tag,而是使用属性名来区别不同的属性
  • 常量是由Java虚拟机规范严格定义的,共有14种。但属性是可以扩展的,不同的虚拟机实现可以定义自己的属性类型
作者

jiaduo

发布于

2021-10-08

更新于

2023-04-03

许可协议