java class类文件结构_完全图解JVM Class文件结构

对一个class文件的字节码进行逐行的分析是理解class文件结构的最佳方式。但是往往复杂的二进制字节码会让人望而却步,或者只有仔细一点点盯着才能保证不花眼。本文的目的在于尽可能完整地拆解JVM的Class字节码并将其分块分析,最终得到的图解结构希望可以帮助到你。

本文参考自来自周志明《深入理解Java虚拟机(第2版)》,拓展内容建议读者可以阅读下这本书。

根据这个简单的例子来说明

以下的例子作为最简单的一个java程序,通过javac执行编译,javap来查看它的反编译结果,当然我们还会更刨根问底地直接使用二进制编辑器查看class文件的二进制字节排布。

15340558532664.jpg

> javap -v Test

Classfile /Users/jinhaoplus/Desktop/Test.class

Last modified 2018-8-12; size 285 bytes

MD5 checksum eac8f02f8ad176b09bfd89cf15e2ed3d

Compiled from "Test.java"

public class top.jinhaoplus.demo.Test

minor version: 0

major version: 52

flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

#1 = Methodref #4.#15 // java/lang/Object."":()V

#2 = Fieldref #3.#16 // top/jinhaoplus/demo/Test.m:I

#3 = Class #17 // top/jinhaoplus/demo/Test

#4 = Class #18 // java/lang/Object

#5 = Utf8 m

#6 = Utf8 I

#7 = Utf8

#8 = Utf8 ()V

#9 = Utf8 Code

#10 = Utf8 LineNumberTable

#11 = Utf8 inc

#12 = Utf8 ()I

#13 = Utf8 SourceFile

#14 = Utf8 Test.java

#15 = NameAndType #7:#8 // "":()V

#16 = NameAndType #5:#6 // m:I

#17 = Utf8 top/jinhaoplus/demo/Test

#18 = Utf8 java/lang/Object

{

public int m;

descriptor: I

flags: ACC_PUBLIC

public top.jinhaoplus.demo.Test();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: return

LineNumberTable:

line 3: 0

public int inc();

descriptor: ()I

flags: ACC_PUBLIC

Code:

stack=2, locals=1, args_size=1

0: aload_0

1: getfield #2 // Field m:I

4: iconst_1

5: iadd

6: ireturn

LineNumberTable:

line 6: 0

}

SourceFile: "Test.java"

15340559957087.jpg

图解概况

如上的字节码阅读起来有诸多障碍,因此我们把上面的字节码按照字节码规范定义的class结构分区为不同的颜色块,不同的分区颜色说明这个区域对应着class结构中的不同区域定义,表示一个整体概念的字节码在图中显示为同一行上:

class-format.png

下面的图是class文件结构的思维导图说明,可以跟上述的实际的一个class的分区作简单的对照:

15340855734850.jpg

详细解释一下class文件的每个分区

下面详细解释一下class文件的每个分区,括号内的数字表示当前区的占位情况,u是字节的意思,如u4表示占4个字节的空间,对应到图中就是4个方格。

1. magic

magic(u4):魔数,class文件的标识开头。

CAFEBABE是固定的JVM Class的魔数,也可以认为是众所周知的Java咖啡Logo的由来。

15340643433632.jpg

2. version

version:class版本,主次版本合起来即可确定版本号。

2.1 minor_version(u2):次版本

2.2 major_version(u2):主版本

Class文件的版本为次版本0X0000、主版本0X0034,对应的是10进制的52.0。说明此Class是在JDK_VERSION=52.0(JDK1.8)的编译器中生成的,同时又可以被版本在JDK_VERSION=52.0及以上的虚拟机上执行(JVM保持了向下兼容性,但是拒绝执行超过它的版本号的Class字节码)。

15340653284566.jpg

3. 常量池:注意是本处的常量池指class字节码中的常量池而非JVM中的常量池(但后者中的数据其实是加载于前者)。

3.1 constant_pool_count

constant_pool_count(u2):常量池大小,定义了常量池中保存的常量个数(准确说常量个数=constant_pool_count-1)。

0X0013表示constant_pool_count=19,常量池中保存的常量个数=18(编号为#1~#18)。

15340662203022.jpg

3.2 constant_pool

constant_pool(constant_pool_count-1个constant_pool_info):实际保存的常量,编号从1开始(将0位留空有特殊考量)。

常量有多种种类,我们这里只提一下我们的Class文件里涉及到的具体的类型。

15340663578215.jpg

3.2.1 CONSTANT_Utf8_info

由utf-8编码的二进制串,其字节码格式为

类型

名称

数量

u1

tag

1

u2

length

1

u1

bytes

length

其中的tag=0X01即为CONSTANT_Utf8_info类型常量的标识。我们Class字节码中的#5、#6、#7、#8、#9、#10、#11、#12、#13、#14、#17、#18都是CONSTANT_Utf8_info常量,因为它们的首位tag=0X01(橘色列),通过utf-8解码这些常量指定长度的二进制串可以得出下面的结果,比如#5号常量length=1(10进制的0X0001),而bytes为0X6D,utf-8解码后就是字符串m,同理可以得到这些二进制串的值(这就是javap反编译出结果的原理,可以参照javap得到的结果对照一下):

#5 m

#6 I

#7

#8 ()V

#9 Code

#10 LineNumberTable

#11 inc

#12 ()I

#13 SourceFile

#14 Test.java

#17 top/jinhaoplus/demo/Test

#18 java/lang/Object

3.2.2 CONSTANT_Class_info

类常量,其字节码格式为

类型

名称

数量

u1

tag

1

u2

index

1

其中的tag=0X07即为CONSTANT_Class_info类型常量的标识,index指向了常量池中类的全限定名的索引序号。

我们Class字节码中的#3、#4是CONSTANT_Class_info类型的类常量,它们的首位tag=0X07(橘色列),通过查找常量池中它们指向的索引序号,我们可以得出这两个类的全限定名:

#3 #17 // top/jinhaoplus/demo/Test

#4 #18 // java/lang/Object

3.2.3 CONSTANT_NameAndType_info

字段或方法的名称和类型常量,其字节码格式为

类型

名称

数量

u1

tag

1

u2

index

1

u2

index

1

其中的tag=0X0C即为CONSTANT_NameAndType_info类型常量的标识,第一个index指向了字段或方法名称在常量池中的索引序号,第二个index指向了字段或方法的描述符在常量池中的索引序号。

字段的描述符就是简单的字段类型,Class文件中的类型为了节省空间进行了简化:如基本类型int->I,double->D,引用类型java/lang/Object -> Ljava/lang/Object。

我们Class字节码中的#15、#16是CONSTANT_NameAndType_info类型的类常量,它们的首位tag=0X0C(橘色列),通过查找常量池中它们两个指向的索引序号,我们可以得出常量#15的名称为#7号常量即,类型为#8号常量即()V。同理可以得到#16的意思。

#15 #7:#8 // "":()V

#16 #5:#6 // m:I

3.2.4 CONSTANT_Fieldref_info

字段引用常量,其字节码格式为

类型

名称

数量

u1

tag

1

u2

index

1

u2

index

1

其中的tag=0X09即为CONSTANT_Fieldref_info类型常量的标识,第一个index指向了声明字段的类或接口的CONSTANT_Class_info常量在常量池中的索引序号,第二个index指向了字段的名称和类型信息CONSTANT_NameAndType_info常量在常量池中的索引序号。

我们Class字节码中的#2是CONSTANT_Fieldref_info类型的类常量,它们的首位tag=0X09(橘色列),通过查找常量池中它指向的索引序号,我们可以得出这个字段的声明类的是top/jinhaoplus/demo/Test,字段的名称是m,类型是I(即int,Class将类型全称映射到成了单字母)。

#2 #3.#16 // top/jinhaoplus/demo/Test.m:I

3.2.5 CONSTANT_Methodref_info

方法引用常量,其字节码格式为

类型

名称

数量

u1

tag

1

u2

index

1

u2

index

1

其中的tag=0X0A即为CONSTANT_Methodref_info类型常量的标识,第一个index指向了声明方法的类或接口的CONSTANT_Class_info常量在常量池中的索引序号,第二个index指向了方法的名称和类型信息CONSTANT_NameAndType_info常量在常量池中的索引序号。

我们Class字节码中的#1是CONSTANT_Fieldref_info类型的类常量,它们的首位tag=0X0A(橘色列),通过查找常量池中它指向的索引序号,我们可以得出这个方法的声明类是java/lang/Object,方法的名称是,类型是()V(即无入参返回void类型的方法)。

#1 #4.#15 // java/lang/Object."":()V

至此我们得到了这个Class中的常量池中全部的常量的含义。这些常量将被下面的其他部分引用到。

4.类信息:

4.1 access_flag

access_flag(u2):说明这个类或接口的访问标志,如private/public/interface/abstract/annotation/enum等,总之是说明了这个类的特征。以不同的特征给出特征位的方式来设置这个u2大小的区域。

如本Class的0X0021实际代表了特征位信息是0000000000110001,即ACC_SUPER|ACC_PUBLIC,表示它是public的class(ACC_SUPER是JDK1.0.2后的默认设置项)。

15340703120217.jpg

4.2 this_class

this_class(u2):说明本类的类索引,0X0003说明本类索引在常量池中的序号为3,上面常量池的分析可以看到本类的全限定名是top/jinhaoplus/demo/Test。

15340708071884.jpg

4.3 super_class

super_class(u2):说明父类的类索引,0X0004说明父类索引在常量池中的序号为4,上面常量池的分析可以看到父类的全限定名是java/lang/Object(这也就是所有Java类的父类都是Object的原因,即使没有明确写出来编译后的Class文件中也会将这个父类声明定义出来)。

15340707742211.jpg

4.4 interface_info

4.4.1 interface_count(u2):说明实现的接口数量,0X0000说明本类没有实现接口,因此不再有接下来的interface信息。

15340710870622.jpg

4.4.2 interface(interface_count个u2):说明接口的类索引。

5.字段信息

5.1 field_count(u2):字段数量

15340856022451.jpg

5.2 field_info(field_count个field_info):字段信息,字段表的结构如下:

类型

名称

数量

u2

access_flag

1

u2

name_index

1

u2

descriptor_index

1

u2

attributes_count

1

attribute_info

attributes

attributes_count

15340861945087.jpg

5.2.1 access_flag(u2)用以记录字段的特征。

比如private/public/protected/static/final/volatile,以不同的特征给出特征位的方式来设置这个u2大小的区域。我们的Class中的0X0001(橙色列)实际代表了特征位信息是0000000000000001,即字段的特征是ACC_PUBLIC(public字段)。

5.2.2 name_index(u2)是字段名在常量池中的索引序号。

我们的Class中的0X0005(蓝色列)指向的常量池中的#5号常量即m。

5.2.3 descriptor_index(u2)是字段描述符在常量池中的索引序号。

我们的Class中的0X0006(青色列)指向的常量池中的#6号常量即I。

5.2.4 字段的属性表是本字段的属性表:

5.2.4.1 attributes_count(u2):字段属性表的属性数量,我们的Class中的0X0000表示本字段无额外的属性表信息。

5.2.4.1 attributes(attributes_count个attribute_info):字段属性表的属性信息,字段属性有自己定义的结构,字段中主要使用的属性包括ConstantValue(final修饰的常量值作为字段的值)、Depreciated(@Depreciated修饰的字段表示弃用)、Signature(泛型参数记录的泛型签名信息,否则编译后擦除类型就无法溯源了)等,他们都有各自定义的结构。

6.方法信息

6.1 method_count

method_count(u2):方法数量

我们的Class这个区的0X0002表示这个类有两个方法。

15341671333047.jpg

6.2 method_info

method_info(method_count个method_info):方法信息,方法表的结构如下:

类型

名称

数量

u2

access_flag

1

u2

name_index

1

u2

descriptor_index

1

u2

attributes_count

1

attribute_info

attributes

attributes_count

15341673836630.jpg

6.2.1 access_flag(u2)用以记录方法的特征。

比如private/public/protected/static/final/synchronized,以不同的特征给出特征位的方式来设置这个u2大小的区域。我们的Class中两个方法的这个区域的0X0001(橙色列)实际代表了特征位信息是0000000000000001,即它们的特征都是ACC_PUBLIC(public方法)。

6.2.2 name_index(u2)是方法名在常量池中的索引序号。

我们的Class中,method_#1的0X0005(蓝色列)指向的常量池中的#7号常量即,而。method_#2的0X000B(蓝色列)指向的常量池中的#11号常量即inc。

6.2.3 descriptor_index(u2)是方法描述符在常量池中的索引序号。

我们的Class中,method_#1的0X0008(青色列)指向的常量池中的#8号常量即()V,而。method_#2的0X000C(青色列)指向的常量池中的#12号常量即()I。

6.2.4 字段的属性表是该方法的属性表:

6.2.4.1 attributes_count(u2):方法属性表的属性数量。

6.2.4.2 attributes(attributes_count个attribute_info):方法属性表的属性信息,方法属性有自己定义的结构,方法中主要使用的属性包括最重要的Code(方法的字节码指令,没有方法执行体的接口和抽象类是没有这个属性的)、Exceptions(声明方法抛出的异常)、Depreciated(@Depreciated修饰的方法表示弃用)、Signature(泛型参数记录的泛型签名信息)等,他们都有各自定义的结构。这里我们具体来看一下最重要的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

attribute_count

1

attribute_info

attributes

attribute_count

a. attribute_name_index(u2):属性名在常量池中的索引序号,Code属性最终找到的常量肯定是Code。

b. attribute_length(u4):该属性的长度。

c. max_stack(u2):该方法的操作数栈最大深度。

d. max_locals(u2):该方法的局部变量表的大小。

e. code_length(u4):字节码指令的大小

f. code(exception_table_length个u1):字节码。

g. exception_table_length(u2):异常表大小。

h. exception_table(exception_table_length个exception_info):异常表大小。

i. attributes_count(u2):方法属性表的大小。

j. attributes(attribute_count个attribute_info):方法属性表。

接下来用我们Class的两个方法来详细说明Code属性:

15341733865157.jpg

method_#1方法:

i. attributes_count = 1

ii. attributes:

a. attribute_name_index:常量0X0009即Code。

b. attribute_length:29(0X0000001D),即下一位起后的29u都是这个属性。

c. max_stack:1(0X0001)。

d. max_locals:1(0X0001)。

e. code_length:5(0X00000005)。

f. code:0X2AB70001B1。(字节码指令的具体含义鉴于与class结构是相对独立的主题不再详述,后续会再单独深入介绍)

g. exception_table_length:0(OX0000)。

h. exception_table:无。

i. attributes_count:1(0X0001)。

j. attributes:

attribute_name_index:LineNumberTable(0X000A),说明这是一个用于记录源码行号和字节码行号映射的属性表。

attribute_length:6(0X00000006).

attribute:LineNumberTable属性表的内部结构:

line_number_table_length:1(0X0001)。

line_number_index:0:3(0X00000003)。

method_#2方法的分析方式如上类似不再赘述。

Class字节码的结构为什么这么设计

乍一看来上面的结构让人很难快速理解,但是如果理解JVM的字节码结构的设计目的就可以加深理解了。

JVM的字节码结构其实是一种由字节码堆砌的表型结构,充分定义占位的结构可以无歧义地将它想要表达的原义还原回去。作为二进制结构主要的表达方式,只要定义好占位情况,表型结构可以通过层层嵌套定义来实现更为复杂的结构、并且可以实现良好的拓展。

比如上面的介绍的方法信息通过方法数量定义了这个表的大小,而每个表entry内部可以再有自己的定义,比如方法信息中还可以包含属性表(即在方法表内部再嵌套一层表),比如这里定义了Code属性表,而Code属性表自身又有良好的表结构定义,这个表内部除了一些一维的字段(比如index、count等不能拓展的字段)外,还有额外的exception_table,但是因为有exception_table_length的表大小限制就可以无歧义地还原回去,此外还有attribute_info,但是因为有attribute_count的表大小限制也可以无歧义地还原回去。用下面的思维导图我们可以直观地看出来这种良好的定义,图中加入了每个一维节点的占位大小:

class%20format.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值