资讯专栏INFORMATION COLUMN

完全图解JVM Class文件结构

ygyooo / 1283人阅读

摘要:对一个文件的字节码进行逐行的分析是理解文件结构的最佳方式。本文的目的在于尽可能完整地拆解的字节码并将其分块分析,最终得到的图解结构希望可以帮助到你。字节码指令的具体含义鉴于与结构是相对独立的主题不再详述,后续会再多带带深入介绍。

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

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

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

> 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"

图解概况

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

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

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

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

1. magic

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

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

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字节码)。

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

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

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

3.2 constant_pool

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

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

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#4CONSTANT_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->Idouble->D,引用类型java/lang/Object -> Ljava/lang/Object

我们Class字节码中的#15#16CONSTANT_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字节码中的#2CONSTANT_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字节码中的#1CONSTANT_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后的默认设置项)。

4.2 this_class

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

4.3 super_class

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

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

4.4.2 interface(interface_count个u2):说明接口的类索引。
5.字段信息 5.1 field_count(u2):字段数量

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

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表示这个类有两个方法。

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

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_#10X0005(蓝色列)指向的常量池中的#7号常量即,而。method_#20X000B(蓝色列)指向的常量池中的#11号常量即inc

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

我们的Class中,method_#10X0008(青色列)指向的常量池中的#8号常量即()V,而。method_#20X000C(青色列)指向的常量池中的#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属性:

method_#1方法:

i. attributes_count = 1

ii. attributes:

a. attribute_name_index:常量0X0009Code

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的表大小限制也可以无歧义地还原回去。用下面的思维导图我们可以直观地看出来这种良好的定义,图中加入了每个一维节点的占位大小:

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76736.html

相关文章

  • Java经典

    摘要:请注意,我们在聊聊单元测试遇到问题多思考多查阅多验证,方能有所得,再勤快点乐于分享,才能写出好文章。单元测试是指对软件中的最小可测试单元进行检查和验证。 JAVA容器-自问自答学HashMap 这次我和大家一起学习HashMap,HashMap我们在工作中经常会使用,而且面试中也很频繁会问到,因为它里面蕴含着很多知识点,可以很好的考察个人基础。但一个这么重要的东西,我为什么没有在一开始...

    xcold 评论0 收藏0
  • 【备战春招/秋招系列】美团面经总结基础篇 (附详解答案)

    摘要:不同于个人面经,这份面经具有普适性。我在前面的文章中也提到了应该怎么做自我介绍与项目介绍,详情可以查看这篇文章备战春招秋招系列初出茅庐的程序员该如何准备面试。是建立连接时使用的握手信号。它表示确认发来的数据已经接受无误。 showImg(https://segmentfault.com/img/remote/1460000016972448?w=921&h=532); 该文已加入开源文...

    Leck1e 评论0 收藏0
  • 后端知识拓展 - 收藏集 - 掘金

    摘要:阻塞,非阻塞首先,阻塞这个词来自操作系统的线程进程的状态模型网络爬虫基本原理一后端掘金网络爬虫是捜索引擎抓取系统的重要组成部分。每门主要编程语言现未来已到后端掘金使用和在相同环境各加载多张小图片,性能相差一倍。 2016 年度小结(服务器端方向)| 掘金技术征文 - 后端 - 掘金今年年初我花了三个月的业余时间用 Laravel 开发了一个项目,在此之前,除了去年换工作准备面试时,我并...

    CoderBear 评论0 收藏0
  • 后端知识拓展 - 收藏集 - 掘金

    摘要:阻塞,非阻塞首先,阻塞这个词来自操作系统的线程进程的状态模型网络爬虫基本原理一后端掘金网络爬虫是捜索引擎抓取系统的重要组成部分。每门主要编程语言现未来已到后端掘金使用和在相同环境各加载多张小图片,性能相差一倍。 2016 年度小结(服务器端方向)| 掘金技术征文 - 后端 - 掘金今年年初我花了三个月的业余时间用 Laravel 开发了一个项目,在此之前,除了去年换工作准备面试时,我并...

    Carl 评论0 收藏0
  • Java编程基础01——计算机基础

    摘要:外部存储器可用于长期保存大量程序和数据,其成本低容量大,但速度较慢。 1_计算机概述(了解) A:什么是计算机?计算机在生活中的应用举例 计算机(Computer)全称:电子计算机,俗称电脑。是一种能够按照程序运行,自动、高速处理海量数据的现代化智能电子设备。由硬件和软件所组成,没有安装任何软件的计算机称为裸机。常见的形式有台式计算机、笔记本计算机、大型计算机等。 应用举例 ...

    xiangzhihong 评论0 收藏0

发表评论

0条评论

ygyooo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<