Class类文件结构
通过不同的编译器(Javac编译器,jrubyc编译器,groovyc编译器等等)将代码编译成规范的class文件,虚拟机只要接收到claas文件而并不关心是class文件时哪一种编译器编译的,这样就到达了(write one,run anywhere)。
Class文件是一组以8bit为基础单位的二进制流,各个数据项目严格按顺序紧凑地排列在class文件中,中间无任何添割符。Class文件格式采用一种类似于C语言结构体的伪结构来储存数据,这种伪结构只有两种数据:无符号数和表。
无符号数属于基本的数据类型,以u1,u2,u3,u4,u8表示1个字节,2个字节,3个字节,4个字节,8个字节,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串。
表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯地以"_info"结尾。表用于描述由层次关系的复合结构的数据,整个class文件本质上就是一张表。
Class文件格式
类型 | 名称 | 数量 |
---|---|---|
u4 | magic(魔数) | 1 |
u2 | minor_version(JDK次版本号) | 1 |
u2 | major_version(JDK主版本号) | 1 |
u2 | constant_pool_count(常量池数量) | 1 |
cp_info | constan_pool(常量表) | constant_pool_count-1 |
u2 | access_flags(访问标志) | 1 |
u2 | this_class(类引用) | 1 |
u2 | super_class(父类引用) | 1 |
u2 | interfaces_count(接口数量) | 1 |
u2 | interfaces(接口数组) | interfaces_count |
u2 | filelds_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
魔数和版本号
每个class文件的头4个字节称为魔数,它的唯一作用就是确定这个文件是否为一个能被虚拟机接收的class文件。值为CAFEBABE,紧接着就是4个字节的版本号,其中前两个为次版本号,后两个为主版本号。到目前前8个字节就确定了。
常量池
紧接着主次版本号就是常量池了,第一个是常量池数量(占两个字节),接下来就是常量池表。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据(constant_pool_count),代表常量池容量计数值 (计数从1开始,其他计数从0开始,因为0有其他作用)。
constant_pool : 主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中每一项常量都是一个表,这些表都有一个共同点,就是表的开始的第一位是一个ul类型的标记位(tag),代表当前这个常量属于哪种类型
例子:
package leetcode;
public class TestClass {
private int m;
public int inc(){
return m+1;
}
}
利用16进制打开的TestClass.class文件,0016代表22-1=21个常量,后面跟随21个常量
利用javap查看TestClass.class,#1~#21代表21个常量
Classfile /E:/Eclipse/code/mycode/bin/leetcode/TestClass.class
Last modified 2019-5-15; size 371 bytes
MD5 checksum 94a379e3887efbb30790e7254f92fa69
Compiled from "TestClass.java"
public class leetcode.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // leetcode/TestClass
#2 = Utf8 leetcode/TestClass
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #3.#11 // java/lang/Object."<init>":()V
#11 = NameAndType #7:#8 // "<init>":()V
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lleetcode/TestClass;
#16 = Utf8 inc
#17 = Utf8 ()I
#18 = Fieldref #1.#19 // leetcode/TestClass.m:I
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 SourceFile
#21 = Utf8 TestClass.java
{
public leetcode.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lleetcode/TestClass;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #18 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lleetcode/TestClass;
}
SourceFile: "TestClass.java"
locals=1, args_size=1
:
在任何实例方法里面,都可以通过this关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。这个处理只对实例方法有效,如果inc()声明为static,那Args_size就不会等于1而是等于0了。
访问标志
这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
access_flags 中一共有 16 个标志位可以使用,当前只定义了其中 8 个(注:在 Java 虚拟机规范中,只定义了开头 5 种标志。JDK 1.5 中增加了后面三种。这些标志位在 JSR-202 规范中声明。),
没有使用到的标志位要求一律为 0。
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系
-
类索引用(this_class)于确定这个类的全限定名,
-
父类索引(super_class)用于确定这个类的父类的全限定名。
-
由于Java语言不允许多重继承,所以父类索引只有一个; 但是接口可以实现好几个,所以是个集合, 除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。
类索引查找全限定名的过程:
通过0001找到第一个常量,第一个常量为07 0002,0002指向第二个变量。
第二个常量标志位为01 ,length为0012,即18,后面的18个字节表示全限定名。
0003查找的过程是一样的
对于接口索引,入口的第一项为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面的接口的索引表不再占用任何字节。
字段表集合
用于描述接口或者类中声明的变量。字段包含类级变量以及实例级变量,但不包含在方法内部声明的局部变量。
每个字段的结构都如下:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
字段访问标识符( access_flags )如下:
标志 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public 访问修饰符 |
ACC_PRIVATE | 0x0002 | private 访问修饰符 |
ACC_PROTECTED | 0x0004 | protected 访问修饰符 |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final |
ACC_VOLATILE | 0x0040 | valatile |
ACC_TRANSIENT | 0x0080 | transient |
ACC_SYNTHETIC | 0x1000 | synthetic |
ACC_ENUM | 0x4000 | enum |
Java本身的语言规则所决定的:
- ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一
- 接口中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志
name_index:
对常量池的引用, 其值为常量池中的有效索引,代表着字段的简单名称。
descriptor_index:
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示
标识字符 | 含义 | 标识字符 | 含义 |
---|---|---|---|
B | 基本类型byte | J | 基本类型long |
C | 基本类型char | S | 基本类型short |
D | 基本类型double | Z | 基本类型boolean |
F | 基本类型float | V | 特殊类型void |
I | 基本类型int | L | 对象类型,如:Ljava/lang/Object; |
对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]
” 类型的二维数组,将被记录为:“[[Ljava/lang/String;
”,一个整型数组 “int[]
” 将被记录为 “[I
”。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()
的描述符为“()V
”,方法 java.lang.String toString()
的描述符为 “()Ljava/lang/String;
”,方法 int indexOf(char[] source, int sourceOffset, int sourceCount, char[]target, int targetOffset, int targetCount, int fromIndex)
的描述符为 “([CII[CIII)I
”。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
private int m;
descriptor_index后面跟随一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。
方法表集合
Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
attributes格式在下章介绍
access_flags
与属性进行对比:
- 因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。
- 与之相对的,synchronized、native、strictfp和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、CC_STRICTFP和ACC_ABSTRACT标志。
通过分析得出了public void init()
方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。
与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器<clinit>
方法和实例构造器<init>
。
属性表
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息;限制相对其他表相对宽松一些。对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。
虚拟机规范预定义的属性有很多,这里只简单介绍一下Code属性
Code属性表的结构
Java虚拟机执行字节码是基于栈的体系结构
。但是与一般基于堆栈的零字节指令又不太一样,某些指令(如invokespecial)后面还会带有参数
继续分析Code中的剩余两个属性:LineNumberTable 和 LocalVariableTable
LineNumberTable
属性:
- 用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。
- 如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
LocalVariableTable
属性:
- 用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系, 它也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么整个Class文件里,Code属性用于描述代码,所有的其他数据项目就用于描述元数据。
详细内容及其他属性请参考《深入理解Java虚拟机》
参考
- 《深入理解Java虚拟机》–周志明
- https://blog.csdn.net/qq_31156277/article/details/80108277