文章目录
  1. JVM学习04-class字节码的结构
  2. 1. 字节码简介
  3. 2. 字节码的文件结构
    1. 2.1 魔数 magic
    2. 2.2 版本 version
    3. 2.3 常量池 constant_pool(cp)
    4. 2.4 访问标识 access flag
    5. 2.5 本类和超类
    6. 2.6 接口 Interfaces
    7. 2.7 字段 Fields
    8. 2.8 方法 Methods
    9. 2.9 属性
  4. 3. 总结

[TOC]

JVM学习04-class字节码的结构

1. 字节码简介

java作为跨平台的语言,其重要性就体现在java源代码编译后的字节码 .class 文件。很多的语言如java、groovy、scala、kotlin等都是基于JVM运行的,其根本就是输出之后的字节码。作为跨平台的一个基础,当然要对字节码的文件结构有一定的认识。

2. 字节码的文件结构

字节码的文件结构一次可以分为(魔数,版本,常量池,访问符,类,超类,接口,字段,方法,属性)。每个部分的数据的大小都采用无符号整形来表示,例如u1,u2,u4等等表示一个字节,两个字节,四个字节。具体参照下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 或者像这样
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info contant_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];
}

图中的数据类型除了无符号整形之外,还有”_info”结尾的数据类型,这种称之为”表”。例如常量池中是存在多个常量的,所以要通过类似表的结构来进行存储,而且因为字节码是依次排列的,所以在描述常量池信息之前还需要对常量池中常量的个数进行描述(标记),这个很容易理解,就是用来判断什么时候读到常量池的结尾了。当然字段,方法和属性以”_info”结尾就类似了。
下面对每一个部分一一描述:

在分析字节码之前介绍一款工具,Java ByteCode Editor

以下的例子基于代码(举例子的时候常量池的索引可能会存在误差,下面阅读的时候请考虑这段话):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.minosa.test;

public class HelloClass {
private String name = "Mimosa";
private int age = 20;
private static final int i = 10;
public String sayHello(){
System.out.println("Hello " + name + " ," + age);
return name;
}

public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
System.out.println(c);
System.out.println("Hello Class !!!");
}
}

2.1 魔数 magic

魔数是字节码固定的数值,在文件的最开始部分,占四个字节,值是 (CA FE BA BE)。

2.2 版本 version

版本分为小版本minor和大版本major,分别占两个字节。例如(00 00 00 33)就表示 51.0版本。如图,稍微看下即可:

2.3 常量池 constant_pool(cp)

常量池是整个字节码结构中最重要的一个部分,因为常量池之后的数据基本都是引用常量池中的数据,后面就会看到。
常量池分为两个部分:cp数量和cp_info。之前说过先是以数量开头作为标识。
这里解决下一个问题:为什么field_info等的数量都是fields_count,而cp_info的数量却要减1。这是因为常量池中的真实存在的数据是以下标1开始的,而下标0则代表”无引用”(不引用常量池中的任意一项),比如一个类没有接口的话就指向常量池中0下标。

常量池中的数据又存在多种类型的常量,如Class_info, Utf8_info等等,如图总表所示:类型有点多(不必害怕),依次使用例子进行描述

1. Utf8_info

Utf8_info 的数据表示utf8编码中的数据,在字节码中分为三个部分:tag标识,bytes长度,bytes数据。
tag表示是固定的,Utf8_info的tag值是(01),占一个字节。
其次是bytes数组的length,占两个字节。
最后是bytes数组中的数据,依次表示,每个字符占一个字节(中文不止一个字节,可自行研究)。
比如一个类的名称(com/minosa/test/HelloClass)是个字符串,指向常量池中一个Utf8_info的数据:

class字节码16进制表示中的数据(用的editplus的16进制查看器打开的):

上图蓝色高亮部分:
(1) 01 tag 表示Utf8_info类型。
(2) 00 1A 表示字节数组的长度,是26
(3) 63 6F 6D … 73 73,表示数组中的数据,就是字符串utf8编码的表示,这里是的数据是(com … ss)。

2. Integer_info

Integer_info 数据分为两个部分:tag标识,int型值
tag固定值为 03;其次是int的值,占4个字节。一般在常量池中的整形,浮点型等数据在java中是static final类型的编译时常量。
比如一个static final int i = 10;

16进制字节码表示:

3. Float_info

同 Integer_info 类型,只是tag为4,数据占4个字节。

4. Long_info

同 Integer_info 类型,只是tag为5,数据占8个字节。

5. Double_info

同 Integer_info 类型,只是tag为6,数据占8个字节。

6. Class_info

Class_info 表示一个类的信息,包含两个部分:tag和类全名。
tag固定值为 07 ,类全名name是一个index(索引),指向一个utf8_info。
如上面的类(com/minosa/test/HelloClass),表示为:

图中类的名称指向常量池中索引为2的常量,在16进制的字节码中表示为:

上图中:魔数是(CA FE BA BE),版本号是(00 00 00 33)。紧接着就是常量池:(00 46)表示常量池中数据的数量,然后就是常量池中的数据。这里的(07 00 02)就表示Class_info。

7. String_info

String_info表示字符串常量,分为两个部分:tag和String值。
tag固定值为 08,String的值是一个指向utf8_info的索引。
比如一个面值为”Mimosa”的string常量,在常量池的表示:

在16进制字节码中的表示:

8. NameAndType_info

NameAndType_info 表示名称和类型的常量,为以下Fieldref_info,Methodref_info,InterfaceMethodref_info服务。
NameAndType_info 分为三个部分:tag,name和描述description。
tag是固定值 0C,name就表示该类型的名称(指向utf8_info的索引),description是该类型的描述(指向utf8_info的索引)。
这里的类型到后面再说,比如一个构造方法的描述:

这里的表示构造方法的描述名称,()V表示构造方法(这个后面会介绍)。
16进制字节码描述:

9. Fieldref_info

Fieldref_info 表示一个字段的描述,分为三个部分:tag,字段所在的Class(指向一个Class_info),字段的描述(指向一个NameAndType_info)。
tag是固定值 09;其次是字段所在的类的描述;然后是字段的NameAndType描述。
比如一个字段 public String name;

图中Class_info上面已经有图了,然后是NameAndType_info为:

Name表示字段的名称为name,Descriptor是字段的类型为(Ljava/lang/String;),类型后面会讲到。
Fieldref_info 在16进制字节码中表示:

10. Methodref_info

Methodref_info 就和 Fieldref_info 同理了,分为tag,方法所在类,方法的描述三个部分。
tag固定值为 0A;其次是方法所在类;最后是方法的描述;
以构造方法为例:

之后的分析就和上面同理。

11. InterfaceMethodref_info

接口的方法定义,与 Methodref_info 同理。
分为三个部分:tag固定值为 0B,方法所在的接口(指向一个Class_info),方法的描述(指向一个NameAndType_info)。

2.4 访问标识 access flag

访问标识标识类的访问类型,占2个字节。所有类型如图:

比如当前类的访问标识为public:

2.5 本类和超类

紧接着是本类和超类的,this_class和super_class都是指向常量池中的一个Class_info。比如之前的例子:

在16进制字节码中的表示:

2.6 接口 Interfaces

接口与常量池类似,分为接口数量和接口描述两个部分。
接口数量占用2个字节;每个接口描述是指向常量池中的一个Class_info,也占2个字节。

由于本例子中接口的数量是0,所以后面没有接口的描述。

2.7 字段 Fields

字段同样分为两个部分:字段数量和字段的描述。
字段数量占2个字节。
每个字段描述分为5个部分:访问标识,字段名(常量池索引),字段描述(常量池索引),属性数量和属性描述。

1. 字段访问标识
与类的访问标识类似,占2个字节。

2. 字段名
字段的名称,指向常量池中的一个utf8_info数据。

3. 字段描述
字段的描述也是指向常量池中的一个utf8_info数据。之前说类型描述后面会讲到,这里列出所有的字段类型的描述符(方法的描述符后面说)。

4. 属性数量和属性描述
attributes_count 占2个字节,后面紧接着就是每个attribute_info 的描述。例如当有一个static final类型的变量,就会生成一个编译时常量Integer_info值的描述。

例子:比如name字段和age字段
首先是字段的数量:

1 . private String name;


name属性依次是:(00 02)表示private;(00 05和00 06)表示name和Descriptor;(00 00)表示属性的数量,这里不存在。

2 . private int age;

同上。

3 . public static final int i = 10;

然后字段有一个ConstantValue(常量)属性:

这里常量池中的#10和#11分为表示:

2.8 方法 Methods

同样,方法也分为数量和方法的描述。
方法的描述和字段类似,也分为:访问标识,名称,描述,属性数量和属性。

1. 访问标识
占用2个字节。

2. 名称和描述
名称和描述都是指向常量池中的utf8_info,各占2个字节。这里说下方法的描述符:

1
2
3
4
5
6
7
8
9
10
// 例举几个例子就明白了
1. 构造方法
==> ()V
表示没有参数,返回值为void

2. int indexOf(char[],int)
==> ([CI)I

3. String name(String,long)
==> (Ljava/lang/String;J)Ljava/lang/String;

3. 属性数量和属性
属性的数量占2个字节。
在field和method中可以有很多属性,类也存在attribute,每个attribute用于描述一个额外的信息。
每个attribute又分为三个部分:attribute名称,attribute的长度和属性的描述。
attribute名称是指向常量池中的一个utf8_info,占2个字节;attribute的长度占4个字节;每一个属性内容占1个字节。
这里列举一些属性的描述:

例如每个方法含有Code属性,Code包含着需要执行的代码(下一博客讲述),Code属性中又含有LineNumberTable和LocalVaribleTable。
LineNumberTable记录在源代码中的行号,LocalVaribleTable就表示之前博客讲过的JVM内存模型中栈中的局部变量分配的表。
例如Exceptions属性表示方法申明的抛出的异常(throws的部分,非try/catch部分)。
例如SourceFile表示生成该字节码的源文件名称。
例如ConstantValue表示final常量的值,上面字段例举过了。

2.9 属性

刚刚说了类也存在属性,那么字节码的最后一项就是类的属性。同样分为属性的数量和属性描述。
例如在本例子中,存在SourceFile属性:

3. 总结

其实字节码的结构还算是比较简单的,稍微过下就好。下面需要理解的就是方法中代码Code的执行过程,可以使用java自带的javap -verbose对字节码进行反汇编,查看执行的指令。理解完了字节码的执行过程,那么在此基础上可以使用ASM等字节码工具更好的对字节码进行操作。

文章目录
  1. JVM学习04-class字节码的结构
  2. 1. 字节码简介
  3. 2. 字节码的文件结构
    1. 2.1 魔数 magic
    2. 2.2 版本 version
    3. 2.3 常量池 constant_pool(cp)
    4. 2.4 访问标识 access flag
    5. 2.5 本类和超类
    6. 2.6 接口 Interfaces
    7. 2.7 字段 Fields
    8. 2.8 方法 Methods
    9. 2.9 属性
  4. 3. 总结