学习资料来自:
https://blog.youkuaiyun.com/zhangjg_blog/article/details/21781021#t4
深入理解Java JVM虚拟机
https://blog.youkuaiyun.com/zhangjg_blog/article/details/21557357
Class文件是一组以8位字节数为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件存储的内容几乎全部是程序的必要的数据,没有空隙存在。
Class文件类似于C语言结构体的伪结构的存储数据,这种结构中只有两种数据类型,无符号数和表。
无符号数属于基本的数据类型,图下展示了对应的类型介绍:
无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成的字符串值。
表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表习惯性以"info"结尾,表用于描述有层次结构的复合结构数据。整个Class文件本质上就是一张表。
Class文件结构类似如下所示:
ClassFile{
magic u4,
minor_version u2,
major_version u2,
constant_pool_count u2,
constant_pool cp_info*constant_pool_count,
access_flags u2,
this_class u2,
super_class u2,
interface_count u2,
interfaces u2 * interface_count,
fields_count u2,
fields field_info * fields_count,
methods_count u2,
methods method_info * methods_count,
attributes_count u2,
attributes attributes_info * attributes_count
}
无论是无符号数还是表,当需要描述同一类型但是数量不定的多个数据的时候,经常会使用一个前置容量计数器加若干个连续的数据项的形式,这时候称之为某一类型的集合。
Class文件介绍
上面列出了文件结构,这里做个整理。
这里讲下面java文件通过javac编译为.class文件:
public class Test {
private static int num=3;
private static final int finalnum=3;
private int check;
public Test(){
check=23;
}
}
class文件对应的字节流码:
cafe babe 0000 0034 0018 0a00 0500 1309
0004 0014 0900 0400 1507 0016 0700 1701
0003 6e75 6d01 0001 4901 0008 6669 6e61
6c6e 756d 0100 0d43 6f6e 7374 616e 7456
616c 7565 0300 0000 0301 0005 6368 6563
6b01 0006 3c69 6e69 743e 0100 0328 2956
0100 0443 6f64 6501 000f 4c69 6e65 4e75
6d62 6572 5461 626c 6501 0008 3c63 6c69
6e69 743e 0100 0a53 6f75 7263 6546 696c
6501 0009 5465 7374 2e6a 6176 610c 000c
000d 0c00 0b00 070c 0006 0007 0100 0454
6573 7401 0010 6a61 7661 2f6c 616e 672f
4f62 6a65 6374 0021 0004 0005 0000 0003
000a 0006 0007 0000 001a 0008 0007 0001
0009 0000 0002 000a 0002 000b 0007 0000
0002 0001 000c 000d 0001 000e 0000 002b
0002 0001 0000 000b 2ab7 0001 2a10 17b5
0002 b100 0000 0100 0f00 0000 0e00 0300
0000 0500 0400 0600 0a00 0700 0800 1000
0d00 0100 0e00 0000 1d00 0100 0000 0000
0506 b300 03b1 0000 0001 000f 0000 0006
0001 0000 0002 0001 0011 0000 0002 0012
注: 1个字节是8位,最多表示0到256 ,而一位16最多只表示到16,即F表示16,要表示到256,就还需要第二位,所以1个字节占2个16进制位,一个16进制位占0.5个字节,即一个字节表示2个十六进制数。
魔数
可以看到第一个字符为u4类型的magic变量,称之为魔数,该变量来表明是否是一个虚拟机能接受的Class文件。从上面的Class文件中可以看到,4个字节magic是0xcafebabe,咖啡宝贝的意思,哈哈。
主次版本号
minor_version代表Java的次版本号占两个字节,major_version代表主版本号也占两个字节。在上述Class文件中:
- minor_version:0x0000
- major_version:0x0034
34代表十进制的52,说明Java版本为1.8,可以通过java -version
来验证准确性:
常量池
接下来的是常量池的入口,常量池可以理解为Class文件中的资源仓库。由于常量池的数量是不固定的,所以需要放置u2类型的数据来表示常量池容量的计数值constant_pool_count,即上述Class文件中的0x0018,转换为十进制为24,表明当前常量池中有23项常量,索引范围为1-24。需要注意的是constant_pool_count计数开始为1,而不是一般计算机中的0。
常量池中主要存放着两大类常量:字面量(Literal)以及符号引用(Symbolic References),字面量接近于Java层中的常量的概念,如文本字符串,申明为final的常量值等等,而符号引用则属于编译原理方面的概念,主要包括下面三类:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
写过jni的对上面的应该就不陌生了,如果需要,可以查阅这篇文章。
Java在执行javac编译的时候,不像c和c++一样有链接的步骤,而是在虚拟机加载Claaa文件时候进行动态的链接。**也就说在Class文件中不会保存各个方法,字段的最终内存信息,音这些字段,方法的符号引用不经过运行期转化的话是无法获得真正的内存入口的。**当虚拟机运行的时候,需要从Class文件的常量池获得对应的符号引用,然后在类创建或者运行时解析,翻译到具体的内存地址当中。
在常量池中,每一项常量都是一个表,JDK1.7之前有11种不同的表结构,在1.7时新增了三种结构,分别是CONSTANT_MethodHandle_info,CONSTANT_MethodType_info,CONSTANT_InvokeDynamic_info。 这14中标有一个共同点,即在表的开始的第一位是一个u1类型的标志位(记为tag),代表当前这个常量属于哪种常量类型。具体的常量类型如下所示:
从上面的Class字节码中可以知道第一项常量的标志位是0x0a,转换成十进制为10,查阅可知10是一种类中方法的符号引用类型CONSTANT_Methodref_info,CONSTANT_Methodref_info表示普通(非接口)方法符号引用,具体的结构如下所示:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index; //CONSTANT_Class_info
u2 name_and_type_index; //CONSTANT_NameAndType_info
}
可以看到u1为tag标志位即十进制的10,下面的class_index表示它指向一个CONSTANT_Class_info的数据项, 这个数据项表示被引用的方法所在的类型。class_index以下的两个字节是一个叫做name_and_type_index的索引, 它指向一个CONSTANT_NameAndType_info,这个CONSTANT_NameAndType_info描述的是被字段或者方法的部分符号的引用。
CONSTANT_Methodref_info这个符号引用包括两部分, 一部分是该方法所在的类, 另一部分是该方法的方法名和描述符。 这就是所谓的 “对方法的符号引用”。
这里如果一直通过Class的字节码去看就太费劲了,我们可以javap来代替对应的查看操作,调用javap -p Test
获得常量池的信息,截图如下:
上图中Constant pool表示Class文件中的常量池,图中总共23个常量值,跟我们上面通过字节码计算的方式得出的结果一致。就很清晰了,我们也不用去一个字节一个字节算出结果,还容易算错。可以看到第一个CONSTANT_Methodref_info对应的是init方法,来自于Obejct类,返回值为void,参数为空。
14种常量项的结构总表如下:
访问标志
在常量池结束之后,紧接着的两个字节表示访问标志,这个标志用于识别类或者接口层次的访问信息,包括:
- 这个Class是类还是接口
- 是否定义为public 类型
- 是否定义为abstract类型
- 如果是类,是否被声明为final类型
- ...
标志位以及含义如下表所示:
这个我们也可以通过javap命令来看到对应的标志:
可以看到Test类的flag为ACC_PUBLIC和ACC_SUPER。
类索引,父索引和接口索引集合
类索引(this_class)以及父类索引(super_class)都是一个u2类型的数据,接口索引集合(interfaces)是一组u2类型的数据集合,在Class文件中,通过上述三个数据来确定这个类的继承关系。
类索引确定这个类的全限定名,福类索引确定这个类的父类的全限定名,接口索引集合用来描述该类实现了哪些接口
类索引,父类索引集合都按顺序排列在访问标志之后,两种索引都用一个u2类型的索引值标识,各自指向了一个类型为CONSTANT_Utf8_info类型的常量中的全限定名字符串。
对饮接口索引集合,入口第一项为u2类型的接口计数器,记录接口索引表的容量。如果没实现任何接口,则为0,后面接口的索引表不占用任何字节。
这里个Test.java,使用javap来反编译查看对应的相关索引:
//TestInters为空接口
public class Test implements TestInters {
private static int num=3;
private static final int finalnum=3;
private int check;
public Test(){
check=23;
}
}
反编译如下:
4Test,即类本身,5为继承的类,这里为Object类,6为接口TestInters,为了验证,在重新测试如下:
//继承跟接口
public class Test extends Father implements TestInters,TestInter2,TestInters1 {
private static int num=3;
private static final int finalnum=3;
private int check;
public Test(){
check=23;
}
}
结果如下:
上述结果说明验证正确。
字段表集合
字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,**包括在方法内部声明的局部变量。**一个字段可以包含的信息如下:
- 字段的作用域(public,private,protected)
- 实例变量还是类变量(有无static修饰符)
- 可变性(final)
- 并发可见性(volatile,强制从主内存中读写)
- 是否可序列化(transient)
- 字段数据类型(基本类型,对象,数组)
- 字段名称
各个修饰符都可以用布尔值来设定是否存在,对于字段的数据类型以及名称,则只能引用常量池中的常量来描述了。字段表的结构如下所示:
字段修饰符在access_flags之中。与类中的access_flags类型类似,都是一个u2类型的数据,标志位含义如下所示:
随后的字段是name_index和descriptor_index,都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。
简单名称是就是我们在申明变量时候写的名称,如:
int kk=0;//kk就是字段的简单名称
void add(); //add就是方法的简单名称
方法和字段的描述符也很好理解,尤其是写过jni的同学就更简单了,定义一个com.lin.Test.java文件
public class Test{
private byte add(int[] a,String b,boolean c,float d){
....
}
}
add方法通过描述符来描述就是:
([ILjava/lang/String;ZF)B
这里就不详细说了,可以查阅Java描述符自行学习。 后面的attributes_count代表着该字段属性的计数器,如果没有额外的属性则为0,如果有,则在类型为attribute_info(即属性表)的attributes中会有对应的数据。属性表在后面进行学习整理。
方法表集合
Class文件存储格式中对方法的描述与对字段的描述几乎是一样的:
因为volatile以及transient关键字不能修饰方法,所以方法表的访问标志中没有ACC_VOLATILE和ACC_TRANSIENT标志。但是可以使用synchronized,native,strictfp和abstract关键字修饰方法,则对应增加了访问标志:
方法里的Java代码会经过编译器编译成字节码指令后,村房子方法属性表集合中一个名为"Code"的属性里。
如果父类方法在子类中没有被重写,那么方法表集合中就不会出现父类的方法信息,但是有可能出现由编译器自动添加的方法,比如<init>和<clinit>方法。
对于每个属性,他的名字需要从常量池中引用一个CONSTANT_Utf8_info的常量来表示,而属性值的结构完全是自定义的,只要通过一个u4的长度属性说明属性所占用的位数即可:
Code属性
Java中的方法体的代码经过javac编译处理后,最终会变成字节码指令存储在Code属性中。Code属性出现在方法表的属性集合之中,但并非所有方法都存在Code属性,如接口以及抽象类中的抽象方法就不存在。Code结构如下所示:
- attribute_name_index为CONSTANT_Utf8_info常量索引,固定值为“Code”,代表Code属性名称。
- attribute_length代表属性值的长度,整个属性值的长度等于整个属性表的长度减去6个字节(attribute_name_index和attribute_length占的字节)。
- max_stack代表操作数栈深度的最大值,任意时刻,操作数栈都不会超过这个深度。虚拟机运行时需要根据这个值来分配栈帧中的操作栈深度。
- max_locals代表了局部变量表所需要的存储空间。max_Locals单位为Slot,是虚拟机为局部变量分配内存的最小单位,1个Slot占用32位,即4字节。局部变量表中的Slot是可以重用的,当代码执行超出一个局部变量的作用域时候,这个Slot就可以被其他局域变量使用。javax编译器会根据变量的作用域分配Slot给各个变量使用,然后计算出max_locals的大小。
- code_length和code用来存储java源程序编译生成的字节码指令。code_length标识字节码长度,code适用于存储字节码指令的一系列字节流。每个字节码指令是一个u1类型的单字节,也就意味着可以表示256条指令。目前虚拟机规定了大概200条指令。需要注意的是,虽然code_length有u4的容量。 ,但是虚拟机规范限制了一个方法不允许超过65536条字节码指令,即实际上只有u2的长度。
- exception_info是异常处理表集合,异常表对于Code而言不一定存在,即我们日常可能会用到的
try catch finally处理机制
。
ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才能使用这项属性,如果在java代码中声明如下代码:
int x=123;
static int k=123;
两只变量赋值的方式是有差异的,对于非static变量的赋值在实例构造器<init>中进行;对于类变量的赋值则由两种方式,在类构造器<clinit>中或者使用ConstantValue属性,具体的实现由虚拟机决定。
ConstantValue属性只能适用于基本类型和String。
InnerCalsses属性
InnerCalsses属性用于记录内部类和宿主类之间的关联。
Deprecated和Synthetic属性
Deprecated和Synthetic属性都属于标志类型的布尔属性,只存在有和没有的区别。
-
Deprecated标识某个类,字段或者方法,已经被程序作者推荐不使用了,即在代码中通过@Deprecated注解设置。
-
Synthetic属性标识该字段或者方法不由Java源码直接产生,而是由编译器自行添加的。所有由非用户代码产生的类,方法,字段都应至少设置Synthetic属性和ACC_SYNTHETIC标志中的一项。 但是<init>方法和<clinit>方法例外。
Signature属性
JDK1.5之后增加到Class文件规范当中,是一个可选的定长属性,可以出现于类,字段表和方法表中,任何类,接口,初始化方法或者成员的泛型签名如果包含了类型变量或者参数化类型,则Synthetic属性会为他记录泛型签名信息。
由于Java语言泛型采用的是伪擦除法实现的伪泛型,在字节码中,泛型信息编译之后通通被擦掉,使用擦除法好处在于实现简单,但是坏处也有,就是无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,如运行期做反射处理无法获得泛型信息。Signature属性则弥补了这个缺陷。
BootstrapMethods属性
主要用于保存invokeddynamic指令引用的引导方法限定符。