Java类文件结构

2020/10/21 posted in  JVM

Class文件对应着唯一一个类或接口的定义信息,但是类或接口并不一定都得定义在文件里(也可以通过类加载器直接生成)。

Class文件是一组以8位字节为基础单位的二进制流。

Java虚拟机规范中规定 ,Class文件格式采用一种类似于C语言结构体的结构来存储数据,有两种数据类型:无符号数和表。

  • 无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1、2、4、8 个字节。
  • 无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。

表是有多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以'_info'结尾。用于描述有层次关系的符合结构的数据。

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行,u4 = 4个字节,前4个字节是魔数,CAFEBABE,每个class文件都是以这个4个字节开始的;
第2行,u2 = 2个字节,代表的是次版本号;
第3行,u2 = 2个字节,代表的是主版本号;
第4行,u2 = 2个字节,constant_pool_conunt 代表的是常量池的容量,常量个数;
第5行,cp_info,constant_pool,代表的是常量池中每个常量的信息,常量个数从1开始到constant_pool_conunt - 1;
第6行,u2 = 2个字节,表示的是access_flags访问标志;
第7行,2个字节,表示的是this_class,类索引;
第8行,2个字节,表示的是super_class,父类索引;
第9行,2个字节,接口索引个数
第10行,接口索引集合
第11行,fields_count,字段集合个数
第12行,fields,字段集合
第13行,2个字节,methods_count,方法个数
第14行,方法集合
第15行,属性表集合个数
第16行,属性表集合信息

分析字节码文件

这是一个字节码文件

CA FE BA BE 00 00 00 34  00 1D 0A 00 05 00 18 09
00 04 00 19 09 00 04 00  1A 07 00 1B 07 00 1C 01
00 04 6E 61 6D 65 01 00  12 4C 6A 61 76 61 2F 6C
61 6E 67 2F 53 74 72 69  6E 67 3B 01 00 03 61 67
65 01 00 01 49 01 00 06  3C 69 6E 69 74 3E 01 00
03 28 29 56 01 00 04 43  6F 64 65 01 00 0F 4C 69
6E 65 4E 75 6D 62 65 72  54 61 62 6C 65 01 00 07
67 65 74 4E 61 6D 65 01  00 14 28 29 4C 6A 61 76
61 2F 6C 61 6E 67 2F 53  74 72 69 6E 67 3B 01 00
07 73 65 74 4E 61 6D 65  01 00 15 28 4C 6A 61 76
61 2F 6C 61 6E 67 2F 53  74 72 69 6E 67 3B 29 56
01 00 06 67 65 74 41 67  65 01 00 03 28 29 49 01
00 06 73 65 74 41 67 65  01 00 04 28 49 29 56 01
00 0A 53 6F 75 72 63 65  46 69 6C 65 01 00 0B 50
65 72 73 6F 6E 2E 6A 61  76 61 0C 00 0A 00 0B 0C
00 06 00 07 0C 00 08 00  09 01 00 10 63 6F 6D 2F
68 69 74 6F 6C 2F 50 65  72 73 6F 6E 01 00 10 6A
61 76 61 2F 6C 61 6E 67  2F 4F 62 6A 65 63 74 00
21 00 04 00 05 00 00 00  02 00 02 00 06 00 07 00
00 00 02 00 08 00 09 00  00 00 05 00 01 00 0A 00
0B 00 01 00 0C 00 00 00  1D 00 01 00 01 00 00 00
05 2A B7 00 01 B1 00 00  00 01 00 0D 00 00 00 06
00 01 00 00 00 03 00 01  00 0E 00 0F 00 01 00 0C
00 00 00 1D 00 01 00 01  00 00 00 05 2A B4 00 02
B0 00 00 00 01 00 0D 00  00 00 06 00 01 00 00 00
09 00 01 00 10 00 11 00  01 00 0C 00 00 00 22 00
02 00 02 00 00 00 06 2A  2B B5 00 02 B1 00 00 00
01 00 0D 00 00 00 0A 00  02 00 00 00 0D 00 05 00
0E 00 01 00 12 00 13 00  01 00 0C 00 00 00 1D 00
01 00 01 00 00 00 05 2A  B4 00 03 AC 00 00 00 01
00 0D 00 00 00 06 00 01  00 00 00 11 00 01 00 14
00 15 00 01 00 0C 00 00  00 22 00 02 00 02 00 00
00 06 2A 1B B5 00 03 B1  00 00 00 01 00 0D 00 00
00 0A 00 02 00 00 00 15  00 05 00 16 00 01 00 16
00 00 00 02 00 17 

每个class文件的头4个字节 CA FE BA BE 称为魔数,也就是上面的图class文件格式中第一个,u4 magic。它的唯一作用 是确定这个文件是否 为一个能被虚拟机接受的class文件。

下两个字节 00 00 代表的是 次版本号, 十进制数 = 0
下两个字节 00 34 代表的是 主版本号,十进制数=52

主次版本号表示的是jdk的版本,52对应的是1.8, 51对应的是1.7

之前遇到的一个错误,编译SpringBoot代码的时候用的1.8,在1.7的java环境中执行的话,就会出现这个52的错误。
Unsupported major.minor version 52.0

--
###常量池及常量

下两个字节 00 1D 代表的是 常量池中常量的个数,
常量个数从1开始的,0x1D = 29,最终结果是28,索引是1--28
第0号常量池被JVM占用,表示的是什么都不引用。

常量分类
字面量类型,比较接近于Java语言层面的常量概念,
符号引用类型,属于编译原理方面的概念
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符

Java代码在编译的时候,是动态链接的,也就是说在Class文件中不会保存各个方法、字段的最终内存信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。
虚拟机运行的时候,需要从常量池获得对应的符号引用,在类创建或运行时解析、翻译到具体的内存地址中。

常量池中每项常量都是一个表,表开始的第一位是一个u1类型的标志位,tag,代表当前这个常量属于哪种类型常量。

每个常量的结构类似这样的

CONSTANT_Class_info{
    u1 : tag = 7
    u2 : name_index 
}


第一个常量
第一个字节 0A
0x0A = 10
在常量池项目类型中,标志位为10的是CONSTANT_Method_info,表示类中 方法的符号引用
后面还有4个字节,前两个字节表示是执行声明字段的类或接口描述符CONSTANT_Class_info的索引项
后两个字节表示的是指向字段描述符CONSTANT_NamaeAndType 的索引项。
前两个字节 00 05
0x00 05 = CONSTANT_Fieldref_info5 ,表示引用的是第5个常量
后两个字节
0x00 18 = 24,表示引用的是第24个常量
索引为5的常量代表的是个类,索引为24的常量代表的是个方法名,也就是说这个方法引用的是 #5.#24

常量池中第一个常量:
// #1 = Methodref #5.#24 // java/lang/Object."":()V

()V 表示的是方法 没有入参,没有返回值


第二个常量
第一个字节 09
0x09 = 9
表示的是 CONSTANT_Fieldref_info,其中的结构是 u1 u2 u2
第一个字节就是标志位9
后面4个字节中,前两个字节表示声明字段的类或者接口描述符CONSTANT_Class_info的索引项
后两个字节表示指向字段描述符CONSTANT_NameAndType的索引项

前两个字节
0x00 04 = 4,表示引用常量池中第4个常量,这个常量代表的是一个类

后两个字节
0x00 19 = 25,表示引用常量池中第25个常量,这个常量代表一个属性名。

常量池中第二个常量:
// #2 = Fieldref #4.#25 // com/hitol/Person.name:Ljava/lang/String;

代表的就是 Person类中name属性。


第三个常量

09 = CONSTANT_Fieldref_info

0x00 04 = 4
0x00 1A = 26

// #4.#26

// #3 = Fieldref #4.#26 // com/hitol/Person.age:I


第四个常量

07 = CONSTANT_Class_info
0x00 1B = 27

// #27

// #4 = Class #27 // com/hitol/Person


第五个常量

07 = CONSTANT_Class_info

0x00 1C = = 28

// #28
// #5 = Class #28 // java/lang/Object


第六个常量

01 = CONSTANT_Utf8_info
length = 0x00 04 = 4,长度为4
bytes = 长度为4的字符串,6E 61 6D 65

http://www.ab126.com/goju/1711.html
在线转换后,

6E 61 6D 65 转换后表示的字符串是name.

// #6 = Utf8 name


第七个常量
01 = CONSTANT_Utf8_info
length = 0x00 12 = 18
bytes = 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B

转换后是 L j a v a / l a n g / S t r i n g ;

顺便提一下,Java中定义的变量或方法名,最大长度就是lenght的最大值,两个字节能表示的最大值就是65535。


第八个常量
01 = CONSTANT_Utf8_info
length = 3
bytes = a g e


第九个常量
01 = CONSTANT_Utf8_info
length = 1
bytes = I


第十个常量
01 = CONSTANT_Utf8_info
length = 6
bytes = < i n i t >


第十一个
01 = CONSTANT_Utf8_info
length =3
bytes = ( ) V


第十二个
01 = CONSTANT_Utf8_info
length =4
bytes =C o d e


第十三个
01 = CONSTANT_Utf8_info
length = 16
bytes =L i n e N u m b e r T a b l e


第十四个变量
01 = CONSTANT_Utf8_info
length = 07
bytes =67 65 74 4E 61 6D 65 = getName


第十五个变量
01 = CONSTANT_Utf8_info
length = 20
bytes = 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B =( ) L j a va / l a n g / S t r i n g ;

--
第十六个
01 = CONSTANT_Utf8_info
length = 07
bytes = 73 65 74 4E 61 6D 65 = s e t N a m e


第十七个
01 = CONSTANT_Utf8_info
length = 07
bytes = 28 4C 6A 61 76
61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56 = ( L j a v
a / l a n g / S t r i n g ; ) V

--
第十八个
01 = CONSTANT_Utf8_info
length = 06
bytes = 67 65 74 41 67 65 = g e t A g e


第十九个
01 = CONSTANT_Utf8_info
length = 03
bytes = 28 29 49 = ( ) I

--
第二十个
01 = CONSTANT_Utf8_info
length = 06
bytes = 73 65 74 41 67 65 = s e t A g e

--
第二十一个
01 = CONSTANT_Utf8_info
length = 04
bytes = 28 49 29 56 = ( I ) V

--
第二十二个
01 = CONSTANT_Utf8_info
length = 0A =10
bytes = 53 6F 75 72 63 65 46 69 6C 65 = S o u r c e F i l e

--
第二十三个
01 = CONSTANT_Utf8_info
length = 0B = 11
bytes = 50 65 72 73 6F 6E 2E 6A 61 76 61 = P e r s o n . j a v a

--
第二十四个
tag = 12 代表的是 CONSTANT_NameAndType_info
该结构还有4个字节,前两个字节表示指向该字段或方法名称常量项的索引,后两个字节表示指向该字段或方法描述符常量项的索引。

前两个字节:00 0A = 10
后两个字节:00 0B = 11
// #24 = #10:#11

--
第二十五个
tag = 0C = 12 = CONSTANT_NameAndType_info
前两个字节:00 06 = 6
后两个字节:00 07 = 7
// #25 = #6:#7

--
第二十六个
tag = 0C = CONSTANT_NameAndType_info
00 08
00 09
// #26 = #8:#9

--
第二十七个
tag = 01 = CONSTANT_Utf8_info
length = 10 = 16
bytes = 63 6F 6D 2F 68 69 74 6F 6C 2F 50 65 72 73 6F 6E = c o m / h i t o l / P e r s o n

--
第二十八个
tag = 01 = CONSTANT_Utf8_info
length = 10 = 16
bytes = 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 = j a v a / l a n g / O b j e c t


access_flags

常量池中28个常量已经分析完成,根据class文件格式表中的类型,下面两个字节表示access_flags。

访问标志

0x00 21 = ACC_PUBLIC + ACC_SUPER
ACC_PUBLIC 、 ACC_SUPER转换为2进制后执行|运算,

   000001
|  100000
----------
   100001

100001对应的16进制就是21。


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

类索引用于确定本类的全限定名、
父类索引用于切丁父类的全限定名。
由于Java单继承的,所以父类索引只有一个。

类索引的结构是CONSTANT_Class_info
类索引00 04,常量池中第4个常量,
父类索引00 05,常量池中第5个常量。

接口索引集合就是用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句的接口顺序从左到右排列在接口索引集合中。
接口索引集合 00 00 表示个数为0

字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

可以包括的信息有:

  1. 字段的作用域
    public\private\protected修饰符

  2. 是实例变量还是类变量
    static修饰符

  3. 可变性
    final

  4. 并发可见性
    volatile修饰符,是否强制从主内存读写

  5. 可否被序列化
    transient修饰符

  6. 字段数据类型
    基本类型、对象、数组

  7. 字段名称

字段表结构


字段修饰符在access_flags中,其访问标志为

access_flags后是两个索引,name_index 和 descriptor_index。他们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符,

--
第一个字段

根据图中所示,前两个字节表示access_flags。
0x00 02 = ACC_PRIVATE 表示这个字段是private修饰的。

接下来两个字节表示name_index,代表字段的简单名称。
00 06 表示常量池中第6个常量 = name。

接下来两个字节表示descriptior_index ,代表方法的描述符。
00 07 = 常量池中第7个常量= Ljava/lang/String;

简单名称是指没有类型和参数修饰的方法或者字段名称
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。L表示是一个对象,后面跟着对象的全限定名。

对于数组类型,每一维度将使用一个前置的'['字符描述,
例如:定义了一个String类型的二维数组
String [][] 将被记录为 “[[Ljava.lang.String;”

第一个字段就表示为 private String name;

字段表都包含的固定数据项目到descriptor_index为止就结束了,不过在之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。

在本例中是00 00 表示没有额外信息。


第二个字段
00 02 = ACC_PRIVATE 表示private

00 08 表示常量池中第8个常量 age
00 09 表示第9个常量 I
00 00 表示没有额外信息

I 表示是int类型。

方法表集合



下面两个字节00 05 表示的是方法的个数。


第一个方法

前两个字节 00 01 表示访问标志access_flags,表示public
接下来两个字节 00 0A 表示名称索引name_index,表示常量池中第10个常量,
接下来两个字节00 0B 表示描述符索引descriptor_index,表示常量池中第11个常量,()V
00 01 属性个数
00 0C = 12 Code

Code属性见下面属性表集合。

这个方法表示的是无参构造方法。


第二个方法
00 01 = public
00 0D = 第12个常量 = Code

Code属性见下面属性表集合


属性表集合

Code属性
Java程序方法中的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性内。

Code属性出现在方法表的属性集合中,但并非所有的方法表都必须存在这个属性,接口或抽象类中的方法就不存在Code属性。
如果有Code属性,其结构为

前两个字节表示的是attribute_name_index是一项指向CONSTANT_Utf8_info的常量的索引,常量值固定为“Code”,代表了该属性的属性名称,

接下来4个字节指示了属性值的长度,
接下来2个字节,max_stack,代表了操作数栈深度的最大值
接下来2个字节,max_locals,代表了局部变量表所需的存储空间。单位是Slot,Slot是虚拟机为局部变量分配内存使用的最小单位。
接下来4个字节,code_length,字节码长度,虽然是4个字节,理论上最大值2^32 - 1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即它实际上只是用了u2的长度,如果超过这个限制,javac编译器也会拒绝编译。
接下来1个字节,code,用于存储字节码指令的一系列字节流,表示一个指令。字节码长度个字节,表示几条指令。
接下来2个字节,exception_table_length ,异常表长度
exception_info 异常表信息
异常表属性结构

包含4个字段,这些字段的含义为:如果当字节码在第start_pc行到第end_pc(不包含end_pc)行出现了类型为catch_type或者其子类的异常,则转到第handler_pc行继续处理。当catch_type为0时,代表任何异常情况都需要转向到handler_pc处理.

接下来2个字节,attributes_count
attributes_info

第一个方法中Code属性:
00 0C = Code
00 00 00 1D 属性值的长度为29
00 01 操作数栈深度最大值为1
00 01 局部变量表空间为1
00 00 00 05 code_length = 5,字节码长度是5

2A B7 00 01 B1

读入2A

0x2A = aload_0 指令,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。

读入B7

指令为
invokespecial,这条指令的作用是以栈顶的reference的数据所执行的对象作为方法接受者,调用此方法的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型的常量,即此方法的方法符号引用。

读入00 01
这个是上一条指令的参数,常量池中第一个常量,表示java/lang/Object."":()V,实例构造器init方法的符号引用。

读入B1

查表得对应的指令为return,含义是返回此方法,并且返回值为void,这条方法指令执行后,当前方法结束。

00 00 表示异常表长度为0


00 01 attributes_count = 1

接下来两个字节
00 0D = 13 = LineNumberTable

LineNumberTable
LineNumberTable,用于描述Java源码行号与字节码行号之间的对应关系,它并不是运行时必须的属性,但 默认会生成到Class文件之中,
主要应用就是,当程序抛出异常时,堆栈中显示的错误行号。


LocalVariableTable 局部变量表
JVM中,Java虚拟机栈执行的时候创建栈帧,用来存放局部变量表,就是这个东西。
用于描述栈帧中局部变量表的中的变量与Java源码中定义的变量之间的关系。


idea中有个插件 jclasslib ,可以直观、友好的查看class文件中的内容


使用javap输出常量表

javap -verbose XXX.class

  Last modified 2020-6-28; size 550 bytes
  MD5 checksum 0a4eff80df81adb2a2ee5ac6206f461c
  Compiled from "Person.java"
public class com.hitol.Person
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#25         // com/hitol/Person.name:Ljava/lang/String;
   #3 = Fieldref           #4.#26         // com/hitol/Person.age:I
   #4 = Class              #27            // com/hitol/Person
   #5 = Class              #28            // java/lang/Object
   #6 = Utf8               name
   #7 = Utf8               Ljava/lang/String;
   #8 = Utf8               age
   #9 = Utf8               I
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               getName
  #15 = Utf8               ()Ljava/lang/String;
  #16 = Utf8               setName
  #17 = Utf8               (Ljava/lang/String;)V
  #18 = Utf8               getAge
  #19 = Utf8               ()I
  #20 = Utf8               setAge
  #21 = Utf8               (I)V
  #22 = Utf8               SourceFile
  #23 = Utf8               Person.java
  #24 = NameAndType        #10:#11        // "<init>":()V
  #25 = NameAndType        #6:#7          // name:Ljava/lang/String;
  #26 = NameAndType        #8:#9          // age:I
  #27 = Utf8               com/hitol/Person
  #28 = Utf8               java/lang/Object
{
  public com.hitol.Person();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 9: 0

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 13: 0
        line 14: 5

  public int getAge();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #3                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 17: 0

  public void setAge(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #3                  // Field age:I
         5: return
      LineNumberTable:
        line 21: 0
        line 22: 5
}
SourceFile: "Person.java"

TODO

类的创建和动态连接

--
参考
《深入理解Java虚拟机》

Java Virtual Machine Specification