Jvm内存模型分析-class文件结构
Jvm内存模型分析-class文件结构
以下面的类为例介绍一下class文件的结构
package jvm;
public class MainTest {
public static int a=0;
public static String b="time";
public int name;
public void test() {
int c=0;
}
}
Javap –v反编译结果为
Classfile /C:/Users/jiajia/eclipse-workspace/jvm/Test/bin/jvm/MainTest.class
Last modified 2019-5-1; size 506 bytes
MD5 checksum f5918edd3feb02f16c996163e06aa82f
Compiled from "MainTest.java"
public class jvm.MainTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // jvm/MainTest
#2 = Utf8 jvm/MainTest
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 b
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 name
#10 = Utf8 <clinit>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Fieldref #1.#14 // jvm/MainTest.a:I
#14 = NameAndType #5:#6 // a:I
#15 = String #16 // time
#16 = Utf8 time
#17 = Fieldref #1.#18 // jvm/MainTest.b:Ljava/lang/String;
#18 = NameAndType #7:#8 // b:Ljava/lang/String;
#19 = Utf8 LineNumberTable
#20 = Utf8 LocalVariableTable
#21 = Utf8 <init>
#22 = Methodref #3.#23 // java/lang/Object."<init>":()V
#23 = NameAndType #21:#11 // "<init>":()V
#24 = Utf8 this
#25 = Utf8 Ljvm/MainTest;
#26 = Utf8 test
#27 = Utf8 c
#28 = Utf8 SourceFile
#29 = Utf8 MainTest.java
{
public static int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static java.lang.String b;
descriptor: Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC
public int name;
descriptor: I
flags: ACC_PUBLIC
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_0
1: putstatic #13 // Field a:I
4: ldc #15 // String time
6: putstatic #17 // Field b:Ljava/lang/String;
9: return
LineNumberTable:
line 4: 0
line 5: 4
LocalVariableTable:
Start Length Slot Name Signature
public jvm.MainTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #22 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljvm/MainTest;
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: iconst_0
1: istore_1
2: return
LineNumberTable:
line 8: 0
line 9: 2
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Ljvm/MainTest;
2 1 1 c I
}
SourceFile: "MainTest.java"
1.Class文件结构概述
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项 时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定, Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础,所以这里要先介绍这两个概念。
无符号数属于基本的数据类型,以ul、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以 “info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class文本质上就是一张表,它由下图所示的数据项构成
2.魔数与class版本(Majic,Minor_version,Major_version)
位置:0-3字节
java的魔数统一为 0xCAFEBABE (来源于一款咖啡)。它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class文件。很多文件存储标准中都使用数来进行身份识别譬如图片格式,如gif或者jpcg等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。
紧接着魔数的4个字节存储的是 Class文件的版本号:第5和第6个字节是次版本号,第七第八字节是主版本号。
java的版本号是从45开始的,JDK1之后的每个JDK大版本发布主版本号向上加1(JDK1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的 Class文件,但是不能运行以后的class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class文件。
3.常量池(Constant_pool)
紧接着主次版本号之后的就是常量池入口。常量池可以理解为class文件的资源仓库。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于java语言层面的常量的概念,比如文本字符串,声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
1. 类和接口的全限定名
2. 字段的名称和描述符
3. 方法的名称和描述符
常量池中常量的数量是不固定的,所以常量池的入口处有一个u2类型的数据,表示常量池中常量的数值大小
0x001E转换为10进制为30,意为常量池中有29个常量。(注意常量池计数是从1而不是0开始)
可以用javap -v 命令反编译查看Class文件的信息
从下图中也可以看出常量池中有29个常量
常量池中的每个常量都是一个表,共有11种不同的表结构,它们有一个共同的特点,就是表开始的第一位都是一个u1类型的标志位(tag,取值为1到12,缺少标志为2的数据类型)。
常量池的项目类型
常量池的数据结构总表(图片来自于《深入理解java虚拟机》)
以上一个类文件为例,简单分析一下常量池的信息
常量池的入口从第9和第10个字节开始,ox001E代表有29个常量项,接下来0x07代表第一项的tag值为7(CONSTANT_Class_info),接下来两个字节0x0002代表指向全限定名常量项的索引索引值为2,接下来ox01代表第二项的tag值为7(CONSTANT_Utf8_info)接下来两个字节0x000c代表字符串占用的字节数length为12,接下来有length(12)个长度为u1(即一个字节)的字符串,即0x6A 74 6D 2F 4D 61 69 6E 54 65 73 74就表示了字符串jvm/MainTest。
和javap –v命令分析的结构一样
4.访问标志
常量池之后,紧跟着2个字节来表示访问标志,用于识别一些类或者接口层次的访问信息,包括:这个class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是的话是否被声明为final等。具体的标志,以及标志的含义如下:
针对MainTest这个类来说,其访问标志应该是ACC_PUBLIC、ACC_SUPER这2个标志为真,所以其值为 0x0001 | 0x0020 = 0x0021
5.类索引、父类索引、以及接口索引集合
类索引(thiss_classsuper class)都是一个u2类型的数据,而接口索引集合(interfaces Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang. Object外,所有Java类的父类索引都不为0接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements语句(如果这个类本身是一个接口,则应当是 extends语句)后的接口顺序从左到右排列在接口索引集合中。
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为 CONSTANTClass info的类描述符常量,通过 CONSTANT Class_ iinfo类型的常量中的索引值可以找到定义在 CONSTANT_utf8 info类型的常量中的全限定名字符串。
对于接口索引集合,入口的第一项——u2类型的数据为接口计数器(interfacescount), 表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
如上图:0x0021为访问标志,0x0001为类索引,0x0003为父类索引。由于没有实现接口,所以Interfaces_count为0 。
6.字段表集合
字段表(feld_info)用于描述接口或者类中声明的变量。字段(feld)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。我们可以想一想在Java中描述一个字段可以包含什么信息?可以包括的信息有:字段的作用域(public private、 protected修饰符)、是实例变量还是类变量,可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
字段信息结构表
字段访问标志:
描述符标识字符含义
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
接口索引后面紧跟着的是字段表信息,字段表的入口前2个字节表示字段的个数,在本例子中只定义了个三个字段,所以其值为0x0003,后面紧跟着的是该字段的描述表。
第一个变量为public static int a=0;所以access_flags访问标志应该为0x0001|0x0008=0x0009
0x0005指向了常量池中的第五个索引(变量名name_index),0x006指向了常量池中的第六个索引(字段名简单描述descriptor_index)。0x0000表示attribute_count = 0,说明本字段没有额外的描述信息。
可参考下图理解
7.方法表集合
方法表的结构与字段表的结构是一样的
从MainTest 类的class文件的信息中可以发现,类中有三个方法如下:
方法表的入口0x0003代表有三个方法
0x0008代表第一个方法的访问标志access_flags为static
0x000A代表第一个方法的方法名指向常量池中的第10个索引
0x000B代表第一个方法的方法描述符指向常量池中的第11个索引
0x0001代表第一个方法的属性集合有一个属性
0x000C代表第一个属性的索引为常量表中的12,对应常量Code.说明此属性是方法字节码描述,这个属性就存储了方法里的java代码编译后的字节码指令。
8.属性表
在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
Code属性
Java方法里的代码被编译处理后,变为字节码指令存储在方法表的Code属性里,但并不是所有的方法表里都有Code属性,例如接口或抽象类中的方法就可能没有该属性。
Code属性数据结构:
上边已经说到0x0001代表有一个属性,并且0x000c代表是code属性
那么根据code属性的数据结构可知0x000c代表属性名,随后四个字节0x00002E代表code属性的长度为46个字节。
紧随attribute_length属性后的两个字节0x0001代表操作数栈的数量为1,随后0x0000代表本地变量表的数量为0.再往后四个字节0x0000000a代表字节码指令的长度为10,接下来10个字节为单字节具体的的字节码指令。
例:0x03 iconst_0 将int型0推送至栈顶
0xb3 putstatic 为指定的类的静态域赋值 0x000d指向了常量池中的第13个索引i.
0x12 ldc 将int, float或String型常量值从常量池中推送至栈顶 0x0f指向了常量池中的第15个索引。
0xb3 putstatic 为指定的类的静态域赋值 0x0011代表了常量池中第17个索引
0xb1 return 从当前方法返回void
方法结束
对应的代码如下:
再往后还有异常信息和其他属性,暂时忽略…