Java虚拟机之Class类文件结构

欢迎关注微信公众号: 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个
1、0a 0004 000f                  Methodref #4, #15
2、09 0003 0010                  Fieldref  #3, #16
3、07 0011                       Class #17
4、07 0012                       Class #18
5、01 0001 6d                    utf-8 m
6、01 0001 49                    utf-8 I 
7、01 0006 3c 69 6e 69 74 3e     utf-8 <init>
8、01 0003 28 29 56              utf-8 ()V
9、01 0004 43 6f 64 65           utf-8 Code
10、01 000f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65     utf-8 LineNumberTable
11、01 0003 69 6e 63             utf-8 inc
12、01 0003 28 29 49             utf-8 ()I
13、01 000a 53 6f 75 72 63 65 46 69 6c 65        utf-8 SourceFile
14、01 000e 54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61    utf-8 TestClass.java
15、0c 0007 0008                 NameAndType #7:#8
16、0c 0005 0006                 NameAndType #5:#6
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                            utf-8 org/fenixsoft/clazz/TestClass
18、01 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
类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

那么紧跟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高级特性与最佳实践这本书,很经典,建议小伙伴们可以看看。

谢谢大家!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容