Java虚拟机之Class类文件结构

944 阅读17分钟

欢迎关注微信公众号: JueCode

正如有一句名言:代码编译的结果从本地机器码变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。 Java语言为什么能write once, run anywhere? 这个其实是因为和各种不同平台相关的虚拟机,这些虚拟机都可以载入和执行同平台无关的字节码。今天我们就来学习下Class类文件结构的一些知识。

1.类文件结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符。Class文件中只有两种数据类型:无符号数和表

无符号数属于基本的数据类型,有u1, u2, u4, u8,分别代表1个字节、2个字节、4个字节和8个字节的无符号数。

表则是由多个无符号数或者其他表复合而成的数据类型。所有表都习惯以_info结尾。目前有14个表格类型:

名称 解释
CONSTANT_utf8_info utf-8编码的字符串
CONSTANT_Integer_info 整形字面量
CONSTANT_Float_info 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 类中方法的符号引用
CONSTANT_Interface_Methodref_info 接口中方法的符号引用
CONSTANT_NameAndType_info 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 表示方法句柄
CONSTANT_MethodType_info 表示方法类型
CONSTANT_InvokeDynamic_info 表示一个动态方法调用点

整个Class文件是有顺序的,整个格式如下面的表格:

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attribute_count 1
attribute_info attributes attribute_count

Class文件格式都是严格按照上面顺序,当然有的类型可能没有,比如一个类没有实现接口,那么interfaces_count 的数值就为0,后面的interfaces就没有,以此类推。

下面我们看一个简单的栗子来分析Class文件结构。

package org.fenixsoft.clazz;

public class TestClass{
	private int m;
	public int inc(){
		return m + 1;
	}
}

通过javac TestClass 可以编译得到TestClass.class文件:

cafe babe 0000 0034 0013 0a00 0400 0f09 0003 0010 
0700 1107 0012 0100 016d 0100 0149 0100 063c 696e 
6974 3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 
696e 654e 756d 6265 7254 6162 6c65 0100 0369 6e63 
0100 0328 2949 0100 0a53 6f75 7263 6546 696c 6501 
000e 5465 7374 436c 6173 732e 6a61 7661 0c00 0700 
080c 0005 0006 0100 1d6f 7267 2f66 656e 6978 736f 
6674 2f63 6c61 7a7a 2f54 6573 7443 6c61 7373 0100 
106a 6176 612f 6c61 6e67 2f4f 626a 6563 7400 2100 
0300 0400 0000 0100 0200 0500 0600 0000 0200 0100 
0700 0800 0100 0900 0000 1d00 0100 0100 0000 052a 
b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003 
0001 000b 000c 0001 0009 0000 001f 0002 0001 0000 
0007 2ab4 0002 0460 ac00 0000 0100 0a00 0000 0600 
0100 0000 0600 0100 0d00 0000 0200 0e

现在看这个十六进制class文件肯定一脸懵*,按照格式来划分:

//TestCalss.class
cafe babe //MagicNumber
0000 //minor_version
0034 //major_version  52 --- jdk 1.8   (50 --- jdk 1.6)
0013 //constant_pool_count 19(从1开始)
0a00 0400 0f09 0003 0010 0700 1107 0012 0100 016d 0100 0149 0100 063c 696e 6974 
3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 696e 654e 756d 6265 7254 6162 6c65 
0100 0369 6e63 0100 0328 2949 0100 0a53 6f75 7263 6546 696c 6501 000e 5465 7374 
436c 6173 732e 6a61 7661 0c00 0700 080c 0005 0006 0100 1d6f 7267 2f66 656e 6978 
736f 6674 2f63 6c61 7a7a 2f54 6573 7443 6c61 7373 0100 106a 6176 612f 6c61 6e67 
2f4f 626a 6563 74 //常量池 18个
0021 //access_flags
0003 //this_class
0004 //super_class
0000 //interfaces_count
0001 //fields_count
0002 0005 0006 0000 //fields
0002 //methods_count
0001 0007 0008 0001 0009	//methods
0000001d 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 01 00 0a 00 00 00 06 00 
01 00 00 00 03 0001 000b 000c 0001 0009 0000 001f 0002 0001 0000 0007 2ab4 0002 
0460 ac00 0000 0100 0a00 0000 0600 0100 0000 0600 0100 0d00 0000 0200 0e//Code

接下来对照着这个十六进制class文件和上面的文件格式来挨个拆解。

2.MagicNumber/version

首先看到前面三个选项,分别是MagicNumber minor_version major_version 其中MagicNumber是固定4个字节的常量0xcafebabe.

//TestCalss.class
cafe babe //MagicNumber
0000 //minor_version
0034 //major_version  52 --- jdk 1.8   (50 --- jdk 1.6)

minor_version和major_version描述的是jdk的版本,十六进制的34转化为十进制就是52,也就是对应jdk 1.8版本,50对应的是jdk 1.6版本,一次类推。

紧接着主次版本号之后的是常量池。

3.常量池

常量池可以理解为Class文件中的资源仓库,是占用Class文件空间最大的数据项目之一。 常量池中常量的数量是不固定的,所以在常量池入口放置一项u2类型的数据代表常量池容易计数值,有个点需要注意这个容量计数是从1而不是0开始。第0项常量空出来是表达“不引用任何一个常量池项目”。 看下我们的栗子, 0x0013即十进制的19,代表常量池中有18项常量

0013 //constant_pool_count 19(从1开始)
0a00 0400 0f09 0003 0010 0700 1107 0012 0100 016d 0100 0149 0100 063c 696e 6974 
3e01 0003 2829 5601 0004 436f 6465 0100 0f4c 696e 654e 756d 6265 7254 6162 6c65 
0100 0369 6e63 0100 0328 2949 0100 0a53 6f75 7263 6546 696c 6501 000e 5465 7374 
436c 6173 732e 6a61 7661 0c00 0700 080c 0005 0006 0100 1d6f 7267 2f66 656e 6978 
736f 6674 2f63 6c61 7a7a 2f54 6573 7443 6c61 7373 0100 106a 6176 612f 6c61 6e67 
2f4f 626a 6563 74 //常量池 

常量池中主要存放两大类常量:字面量和符号引用。 字面量接近Java中的常量概念,比如字符串,声明为final的常量值等。 符号引用包括下面三类:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池中的每一项常量都是一个表,不同的表是有不同的结构,接下来我们来看看14种表的具体含义:

名称 标志 描述
CONSTANT_utf8_info 1 utf-8编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_Interface_Methodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 表示方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

通过命令

javap -verbose TestClass

就可以把上面的18个常量都计算出来,省得自己挨个根据ASCII码进行计算,得到下面的常量表:

常量池//常量池 18个
10a 0004 000f					Methodref #4, #15
209 0003 0010 					Fieldref  #3, #16
307 0011						Class #17
407 0012 						Class #18
501 0001 6d					utf-8 m
601 0001 49					utf-8 I 
701 0006 3c 69 6e 69 74 3e		utf-8 <init>
801 0003 28 29 56				utf-8 ()V
901 0004 43 6f 64 65 			utf-8 Code
1001 000f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 	utf-8 LineNumberTable
1101 0003 69 6e 63 			utf-8 inc
1201 0003 28 29 49 			utf-8 ()I
1301 000a 53 6f 75 72 63 65 46 69 6c 65 		utf-8 SourceFile
1401 000e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61 	utf-8 TestClass.java
150c 0007 0008					NameAndType #7:#8
160c 0005 0006 				NameAndType #5:#6
1701 001d 6f 72 67 2f 66 65 6e 69 78 73 6f 66 74 2f 63 6c 61 7a 7a 2f 54 65 73 74 43 6c 61 73 73 							 utf-8 org/fenixsoft/clazz/TestClass
1801 0010 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 		utf-8 java/lang/Object

举个栗子,比如第三个开头是07,那么就是对应CONSTANT_Class_info这个info,而CONSTANT_Class_info对应的是下面的数据结构:

类型 名称 数量
u1 tag 1
u2 name_index 1

那么紧跟07 后面的11就是索引第11项常量的意思,第11项是01 0003 69 6e 63, 其中tag是01,也就是CONSTANT_utf8_info这个info,它的数据结构:

类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

所以,长度是3,往后数三个字节就是69 6e 63,对应的就是inc,这个也就是方法的名称,其他的都是这样的分析方式: 首先找到tag对应的表数据结构,然后根据数据结构拆分。

篇幅所限,其他的常量项的结构可以参考深入理解Java虚拟机

紧接着常量池后的是访问标志。

4.访问标志

在常量池之后紧接这两个字节是访问标志,识别一些类或者接口层次的访问信息:

Class是类或者接口 是否public 是否abstract 是否final

具体的标志位和含义如下面表格:

名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public
ACC_FINAL 0x0010 是否为final
ACC_SUPER 0x0020 JDK 1.0.2之后编译出来的类这个标志都为真
ACC_INTERFACE 0x0200 是否为一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 是否是注解
ACC_ENUM 0x4000 是否是枚举

在我们这个栗子中类是public 是JDK1.8编译出来的,所以access_flags的值为:ACC_PUBLIC | ACC_SUPER = 0x0021

5.类索引/父类索引/接口索引

在访问标志后分别是this_class/super_class/interfaces_count

0003 //this_class			确定这个类的全限定名
0004 //super_class 		 	java.lang.Object该值就是0000
0000 //interfaces_count		该类没有实现任何接口,接口的索引表不占用任何字节

有的小伙伴就要急了,上面的0003为什么代表this_class?其实这个0003就是在常量池中的索引,回顾前面常量池中第3的索引是:07 0011这个是CONSTANT_Class_info的数据结构,指向第17的索引:

01 001d 6f 72 67 2f 66 65 6e 69 78 73 6f 66 74 2f 63 6c 61 7a 7a 2f 54 65 73 74 43 6c 61 73 73

这个是CONSTANT_utf8_info的数据结构,对应就是

org/fenixsoft/clazz/TestClass

这个就是类的全限定名。

其它两个的分析以此类推,在这个例子中没有实现接口,所以接口数量是0,也就没有后面的interfaces。

紧接着的就是fields_count和fields。

6.字段表集合

字段表field_info用于描述类和接口中声明的变量。变量包括类级变量和实例级变量,但是不包括方法中的变量。描述字段的信息都有哪些?有作用域(public/private/protect等),static,字段名字,字段数据类型,其中可以用布尔类型描述的有:

字段的作用域,public/private/protected 实例变量还是类变量,static 可变性,final 并发可见性, volatile 可否被序列化, transient

类似与上面的access_flags, 能用布尔类型表示的定义下面的标志位:

名称 标志值
ACC_PUBLIC 0x0001
ACC_PRIVATE 0x0002
ACC_PROTECTED 0x0004
ACC_STATIC 0x0008
ACC_FINAL 0x0010
ACC_VOLATILE 0x0040
ACC_TRANSIENT 0x0080
ACC_SYNTHETIC 0x1000
ACC_ENUM 0x4000

不能用布尔类型描述的有:

字段名字 字段数据类型,基本类型/对象/数组

字段名称肯定是索引常量池中的数据项,字段数据类型呢?专门定义了描述符来标识数据类型, 对象类型用字符L加对象的全限定名来表示:

标识字符 含义 标识字符 含义
B 基本类型byte J 基本类型long
C 基本类型char S 基本类型short
D 基本类型double Z 基本类型boolean
F 基本类型float V 特殊类型void
I 基本类型int L 对象类型,如L/java/lang/Object

对于数组类型,每一个维度使用一个前置的“[”字符来描述,如“String[][]”表示为“[[Ljava/lang/String;”

字段表也有专门的结构, descriptor_index之后可以跟着属性表集合存储一些额外的信息,比如private static int m = 123, 那么可能会有一项ConstantValue的属性存储123这个值。

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

对于我们的例子TestClass, private int m;

//fields_count
0001 
//fields
0002 //private
0005 //m
0006 //I
0000 //attribute_count

紧跟着字段表之后的就是方法表集合。

7.方法表集合

方法表集合和字段表集合很类似,有一个区别就是用描述符描述方法时,需要先参数列表后返回值,比如

void inc()  ------> ()V

java.lang.String toString(int index) ---> (I)Ljava/lang/String

跟属性表一样,方法表也有专门的数据结构:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

在TestClass中有两个方法,一个是默认构造函数,一个是方法inc

//methods_count,编译器添加的实例构造器<init>和源码inc()
0002 
//methods
0001 	//public
0007	//<init>
0008 	//()V
0001	        //attribute_count
0009	//Code,存放方法里面的Java代码
......
//methods
0001      //public 
000b     //inc
000c     //()I
0001     //attribute_count
//Atrribute
//Code
0009    //Code,存放方法里面的Java代码

其中Code是方法的属性,用于存放方法的Java代码编译成的字节码指令。

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

8.属性表集合

虚拟机规范预定义的属性有21项,这里简单看下常用的几项:

属性 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
SourceFile 类文件 记录源文件名称

属性表结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

其中Code属性表的结构, attribute_name_index是指向常量池的索引,这里就是'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 1
u2 attributes_count 1
attribute_info attributes attributes_count

在我们例子中就是:

//Atrribute
0009 //attribute_name_index--->Code	
0000001d //attribute_length--->29
0001 	//max_stack 操作数栈
0001 	//max_locals	局部变量表需要的存储空间 单位slot
00000005 //code_length 字节码长度
2a b7 00 01 b1 //code 存储字节码指令的一序列字节流
0000 //exception_table_length
0001 //attributes_count--->Code的属性
//LineNumberTable描述Java源码行号与字节码行号之间的对应关系
000a //attribute_name_index
00000006 	//attribute_length
0001 		//line_number_table_length
0000 		//start_pc 字节码行号
0003 		//Java源码行号

//method
0001      //public 
000b     //inc
000c     //()I
0001     //attribute_count
//Atrribute
//Code
0009    //Code,存放方法里面的Java代码
0009 
0000001f 
0002 
0001 
00000007 
2a b4 00 02 04 60 ac	//code 存储字节码指令的一序列字节流
0000
0001
//LineNumberTable
000a
00000006
0001
0000
0006
0001
//SourceFile
000d		//SourceFile
00000002
000e

9.总结

能读懂Class类文件结构是理解虚拟机的入门功课,本次分享从一个简单例子详细阐述了类文件的结构格式,有一些细节没有仔细说明,比如属性表的另外的属性,还有常量池中数据项,属性表中异常表。但是有了上面的知识储备,自行分析剩下的就不是什么问题了。

另外,本文的思路和例子也是参考深入理解Java虚拟机: JVM高级特性与最佳实践这本书,很经典,建议小伙伴们可以看看。

谢谢大家!