Java Class文件结构解析 及 实例分析验证
在文章《Java三种编译方式:前端编译 JIT编译 AOT编译》中了解到了它们各有什么优点和缺点,以及前端编译+JIT编译方式的运作过程;在《Java前端编译:Java源代码编译成Class文件的过程》了解到javac编译的大体过程。
Class文件是JVM执行引擎的数据入口,也是Java技术体系的基础构成之一;了解Class文件的结构对后面进一步了解JVM执行引擎有很重要的意义。
下面我们详细了解Class文件:先对Class文件结构有个大体了解,并了解Class文件结构里的一些名称定义;而后再详细说明结构中每一项数据的含义,并用测试程序编译Class文件来分析验证Class文件结构。
1、Class文件概述
两种常见的程序编译存储文件格式分类:
1、编译成"01..."二进制格式的存储机器码文件格式,直接运行在操作系统上;
2、编译成与操作系统和机器指令集无关的格式作为存储文件格式,运行在隔离硬件平台的虚拟机之上;
C/C++编译出来的程序属于第一种,而Class文件属于第二种;
1-1、什么是Class文件?
Class文件是经过前端编译(如javac编译)后生成的文件(以.class为后缀),一个Class文件对应一个类或一个接口。
Class文件存储的内容称为字节码(ByteCode),包含了JVM指令集和符号表以及若干其他辅助信息。
JVM规范定义了Class文件结构格式,每种JVM实现必须满足规范定义,这样JVM实例才能加载Class文件,运行字节码内容。但JVM的实现可以在JVM规范的约束下对具体实现做出修改和优化(如自定义属性信息,JVM会忽略不认识的属性表)。
从数据类型看,Class文件是一组以8位字节(8-bit bytes)为基础的二进制字节流构成,8位以上的数据以大端(Big-Endian)的高位在前的顺序进行存储,中间没有添加任何分隔符。
从文件形式看,JVM加载的Class文件不一定来磁盘,还可以来自网络数据,甚至在运行时直接编译代码字符串生成 。
1-2、Class文件的作用(优点)是什么?
JVM规范定义Class文件格式来存储字节码,是虚拟机实现平台无关性、以及语言无关性的关键。
1、平台无关性
Java语言有"一次编写,到处运行(Write Once,Run Anywhere)"特性。
通过针对不同平台实现的虚拟机作为字节码执行引擎,它在多种操作系统和架构上提供 Java 运行时环境。
2、语言无关性
虚拟机只与字节码关联,不与任何语言绑定。
字节码和虚拟机也是语言无关性的基础,因为不只Java语言程序可以编译生成字节码,其他很多语言(如JRuby、Javascript等)也可以通过相应的编译器编译成JVM规范规定的字节码格式。
可以看出,字节码的语义描述能力比Java语言更强大。
目前JDK7和JDK8的JVM已经可以支持一些其他语言运行了,如Croovy、JRuby、Jython、Scala等。
1-3、Class文件结构的发展
相对于Java语言、API和其他方面的变化,Class文件结构一直处于比较稳定的状态,因为作为一种JVM规范,变化太多会增大实现难度,出现版本不兼容的情况。
Class文件的结构在几个版本中的变化不大,Class文件的主体结构、字节码指令的语义和数量几乎没有出现过变动,只是为适应新特性,增加了几个标记和属性。
即几乎所有Class文件格式的改进都集中在访问标志、属性表这些设计上,就可扩展的数据结构中添加内容,如:
访问标志新加入5个ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_BRIDGE、ACC_VARARGS标志;
属性表集合中,JDK1.5到JDK1.7增加12项新的属性,JDK1.8又增3项;
这些属性大部分用于支持Java语言的新特性,如泛型、枚举、变长参数、动态注解等;
还有一些为支持性能改进和调试信息,如JDK1.6的新类型校验器StackMapTable属性和对非Java代码用到的SourceDebugExtension属性;
字节码指令也只有几次轻微的变动,如:
JDK1.0.2时改动过invokespecial指令的语义,JDK1.7增加了invokedynamic指令,禁止了ret和jsr指令。
2、Class文件结构相关定义说明
我们先对Class文件结构有个大体了解,并了解Class文件结构里的一些名称定义,后面再详细说明结构中每一项数据的含义。
2-1、Class文件的结构是怎样的?
Class文件结构根据《Java虚拟机规范》定义的结构进行存储,类似于C语言的结构体的伪结构。
伪结构中各个数据都有相应的含义,并且各个数据项必须严格按规定的先后顺序排列,它们之间没有任何分隔符和空隙。
所以,存储的几乎都是运行需要数据(和指令),结构及说明如下:
类型 | 名称 | 数量 | 说明 |
u4 | magic | 1 | 魔数:确定一个文件是否是Class文件 |
u2 | minor_version | 1 | Class文件的次版本号 |
u2 | major_version | 1 | Class文件的主版本号:一个JVM实例只能支持特定范围内版本号的Class文件(可以向下兼容)。 |
u2 | constant_pool_count | 1 | 常量表数量 |
cp_info | constant_pool | constant_pool_count-1 | 常量池:以理解为Class文件的资源仓库,后面的其他数据项可以引用常量池内容。 |
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 | attributes_count | 1 | 属性表数量 |
attributes_info | attributes | attributes_count | 属性表集合:用于描述某些场景专有的信息,如字节码的指令信息等等。 |
这里先对Class文件结构有个大体的了解,后面会详细说明每一项数据,下面先来了解Class文件结构里的一些名称定义。
2-2、Class文件结构的数据类型是什么?
结构中只有两类数据类型:无符号数和表。
1、无符号数
无符号数属于基本的数据类型,以u1、u2、u4、u8来表示一个字节、两个字节...的无符号数;
无符号数用来描述数字、索引引用、数量值或UTF-8编码构成的字符串值。
2、表
表是由多个无符号数或其他表作为数据项构成的复合数据类型,一般以"_info"结尾;
表用来描述有层次关系的复合结构的数据;
表中的项长度不固定;
整个Class文件本质上就是一个表。
2-3、全限定名称、非全限定名称、描述符以及签名
它们是在Class文中结构中的字段、方法、类、接口都可能用到的表示,都存储在常量池,为CONSTANT_Utf8_info类型常量UTF-8字符串。
其他数据项中通过索引引用,如当前类索引(this_class)都指向CONSTANT_Class_info常量类型数据,而CONSTANT_Class_info里通过索引指向CONSTANT_Utf8_info常量类型数据,这样就可以找到当前类全限定名。
1、全限定名称(Fully Qualified Name)
全限定名是在整个JVM中的绝对名称,可以表示Class文件结构中的类或接口的名称。
都通过全限定形式(Fully Qualified Form)来表示,这被称作它们的"二进制名称"(JLS §13.1);但用来分隔各个标识符的符号不在是ASCII 字符点号('.'),而是被 ASCII 字符斜杠('/')所代替。
如,类 Thread 的正常的二进制名是"java.lang.Thread",在 Class 文件面,对该类的引用是通过来一个代表字符串"java/lang/Thread"的CONSTANT_Utf8_info 结构来实现的。
2、非全限定名称(Unqualified Names)
也称为简单名称;
方法名、字段名和局部变量名都被使用非全限定名进行存储;
如,"java.lang.Object"表示为"Object"。
3、描述符(Descriptor)
描述符是一个描述字段或方法的类型的字符串。
(A)、字段描述符(Field Descriptor):
是一个表示类、实例或局部实例变量的语法符号;
由下面语法产生的字符序列:
(B)、方法描述符(Method Descriptor)
描述一个方法所需的参数和返回值信息,即包括参数描述符(ParameterDescriptor)和返回描值述符(ReturnDescriptor);
由下面语法产生的字符序列:
一个方法无论是静态方法还是实例方法,它的方法描述符都是相同的;
方法额外传递参数this,不是由法描述符来表达的;
而是由 Java 虚拟机实现在调用实例方法所使用的指令中实现的隐式传递;
(C)、描述符中基本类型表示字符如下:
字符 | 类型 | 含义 |
B | byte | 有符号字节型数 |
C | char | Unicode 字符,UTF-16 编码 |
D | double | 双精度浮点数 |
F | float | 单精度浮点数 |
I | int | 整型数 |
J | long | 长整数 |
S | short | 有符号短整数 |
Z | boolean | 布尔值:true/false |
L Classname; | reference | 一个名为<Classname>的实例 |
[ | reference | 一个一维数组 |
V | void | void返回值(其实不属于基本类型,而是VoidDescriptor) |
例如:
描述int实例变量的描述符是"I";
java.lang.Object实例描述符是"Ljava/lang/Object;";
double的三维数组"double d[][][];"的描述符为"[[[D";
Object mymethod(int i, double d, Thread t)方法描述符是"(IDLjava/lang/Thread;)Ljava/lang/Object";
更多请参考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.3
4、签名(Signature)
签名是用于描述字段、方法和类型定义中的泛型信息的字符串,这应该是JDK1.5引入泛型(类型变量或参数化类型)后的而出现的。
Java编译器必须为声明使用类型变量或参数化类型的任何类、接口、构造函数、方法或字段发出签名。类型变量或参数化类型在编译时经过类型擦除变为原始类型,所以它们都是不在Java虚拟机中使用的类型,而Java编译器需要这类信息来实现(或辅助实现)反射(reflection)和跟踪调试功能。
HotSpot VM实现在加载和链接时,并不校验Class文件的签名内容,直到被反射方法调用时才会校验。
在Java语言中,任何类、接口、初始化方法或成员的泛型签名,如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则该字段在字段表集合中(field_info fields[fields_count])对应的字段信息(field_info),或该方法在方法表集合(method_info methods[methods_count])对应的方法信息(method_info),存在Signature属性会为它记录泛型签名信息,Signature属性存在指向CONSTANT_Utf8_info常量类型数据的索引,这样就可以找到相应的签名字符串。
(A)、类签名(Class Signature)
作用是把Class申明的类型信息编译成对应的签名信息;
描述当前类可能包含的所有的(泛型类型的)形式类型参数,包括直接父类和父接口;
由 ClassSignature 定义:
(B)、字段类型签名(Field Type Signature)
作用是将字段、参数或局部变量的类型编译成对应的签名信息;
由 JavaTypeSignature定义,包括基本类型和引用类型的签名:
(C)、方法签名(Method Signature)
作用是将方法中所有的形式参数的类型编译成相应的签名信息(或将它们参数化);
由 MethodTypeSignature 定义:
计算方法的特征签名在前文《Java前端编译:Java源代码编译成Class文件的过程》"2-2、填充符号表 第4点、计算方法的特征签名"时曾提到过;
更多关于签名请参考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.9
更多关于泛型编译类型擦除请参考上文:《Java前端编译:Java源代码编译成Class文件的过程》4-2、解语法糖
2-4、描述符、签名以及特征签名的区别
1、描述符和签名的区别
通过上面描述符与签名的详细说明,我们知道:
描述符:
描述符是一个描述字段或方法的类型的字符串;
签名:
描述字段、方法和类型定义中的泛型信息的字符串;
区别:
(A)、范围不同
描述符不能描述类,所以没有类描述符;
签名是JDK1.5引入泛型(类型变量或参数化类型)后的而出现的;
签名需要(字段、方法和类中)有类型变量或参数化类型的时候才会出现。
(B)、对于方法来说
描述符 = 参数类型 + 参数顺序 + 返回值类型;
签名 = 描述符 + FormalTypeParametersopt + ThrowsSignature;
签名还包括类型变量或参数化类型编译未擦除类型前的信息(FormalTypeParametersopt),和抛出的异常信息(ThrowsSignature);
也就是说,当一个方法没有类型变量或参数化类型,也没有抛出异常时,签名和描述符是一样的,不过这时候也就没有签名信息存在了。
2、方法的描述符(签名)和特征签名的区别
方法特征签名:
用于区分两个不同方法的语法符号;
是和上面说的签名是两个不同的概念;
这个在Java语言层面和JVM层面是不同的,这个问题在前文《Java前端编译:Java源代码编译成Class文件的过程》"2-2、填充符号表 第4点、计算方法的特征签名"时曾提到过;
(A)、Java语言层面的方法特征签名
特征签名 = 方法名 + 参数类型 + 参数顺序;
更多请参考:http://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.2
(B)、JVM层面的方法特征签名
特征签名 = 方法名 + 参数类型 + 参数顺序 + 返回值类型;
即特征签名 = 方法名 + 描述符;
如果存在类型变量或参数化类型,还包括类型变量或参数化类型编译未擦除类型前的信息(FormalTypeParametersopt),和抛出的异常信息(ThrowsSignature),即方法名+签名;
Java语言重载(Overload)一个方法,需要ava语言层面的方法特征签名不同,即不包括方法返回值;而Class文件中有两个同名同参数(类型、顺序都相同),但返回值类型不一样,也是允许的,可以正常运行,因为JVM层面的方法特征签名包括返回值类型。
同样的,对字段来说,Java语言规定字段无法重载,名称必须不一样;但对Class文件来说,只要两个字段描述(类型)不一样,名称一样也是可以的。
(篇幅有限,后面有时间再来验证)
3、Class文件结构分析验证
上面我们对Class文件结构有个大体了解,并了解Class文件结构里的一些名称定义;下面将详细说明结构中每一项数据的含义,同时,先用javac编译测试程序ClassFileTest.java,对照编译出来的Class文件进行说明,测试程序如下:
import java.util.HashMap; import java.util.Map; public class ClassFileTest { public String mString = "blog.csdn.net/tjiyu"; private static String mStaticString = "hello"; private final static String mFinalStaticString = "java"; public Map<String, String> getMap() { Map<String, String> map = new HashMap<String, String>(); map.put(mStaticString, mFinalStaticString); return map; } }编译命令如下:
javac ClassFileTest.java
然后用javap反编译ClassFileTest.class文件,并保存到ClassFileTest.txt文件,方便分析:
javap -verbose ClassFileTest > ClassFileTest.txt
注意,这里使用JDK8。
3-1、魔数
Class文件开始是4个字节定义为魔数(Magic Number);
唯一作用:确定一个文件是否是Class文件;
魔数可以自由选择,只要没有广泛使用而且不会引起混淆的即可,这样就不会因为扩展名改变而无法识别;其他许多文件类型格式头都存在魔数,如gif、jpeg等。
Class文件的魔数为"0xCAFEBABE"(咖啡宝贝),比照ClassFileTest.class如下:
3-2、Class文件的版本
魔数之后的4个字节,第5、6这两个字节是次版本号(Minor Version),如m;第7、8这两个字节是主版本号(Major Version),如M,构成版本号M.m,大小的顺序为:49.5 < 50.0 < 50.1。
一个Java虚拟机实例只能支持特定范围内版本号的Class文件,高版本号的Java虚拟机实现可以向下兼容低版本号的Class文件,反之则不成立。
从45开始,JDK1.0.2中的Oracle Java虚拟机支持版本45.0~45.3;
JDK1.1.*支持45.0~45.65535;
JDK1.k(k≥2)时,对应的Class文件格式版本号的范围是45.0至44+k.0。
在ClassFileTest.class中为"00000034",如下:
即minor_version为0,major_version为52(0034对应的十进制),版本为52.0(符合JDK8),反编译信息ClassFileTest.txt中也明确列出,如下:
3-3、常量池
Class文件的版本号之后,即从第9个字节开始;
先是常量池数量计数值,接着是常量池(表);
1、常量池数量计数值
因为常量池数量不固定,所以需要一个数量计数值;
从1开始,第0项空出,后面其他数据项引用常量池第0项(即为索引值0时),可以表示"不引用任何一个常量池项内容";
只有常量池是从1开始,后面的其他集合类型的计数值都是从0开始;
注意,CONSTANT_Long_info 或 CONSTANT_Double_info常量结构占两个常量表项的空间,其他都占一个空间,即如果一个CONSTANT_Long_info 或 CONSTANT_Double_info结构的项在常量池中的索引为n,则常量池中下一个有效的项的索引为 n+2。
在ClassFileTest.class中为"002A",如下:
即常量项数量为42(2A对应的十进制),反编译信息ClassFileTest.txt中也明确列出常量池各项,可以看到第一项索引为1,且最大项索引为41,一共42项(加上索引为0的项,且没Double和Long占两个索引的结构),如下:
2、常量池
常量池每一项常量都是一个表;
包含Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量;
主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References);
(A)、字面量:常见的常量,如文本字符串、声明为final的常量值等;
(B)、符号引用:需要编译原理的概念,主要包括三类常量:
(I)、类和接口的全限定名(Full Qualified Name);
(II)、字段的名称和描述符(Descriptor);
(III)、方法的名称和描述符;
Class文件不会像C/C++编译后保存各个方法、字段的内存布局信息;JVM运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中;
所以,常量池可以理解为Class文件的资源仓库,后面的其他数据项可以引用常量池内容;
所有的常量池项都具有如下通用格式:
tag项开头,一个字节的标志位;
tag的标识了后面info[]项的内容,也就是这个常量属于哪种类型;
JDK1.7前有11种的常量类型结构,JDK1.7为更好支持动态语言调用,又增加了额外的3种(CONSTANT_MethodHandle、CONSTANT_MethodType、CONSTANT_InvokeDynamic),到了JDK1.8也是14种,14种常量类型对应的tag项说明如下:
14种常量类型,结构各不同;
注意,从《Java前端编译:Java源代码编译成Class文件的过程》可以知道,编译器会自动生成一些常量,如字段、方法的描述符(<clinit>)等;
对测试程序,反编译信息ClassFileTest.txt中也明确列出常量池各项如下:
(1)、CONSTANT_Fieldref_info、CONSTANT_Methodref_info 和CONSTANT_InterfaceMethodref_info结构
从上面看到,反编译信息第一项常量结构为CONSTANT_Methodref_info,下面先来了解它。
字段,方法和接口方法由类似的结构表示;
它们的结构里面的三项都一样,结构如下:
tag:
字段为9,方法为10,接口方法为11(从上面表格中可以看到);
class_index:
是对CONSTANT_Class _info类型常量的一个有效索引,代表一个类或接口,而当前字段或方法是这个类或接口的成员;
name_and_type_index:
是对CONSTANT_NameAndType_info类型常量的一个有效索引,代表当前字段或方法的名字和描述符(名字和描述符定义看前面)。
从上面看到,反编译信息第一项常量结构为CONSTANT_Methodref_info,从后面注释的信息看,该方法是java/lang/Object类的实例初始化函数<init>(),如下:
在ClassFileTest.class文件中字节流数据为常量项数量"002A"后面的5个字节—"0A000B001C",如下:
Tag:对应"0A"即为10;
class_index:对应"000B"为11,索引到第11项常量,可以看到为CONSTANT_ Class _info结构类型,该结构中又索引到第38项常量—"java/lang/Object",表示该方法是java/lang/Object的成员,如下
class_index:对应"001C"为28,可以看到为CONSTANT_ NameAndType _info结构类型,该结构中又索引到第17项和第18项常量,分别表示该方法的名称"<init>"和描述符"()V",如下:
另外,通过反编译信息发现,该方法只在javac编译器自动添加的ClassFileTest类实例初始化函数(实际名称也是<init>())中通过invokespecial指令调用(即super()函数调用),如下:
(2)、CONSTANT_Class_info结构
该结构在上面字段,方法和接口方法结构中已经提到过了(class_index项);
用于表示一个类或接口;
结构如下:
tag:
为7;
name_index:
是对CONSTANT_Utf8_info类型常量的一个有效索引,代表一个类或接口的全限定名(全限定名定义看前面)。
字节码指令 anewarray 和 multianewarray 创建的引用型数组对象,可以通过常量池中的CONSTANT_Class_info结构来引用类数组;对于这些数组,类的名字就是数组类型的描述符;例如:表示一维 Thread 数组类型"Thread[]"的名字是:“[Ljava/lang/Thread;”;
这里我们来看下ClassFileTest类的常量表示,先通过反编译信息找到ClassFileTest类的位置,可以看到在常量池第7项,相关常量如下:
tag为7即07(u1类型),而且索引值为#33对应十六进制为0021(u2类型),所以在ClassFileTest.class中找出实际数据"070021"(当然还得验证前后的数据,可以看到前面"09070020"是对应第6项反编译信息的),如下:
另外,反编译信息中看到,只有"mString"和"mStaticString"两个字段引用ClassFileTest类结构常量(#7),而"mFinalStaticString"字段和"getMap()"方法并没有相关CONSTANT_Fieldref_info、CONSTANT_Methodref_info定义,这是什么呢?
对于"getMap()"方法没定义很容易理解,因为程序中并没有调用它,所以并不需要定义CONSTANT_Methodref _info来引用;但相关常量信息还是必须有的,因为在方法表集合中存储"getMap()"方法表需要引用相关常量(详情见后面的方法表集合介绍),一些相关常量信息如下:
对于"mFinalStaticString"字段,在getMap()"方法中存在访问调用,但为什么又没有CONSTANT_Fieldref_info定义呢?这因为该字段被"final static "修饰,javac编译时字段值将被分配为ConstantValue属性存储在字段表field_info结构的属性表中(详情见后面的字段表集合和ConstantValue属性介绍),而getMap()"方法中直接访问相应的常量(和"mStaticString"访问方式不同),如下:
(3)、CONSTANT_Utf8_info结构
该结构在上面两个结构介绍中已经提到过了(name_index项),Class文件中出现的频率极高;
用于表示字符串常量的值;
结构如下:
tag:
为1;
length:
该值指明了bytes[]数组的长度,也即字符串长度;
字段、方法名都需要引用该常量类型,所以字段、方法名最大长度是65535;
bytes[]:
UTF-8缩略编码表示的字符串。
注意,改进的UTF-8缩略编码与UTF-8编码的区别:
在范围'\u0001'至'\u007F'内的字符用1个单字节表示;
字符为'\u0000'(表示字符'null'),或者在范围'\u0080'至'\u07FF'的字符用两字节表示;
在范围'\u0800'至'\uFFFF'中的字符像普通UTF-8编码一样用3个字节表示;
更多UTF-8缩略编码信息请参考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.7
接着前面的ClassFileTest类的常量分析,它索引到第33项Utf8常量类型,数据表示字符串"ClassFileTest",不过可以看到ClassFileTest.class中有两个位置存储该字符串,另一个是第27项的"ClassFileTest.java",如下:
我们找到第二个并且验证前面的"01000D",tag为"01"符合,length为"000D"表示后面长度为13也符合,如下:
上面只介绍了三种常量结构,更多常量结构请参考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4
3-4、访问标志
用于表示这个类或者接口的访问权限及基础属性;
可以用16位掩码标志,目前只使用8种(JDK1.5增加后面三种),没使用的需要设置为0;
相应标志及含义如下:
标记名 | 值 | 含义 |
ACC_PUBLIC | 0x0001 | 可以被包的类外访问。 |
ACC_FINAL | 0x0010 | 不允许有子类。 |
ACC_SUPER | 0x0020 | 当用到 invokespecial 指令时,需要特殊处理的父类方法。 |
ACC_INTERFACE | 0x0200 | 标识定义的是接口而不是类。 |
ACC_ABSTRACT | 0x0400 | 不能被实例化。 |
ACC_SYNTHETIC | 0x1000 | 标识并非 Java 源码生成的代码。 |
ACC_ANNOTATION | 0x2000 | 标识注解类型 |
ACC_ENUM | 0x4000 | 标识枚举类型 |
带有ACC_INTERFACE标志的类,意味着它是接口而不是类,反之是类而不是接口;
如果一个 Class 文件被设置了ACC_INTERFACE标志,那么同时也得设置ACC_ABSTRACT标志(JLS §9.1.1.1),同时它不能再设置ACC_FINAL、ACC_SUPER和ACC_ENUM标志;
ACC_SUPER 标志用于确定该 Class 文件里面的 invokespecial 指令(在JDK1.0.2发生过改变)使用的是哪一种执行语义;目前 Java 虚拟机的编译器都应当设置这个标志。
在ClassFileTest.class常量池42项数据后面就是访问标志数据"0021",对应的标志为ACC_PUBLIC和ACC_SUPER,反编译信息中也明确指出,如下:
3-5、类索引、父类索引与接口索引集合
这三项数据确定这个类的继承关系;
这些索引都指向CONSTANT_Class_info常量类型数据,我们知道CONSTANT_Class_info里通过索引指向CONSTANT_Utf8_info常量类型数据,这样就可以找到当前类、父类、实现接口的全限定名。
1、类索引
用来确定这个类的的全限定名;
在ClassFileTest.class中访问标志数据后面就是类索引数据"0007",即引用到ClassFileTest类常量数据,如下:
2、父类索引
用来确定这个类的父类的全限定名;
因为Java语言不允许多生继承,所以父类索引只有一个;并且除java.lang.Object外,其他所有类的父类索引都不能为0;
对于接口来说,索引指向的项必须为代表java.lang.Object的CONSTANT_Class_info类型常量;
在ClassFileTest.class中类索引数据后面就是父类索引数据"000B",即引用到java/lang/Object类常量数据,如下:
3、接口索引集合
用来描述这个类实现了哪些接口;
implements(接口extends)后实现的按顺序从左到右排列在接口索引集合;
如果没有实现任何接口,interfaces_count为0,后面不再有interfaces[interfaces_count];
在ClassFileTest.class中父类索引数据后面就是接口索引数据"0000",即没实现任何接口,如下:
3-6、字段表集合
字段表集合描述当前类或接口声明的所有字段;
field_info用于表示当前类或接口中某个字段的完整描述,包括类字段(static字段)或实例字段,不包括局部变量,但不包括从父类或父接口继承的部分字段;
对内部类,编译器可能自动添加对外部类实例的字段;
field_info 结构格式如下:
access_flags:
是用于定义字段被访问权限和基础属性的掩码标志;
和前面类的访问标志一样,没使用的需要设置为0;
有些标记是互斥的,如不能同时设置标志 ACC_FINAL 和 ACC_VOLATILE;
具体标志及含义如下:
标记名 | 值 | 说明 |
ACC_PUBLIC | 0x0001 | public,表示字段可以从任何包访问 |
ACC_PRIVATE | 0x0002 | private,表示字段仅能该类自身调用 |
ACC_PROTECTED | 0x0004 | protected,表示字段可以被子类调用 |
ACC_STATIC | 0x0008 | static,表示静态字段 |
ACC_FINAL | 0x0010 | final,表示字段定义后值无法修改(JLS§17.5) |
ACC_VOLATILE | 0x0040 | volatile,表示字段是易变的 |
ACC_TRANSIENT | 0x0080 | transient,表示字段不会被序列化 |
ACC_SYNTHETIC | 0x1000 | 表示字段由编译器自动产生 |
ACC_ENUM | 0x4000 | enum,表示字段为枚举类型 |
name_index:
指向CONSTANT_Utf8_info类型常量的索引;
表示当前字段的非全限定名(简单名称);
descriptor_index:
指向CONSTANT_Utf8_info类型常量的索引;
表示当前字段的描述符;
attributes_count:
表示当前字段的附加属性的数量;
attributes[attributes_count]:
表示当前字段的一些额外的属性信息;
如"final static"修饰的字段,可能存在ConstantValue的属性;
泛型字段存在Signature属性;
详见下面属性表集合;
另外前面曾说过,Java语言规定字段无法重载,名称必须不一样;但对Class文件来说,只要两个字段描述(类型)不一样,名称一样也是可以的。
在ClassFileTest.class中接口索引集合数据后面就是字段表集合数据,先是字段表数量数据"0003"表示定义有三个字段"mString"、"mStaticString"以及"mFinalStaticString",如下:
1、mString字段
public String mString = "blog.csdn.net/tjiyu";
字段表数量数据后面就是mString字段数据,如下:
access_flags:"0001"表示ACC_PUBLIC 标志,即"public"修饰;
name_index:"000C"表示索引第12项的常量,即字段名称为"mString",如下:
descriptor_index:"000D"表示索引第13项的常量,即字段描述符为"Ljava/lang/String;",如上图;
attributes_count:"0000"表示字段附加属性的数量为0,即后面没有属性数据了。
2、mStaticString字段
private static String mStaticString = "hello";
mString字段表数据后面就是mStaticString字段数据,如下:
access_flags:"000A"表示ACC_PRIVATE和ACC_STATIC标志,即被"private"和"static"修饰;
name_index:"000E"表示索引第14项的常量,即字段名称为"mStaticString",如图:
descriptor_index:"000D"表示索引第13项的常量,即字段描述符为"Ljava/lang/String;",如上图;
attributes_count:"0000"表示字段附加属性的数量为0,即后面没有属性数据了。
3、mFinalStaticString字段
private final static String mFinalStaticString = "java";
mStaticString字段表数据后面就是mFinalStaticString字段数据,如下:
access_flags:"001A"表示ACC_FINAL 、ACC_PRIVATE以及ACC_STATIC标志,即同时被"final"、"private"和"static"修饰;
name_index:"000F"表示索引第16项的常量,即字段名称为"mFinalStaticString",如图:
descriptor_index:"000D"表示索引第13项的常量,即字段描述符为"Ljava/lang/String;",如上图;
attributes_count:"0001"表示字段附加属性的数量为1,即后面有一项属性数据;
attributes[]:这就是在上面介绍常量池最后分析说到的ConstantValue属性;ConstantValue属性包括两个u2和一个u4类型数据,即为"0010 00000002 0008",ConstantValue属性详见后面介绍。
更多字段表集合信息请参考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.5
3-7、方法表集合
methods[]数组只描述当前类或接口中声明的方法;
包括实例方法、类方法、实例初始化方法方法和类或接口初始化方法方法;但不包括从父类或父接口继承的方法;
可能存在编译自动添加的方法,如类构造器"<clinit>"和实例构造器"<init>";
method_info用于表示当前类或接口中某个方法的完整描述;
如attributes[]中可能存在"Code"属性,表示方法逻辑代码编译后的字节码指令;
method_info 结构格式如下:
access_flags:
用于定义当前方法的访问权限和基本属性的掩码标志;
和前面类的访问标志一样,没使用的需要设置为0;
有些标记是互斥的,如不能同时设置标志 ACC_FINAL 和 ACC_ABSTRACT;
具体标志及含义如下:
标记名 | 值 | 说明 |
ACC_PUBLIC | 0x0001 | public,方法可以从包外访问 |
ACC_PRIVATE | 0x0002 | private,方法只能本类中访问 |
ACC_PROTECTED | 0x0004 | protected,方法在自身和子类可以访问 |
ACC_STATIC | 0x0008 | static,静态方法 |
ACC_FINAL | 0x0010 | final,方法不能被重写(覆盖) |
ACC_SYNCHRONIZED | 0x0020 | synchronized,方法由管程同步 |
ACC_BRIDGE | 0x0040 | bridge,方法由编译器产生 |
ACC_VARARGS | 0x0080 | 表示方法带有变长参数 |
ACC_NATIVE | 0x0100 | native,方法引用非java语言的本地方法 |
ACC_ABSTRACT | 0x0400 | abstract,方法没有具体实现 |
ACC_STRICT | 0x0800 | strictfp,方法使用FP-strict浮点格式 |
ACC_SYNTHETIC | 0x1000 | 方法在源文件中不出现,由编译器产生 |
name_index:
指向CONSTANT_Utf8_info类型常量的索引;
表示当前方法的非全限定名(简单名称);
descriptor_index:
指向CONSTANT_Utf8_info类型常量的索引;
表示当前方法的描述符;
attributes_count:
表示当前方法的附加属性的数量;
attributes[attributes_count]:
表示当前方法的一些属性信息;
如重要的"Code"属性,表示方法编译后的字节码指令;
有泛型参数存在Signature属性;
详见下面属性表集合;
另外,前面曾说过,java语言重载(Overload)一个方法,需要特征签名不同;而Class文件中特征签名范围更大,包括方法返回值。
在ClassFileTest.class中字段表集合的mFinalStaticString字段数据后面就是方法表集合数据,先是方法表数量数据"0003"表示有三个方法:类构造器"<clinit>"、实例构造器"<init>"以及自定义的"getMap"方法,如下:
1、实例构造器"<init>"
先是实例构造器"<init>()"方法,这是在前文《Java前端编译:Java源代码编译成Class文件的过程》提到过的:对于实例构造器<init>(),如果程序代码中定义有构造函数,它在解析的语法分析阶段被重命名为<init>();如果没有定义构造函数,则实例构造器<init>()是在填充符号表时添加的;并把需要初始化的变量以及需要执行的语句块添加到相应的构造器中。
方法表数量数据后面就是实例构造器"<init>"数据,如下:
access_flags:"0001"表示ACC_PUBLIC 标志,即"public"修饰;
name_index:"0011"表示索引第17项的常量,即方法名称为"<init>",如下:
descriptor_index:"0012"表示索引第18项的常量,即字段描述符为"()V",如上图;
attributes_count:"0001"表示字段附加属性的数量为1,即后面有一项属性数据;
attributes[]:这就是重要的"Code"属性,表示方法编译后的字节码指令
"Code"属性详见后面属性集合介绍,这里看下该方法的反编译信息,可以看到Code属性中除了字节码指令,后面还包含程序行号信息LineNumberTable属性,如下:
(A)、"super()"调用"
可以看到前面介绍第一项常量时说过的"invokespecial #1",先调用Object类型的实例构造器"<init>",这相当于"super()"调用(前面文章也说过是编译器自动添加的);
(B)、初始化"mString"字段
"ldc #2":加载第2项常量字符串"blog.csdn.net/tjiyu"到操作栈;
"putfield #3":初始化"mString"字段--将操作栈顶的字符串"blog.csdn.net/tjiyu"赋值给字段"mString";
(C)、对实例对象的操作
至于"aload_0"表示把局部变量表中的第0项"this"实例加载到操作栈,也即"<init>"中的"super()"调用"和初始化字段"mString"都是对该实例对象进行的操作,所以叫做实例构造器;
关于JVM指令集,篇幅有限,后面有时间另外介绍,可以先参考:
《JVM规范 JavaSE8版》2.11 字节码指令集简介:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11
《JVM规范 JavaSE8版》第6章 Java虚拟机指令集:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
2、"getMap"方法
这是程序中自定义的方法,实例构造器"<init>"数据后面就是"getMap"方法数据,如下:
access_flags:"0001"表示ACC_PUBLIC标志,即被"public"修饰;
name_index:"0015"表示索引第21项的常量,即方法名称为"getMap",如下:
descriptor_index:"0016"表示索引第22项的常量,即字段描述符为"()Ljava/util/Map;",如上图;
attributes_count:"0002"表示字段附加属性的数量为2,即后面有两项属性数据;
attributes[]:包括重要的"Code"属性--表示方法编译后的字节码指令,以及"Signature"属性--表示该方法是一个泛型方法,记录的是被编译器擦除的泛型签名信息。
"Code"属性和"Signature"属性详见后面属性集合介绍,这里先看下该方法的反编译信息,可以看到Code属性中除了字节码指令,后面还包含程序行号信息LineNumberTable属性,如下:
(A)、对两个字段变量的访问方式不同
这里需要注意的是:getMap()"方法中"map.put(mStaticString, mFinalStaticString);"对两个字段变量的访问方式不同:对"mStaticString"需要通过"getstatic #6"来将该字段的值取出,并推入到操作数栈顶;而对"mFinalStaticString"则通过"ldc #8"直接将第8项常量"java"字符串加载到操作栈顶;然后再执行"invokeinterface #9, 3"即"map.put()"接口方法;
为什么对"mFinalStaticString"的访问直接引用到"java"字符串?因为"final"的不变性。
方法其他指令的执行也不说了,以后有时间再另写文章详细介绍。
3、类构造器"<clinit>"
对于类构造器"<clinit>"方法,也在前文《Java前端编译:Java源代码编译成Class文件的过程》"4-3、字节码生成"提到过的:类构造器"<clinit>"在转换字节码前由编译自动添加,提前是有需要初始化执行的类变量和块(static)。
"getMap"方法表数据后面就是类构造器"<clinit>""数据(结束),如下:
access_flags:"0008"表示ACC_STATIC标志,即被"static"修饰,是静态方法;
name_index:"0019"表示索引第25项的常量,即方法名称为"<clinit>",如下:
descriptor_index:"0012"表示索引第18项的常量,即字段描述符为"()V",如上图;
attributes_count:"0001"表示字段附加属性的数量为1,即后面有一项属性数据;
attributes[]:这就是重要的"Code"属性,表示方法编译后的字节码指令
"Code"属性详见后面属性集合介绍,这里看下该方法的反编译信息,如下:
(A)、初始化"mStaticString"字段
"ldc #10":加载第2项常量字符串"hello"到操作栈;
"putstatic #6":初始化"mStaticString"字段--将操作栈顶的字符串"hello"赋值给字段"mStaticString";
(B)、对类变量和块(static)的操作
这里并不需要像在实例构造器"<init>"中的"aload_0",因为"<clinit>"不是对实例对象进行操作,而是对类变量和块(static)的操作,所以叫做类构造器;
"mFinalStaticString"字段的初始化问题:
另外需要注意,"mString"字段变量在实例构造器"<init>"中初始化,而"mStaticString"字段变量在类构造器"<clinit>"中初始化,那"mFinalStaticString"字段变量的初始化呢?从前面的描述和反编译信息中都没有看到;这就要从"mFinalStaticString"字段拥有的ConstantValue属性说起了,详见后面的ConstantValue属性介绍。
所以,前面一直说的类构造器"<clinit>"中对类变量和块操作,这类变量除需要是"static"外,还需"非final"。
更多方法表集合信息请参考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.6
到这里ClassFileTest.class的介绍基本结束了,但是在上面很多结构中最后存在"attribute_info attributes[]",这并没有太详细介绍;下面来看看属性表集合都有些什么。
3-8、属性表集合
用于描述某些场景专有的信息;
集合中各属性表没有严格的顺序;
可以自定义属性信息,JVM会忽略不认识的属性表;
在Class文件的 ClassFile结构、字段表、方法表中都可以存储放自己的属性表集合,所以并不像最前面那Class文件结构那么直观,即属性不都是放在Class文件的最后,而各属性可以存放的位置如下:
JDK1.7中增加到20项属性类型,JDK1.8中增加到23项属性类型(RuntimeVisibleTypeAnnotations、RuntimeInvisibleTypeAnnotations、MethodParameters),如下:
五个属性对于Java虚拟机正确解释Class文件至关重要:
ConstantValue、Code、StackMapTable、Exceptions、BootstrapMethods;
十二个属性对于通过Java SE平台的类库来正确解释Class文件至关重要:
InnerClasses、EnclosingMethod、Synthetic、Signature、RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations、RuntimeInvisibleParameterAnnotations、RuntimeVisibleTypeAnnotations、RuntimeInvisibleTypeAnnotations、AnnotationDefault、MethodParameters;
六个属性对于通过Java虚拟机或Java SE平台的类库来正确解释Class文件并不重要,但对于工具非常有用:
SourceFile、SourceDebugExtension、LineNumberTable、LocalVariableTable、LocalVariableTypeTable、Deprecated;
属性(attribute_info)的通用格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info常量的索引,表示属性的名称;
attribute_length:
给出了跟随其后的属性信息的字节长度;
info:
属性信息,各种属性结构不同;
1、Code属性
Java方法体中的代码经过javac编译处理,生成字节码的指令信息;
出现在方法表的属性集合中,也可能没有(如方法被声明为native或者abstract类型);
Code 属性的格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info类型常量的索引,常量中固定字符串"Code";
attribute_length:
表示当前属性的长度,不包括开始的 6 个字节;
max_stack:
给出了当前方法的操作数栈的最大深度(在运行执行的任何时间点都不超过),JVM运行时根据这个值分配栈帧(Stack Frame)中的操作栈深度;
max_locals:
给出了分配在当前方法引用的局部变量表中的局部变量个数,包括调用此方法时用于传递参数的局部变量;
其实是局部变量的存储空间大小,单位是Slot(JVM为局部变量分配内存的最小单位);
long和double类型的局部变量的占两个Slot,其它类型的局部变量的只占一个.
执行超出局部变量作用域后,变量占的Slot空间可以被其他变量重用;
code_length:
code_length项给出了当前方法的 code[]数组的字节数;
code_length虽然是u4类型,但0<code_length<65536,即code[]数组不能为空,也不能超过65536,所以程序中方法体不能写得过长;
code[]:
存储实现当前方法的JVM字节码指令;
一个code是u1类型,最大可以表示256个指令;
目前JVM规范已经定义了其中约200条编译值对应的指令含义;
详见后面的字节码指令简介;或官方指令说明请参考:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.9
exception_table_length:
exception_table[exception_table_length]:
数组的每个成员表示code[]数组中的一个异常处理器(Exception Handler);
start_pc 和 end_pc:
两项的值表明了异常处理器在 code[]数组中的有效范围;
handler_pc:
表示一个异常处理器的起点,它的值必须同时是一个对当前 code[]数组中某一指令的操作码的有效索引;
catch_type:
指向CONSTANT_Class_info类型常量的索引,表示要捕获异常的类型;
为0表示捕获所有类型的异常,可以用来实现finally语句;
attributes_count:
attributes[attributes_count]:
一个Code属性可以有任意数量的可选属性与之关联;
只能是LineNumberTable(程序行号信息)、LocalVariableTable(栈上变量信息)、LocalVariableTypeTable和StackMapTable属性;
在前面方法表集合中说到在ClassFileTest.class文件中的三个方法都存在Code属性,这里拿自定义的"getMap"方法来说明,其中attributes_count:"0002"表示后面有两项属性数据,前面的是"Code"属性,后面的是"Signature"属性,"Code"属性数据如下;
attribute_name_index:"0013"索引第19项的常量,即属性名称为"Code";
attribute_length:"00000036"表示当前属性后面内容的长度为54;
max_stack:"0003"表示当前方法的操作数栈的最大深度为3;
max_locals:"0002"表示分配在当前方法引用的局部变量表中的局部变量个数为2("this"和"map"变量,"this"作为参数传入);
code_length:"00000016"当前方法的code[]数组的字节数为22;
code[]:22个字节的当前方法的JVM字节码指令数据如下:
(A)、"BB0004":其中"BB"查询JVM指令集表可以看到,"new"指令后面接一个操作数,即表示指令"new #4";
(B)、"59":查表得知表示"dup"指令,后面没有操作数;
(C)、"B70005":其中"B7"查指令集表得知为"invokeinterface",后面接一个操作数,即表示指令"invokespecial #5";
同理,这些都可以在反编译信息中清楚看到…..
exception_table_length:"0000"表示没有异常处理器,后面没有exception_table[];
attributes_count:"0001"表示当前Code属性中有一个附加属性;
attributes[]:通过反编译信息看到这个附加属性是LineNumberTable,表示程序行号信息;LineNumberTable属性就不再多说了(下面有介绍),表示的数据如下:
再次给出""方法的反编译信息,可以验证上面的数据说明,如下:
2、Signature属性
JDK1.5后引入泛型后增加的,仅仅对泛型类型有意义;
可以位于类、方法或字段表的属性集合中;
任何类、接口、初始化方法或字段成员,如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types) ,则Signature属性会为它记录泛型签名信息;
因为泛型在编译阶段类型被擦除,像字段表、方法表中的描述符记录的是类型擦除后的信息,使得无法在运行阶段做反射时获得实际类型;
Signature 属性格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info类型常量的索引,表示字符串"Signature";
attribute_length:
必须为2;
signature_index:
指向CONSTANT_Utf8_info类型常量的索引,表示类签名或方法类型签名或字段类型签名;
这里接着上面的"getMap"方法分析(三个方法也只有"getMap"是泛型方法),前面分析了"Code"属性,后面的"Signature"属性数据如下;
attribute_name_index:"0017"表示索引第23项常量,即属性名称为"Signature",如下图:
attribute_length:"00000002"必须为2;
signature_index:"0018"表示索引第24项常量,表示未擦除类型前的方法类型签名"()Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>;",如上图;而擦除后的在前面方法表介绍的方法描述符中,为"()Ljava/util/Map;",如上图第22项;
更多关于泛型编译类型擦除请参考上文:《Java前端编译:Java源代码编译成Class文件的过程》4-2、解语法糖
前面有关于签名与描述符之于泛型的说明;或参考官方说明:http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.9.1
3、ConstantValue属性
表示一个常量字段的值;
只有static关键字修饰(对javac编译还得有"final")的变量才有这个属性;
位于field_info结构的属性表中;
存在则说明这个field_info结构表示的常量字段值将被分配为它的ConstantValue属性表示的值;
用于通知JVM自动为静态变量赋值;
使用ConstantValue属性赋值过程发生在引用类或接口的类初始化方法<clinit>()执行之前;
ConstantValue 属性的格式如下:
attribute_name_index:
指向CONSTANT_Utf8_info类型常量的索引,表示字符串"ConstantValue";
attribute_length:
固定为2;
constantvalue_index:
必须是一个对常量池的有效索引;
常量池在该索引处的项给出该属性表示的常量值;
常量池的项的类型表示的字段类型,可以是CONSTANT_Long、CONSTANT_FloatCONSTANT_Double、CONSTANT_Integer(int,short,char,byte,boolean)、CONSTANT_String;
从上面字段表集合和方法表集合中对于测试程序中的三个变量的分析,可以知道只有"mFinalStaticString"字段存在ConstantValue属性,实际数据是""0010 00000002 0008""(见3-6字段表集合第3点):
attribute_name_index:"0010"表示索引第16项常量,即属性名为"ConstantValue";
attribute_length:"00000002"固定为2;
constantvalue_index:"0008"表示索引第8项常量,给出该属性的常量值"java",如下图:
总结前面这些描述(包括在字段表、方法表集合的分析),得出: