Class文件结构

x33g5p2x  于2021-12-18 转载在 其他  
字(7.5k)|赞(0)|评价(0)|浏览(284)

Class文件结构

Java技术能够一直保持非常好的向后兼容性,这点Class文件结构的稳定性功不可没。Java目前已经发展到JDK14,但是class文件结构的内容,绝大部分在JDK1.2时代就已经定义好了。虽然JDK1.2的内容比较古老,但是java发展经历了十余个大版本,但是每次基本上只是在原有结构基础上新增内容、扩充功能,并未对定义的内容做修改。

Class文件是一组以8位字节为基础单位的二进制流(实际上任何文件都是以二进制流形式存在的)。

工欲善其事,必先利其器。下面先介绍几个实用的工具。

工具介绍

Sublime/HexView:用来查看文件的16进制内容。

javap:JDK自带的反编译工具。能将.class字节码文件解析成可读的文件格式。

jclasslib:在javap的基础上提供了可视化界面,能够更加直观的查看字节码中的内容。它还分门别类的对类中的各个部分进行了整理,非常的人性化。同时,它还提供了Idea的插件,你可以从plugins中搜索到它。

Class文件格式

演示代码:

package com.morris.jvm.bytecode;

public class ByteCode {
}

对应的字节码如下:

cafe babe 0000 0034 0010 0a00 0300 0d07
000e 0700 0f01 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162
6c65 0100 0474 6869 7301 0022 4c63 6f6d
2f6d 6f72 7269 732f 6a76 6d2f 6279 7465
636f 6465 2f42 7974 6543 6f64 653b 0100
0a53 6f75 7263 6546 696c 6501 000d 4279
7465 436f 6465 2e6a 6176 610c 0004 0005
0100 2063 6f6d 2f6d 6f72 7269 732f 6a76
6d2f 6279 7465 636f 6465 2f42 7974 6543
6f64 6501 0010 6a61 7661 2f6c 616e 672f
4f62 6a65 6374 0021 0002 0003 0000 0000
0001 0001 0004 0005 0001 0006 0000 002f
0001 0001 0000 0005 2ab7 0001 b100 0000
0200 0700 0000 0600 0100 0000 0300 0800
0000 0c00 0100 0000 0500 0900 0a00 0000
0100 0b00 0000 0200 0c

各个数据项严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。

无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表1个字节(一个字节是由两位16进制数组成)、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,它由下面几个部分组成:

类型名称解释数量
u4magic魔数1
u2minor_version次版本号1
u2major_version主版本号1
u2constant_pool_count常量池常量个数1
constant_pool_infoconstant_pool常量池constant_pool_count -1
u2access_flags访问标记1
u2this_class类索引1
u2superclass父类索引1
u2interfaces_count接口索引数量1
u2interfaces接口内容interfaces_count
u2field_count字段表字段数量1
field_infofields字段表field_count
u2methods_count方法表方法数量1
method_infomethods方法表methods_count
u2attributes_count属性表属性数量1
attribute_infoattributes属性表attributes_count

魔数与版本号

每个Class文件的头4个字节0xcafe babe称为魔数(Magic Number),它的作用是确定这个文件是否为一个能被虚拟机接受的Class文件。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

紧接着魔数后的4个字节存储的是Class文件的版本号:第5、6个字节是次版本号(Minor Version),第7、8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布,主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

0x0000表示次版本号为0,0x0034表示主版本号为52,也就是JDK1.8。

下表列出了各个版本JDK的十六进制版本号信息:

JDK版本次版本号主版本号十进制
JDK1.10000002D45
JDK1.20000002E46
JDK1.30000002F47
JDK1.40000003048
JDK1.50000003149
JDK1.60000003250
JDK1.70000003351
JDK1.80000003452

常量池

常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。

而符号引用则属于编译原理方面的概念,包括了三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。

0x0010表示有15个常量(从1开始计数),15个常量的内容如下。

#1 = Methodref          #3.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // com/morris/jvm/bytecode/ByteCode
   #3 = Class              #15            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/morris/jvm/bytecode/ByteCode;
  #11 = Utf8               SourceFile
  #12 = Utf8               ByteCode.java
  #13 = NameAndType        #4:#5          // "<init>":()V
  #14 = Utf8               com/morris/jvm/bytecode/ByteCode
  #15 = Utf8               java/lang/Object

对应的二进制内容如下:

0a 0003 000d // Methodref #3 #13
07 000e // Class # 14
07 000f // Class #15
01 0006 3c696e69743e // Utf8 <init>
01 0003 282956 // Utf8 ()V
01 0004 436f6465 // Utf8 Code
01 000f 4c696e654e756d6265725461626c65 // Utf8 LineNumberTable
01 0012 4c6f63616c5661726961626c655461626c65 // Utf8 LocalVariableTable
01 0004 74686973 // Utf8 this
01 0022 4c636f6d2f6d6f727269732f6a766d2f62797465636f64652f42797465436f64653b // Utf8 Lcom/morris/jvm/bytecode/ByteCode;
01 000a 536f7572636546696c65 // Utf8 SourceFile
01 000d 42797465436f64652e6a617661 // Utf8 ByteCode.java
0c 0004 0005 // NameAndType #4 #5
01 0020 636f6d2f6d6f727269732f6a766d2f62797465636f64652f42797465436f6465 // Utf8 com/morris/jvm/bytecode/ByteCode
01 0010 6a6176612f6c616e672f4f626a656374 // Utf8 java/lang/Object

访问标志

用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

0x0021表示ACC_SUPPER和ACC_PUBLIC标志位为1。

具体的标志位以及标志的含义见下表:

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可设置
ACC_SUPER0x0020是否允许使用invokespedal字节码指令的新语义,invokespecial指令的语义在JDK1.2发生过改变,为了区别这条指令使用哪种语义,JDK1.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举

类索引、父类索引与接口索引集合

这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句后的接口顺序从左到右排列在接口索引集合中。

0x0002表示类的索引为#2。

0x0003表示父类的索引为#3。

0x0000表示类没有实现任何接口。

字段表集合

描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。

字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

0x0000表示类的字段个数为0。

字段表的每个字段用一个名为field_info的表来表示,field_info表的数据结构如下所示:

类型名称数量含义
u2access_flags1字段访问标识
u2name_index1字段名称索引项
u2descriptor_index1字段描述符索引项
u2attributes_count1属性表计数器
attribute_infoattributesattribute_count属性表

方法表集合

描述了方法的定义,但是方法里的Java代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”。

0x0001表示方法的个数。

0x0001表示方法的访问标识,ACC_PUBLIC。

0x0004表示方法名的索引为#4。

0x0005表示方法的描述符的索引为#5,也就是参数和返回值。

方法表中的每个方法都用一个method_info表示,其数据结构如下:

类型名称数量含义
u2access_flags1方法访问标识
u2name_index1方法名称索引项
u2descriptor_index1方法描述符索引项
u2attributes_count1属性表计数器
attribute_infoattributesattribute_count属性表

存储Class文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在Code属性表中。

0x0001表示属性表的计数为1。

属性表的表结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1attribute_infoattribute_length

Code 属性的表结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2max_stack1
u2max_locals1
u4code_length1
u1codecode_length
u2exception_table_length1
exception_infoexception_tableexception_table_length
u2attributes_count1
attribute_infoattributesattributes_count

0x0006对应常量池中的索引为6,Code。

0x0000 0x002f表示属性表的长度为2f。

0x0001表示操作数栈的最大深度为1。

0x0001表示本地变量表的槽slot数量为1。

0x0000 0005表示字节码的长度为5。

0x2a对应的字节码指令为aload_0,将本地变量表的第一个值push到栈顶。

0xb7 0001对应的字节码指令为invokespecial #1 <java/lang/Object.<init>>,调用父类的构造方法。

0xb1对应的字节码指令为return,将栈顶的值返回。

0x0000表示异常表的数量。

0x0002表示属性表的长度。

属性表的表结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1attribute_infoattribute_length

0x0007表示属性名的索引为#7,LineNumberTable。

0x00000006表示属性长度为6个字节。

0x0001表示LineNumberTable的长度为1。

LineNumberTable表的表结构如下图所示:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2line_number_table_length1
line_number_infoline_number_tableline_number_table_length

接着跟着1个line_number_info类型的数据,下面是line_number_info表的结构,其包含了start_pc和line_number两个u2类型的数据项。前者是字节码行号,后者是Java源码行号。

类型名称数量
u2start_pc1
u2line_number1

0x0000 :start_pc,字节码行号。
0x0003:line_number,Java源码行号。

接下来是局部变量表:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2local_variable_table_length1
local_variable_infolocal_variable_tablelocal_variable_table_length

0x0008表示属性名的索引为#8,LocalVariableTable。
0x0000000c表示局部变量表的长度为12。

local_variable_info的结构:

类型名称数量
u2start_pc1
u2length1
u2name_index1
u2descriptor_index1
u2index1

0x0001表示局部变量的个数。

0x0000表示这个局部变量生命周期开始的字节码偏移量。

0x0005表示这个局部变量作用范围的长度。

0x0009表示这个局部变量的名称的索引为#9,也就是this。

0x000a表示这个局部变量的描述符的索引为#10,对应为Lcom/morris/jvm/bytecode/ByteCode。

0x0000表示这个局部变量在局部变量表中槽的位置。

属性表集合

最后一个就是属性表集合。

0x0001表示属性表的长度为1。

接下来是SourceFile,SourceFile属性的表结构如下:

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2sourcefile_infoattribute_length

0x000b表示属性名称的索引为#11,也就是SourceFile。

0x00000002表示属性长度为2。

0x000c表示SourceFil 的常量池索引,即该字节码文件的源文件名称ByteCode.java。

相关文章