欢迎关注微信公众号: JueCode
正如有一句名言: 代码编译的结果从本地机器码变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。 Java语言为什么能write once, run anywhere? 这个其实是因为和各种不同平台相关的虚拟机,这些虚拟机都可以载入和执行同平台无关的字节码。今天我们就来学习下Class类文件结构的一些知识。
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文件和上面的文件格式来挨个拆解。
首先看到前面三个选项,分别是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版本,一次类推。
紧接着主次版本号之后的是常量池。
常量池可以理解为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 |
那么紧跟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虚拟机 。
紧接着常量池后的是访问标志。
在常量池之后紧接这两个字节是访问标志,识别一些类或者接口层次的访问信息:
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
在访问标志后分别是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。
字段表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
紧跟着字段表之后的就是方法表集合。
方法表集合和字段表集合很类似,有一个区别就是用描述符描述方法时,需要先参数列表后返回值,比如
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代码编译成的字节码指令。
最后一个格式就是属性表集合了。
虚拟机规范预定义的属性有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
能读懂Class类文件结构是理解虚拟机的入门功课,本次分享从一个简单例子详细阐述了类文件的结构格式,有一些细节没有仔细说明,比如属性表的另外的属性,还有常量池中数据项,属性表中异常表。但是有了上面的知识储备,自行分析剩下的就不是什么问题了。
另外,本文的思路和例子也是参考 深入理解Java虚拟机: JVM高级特性与最佳实践 这本书,很经典,建议小伙伴们可以看看。
谢谢大家!