虚拟机可以运行在各种不同平台上,这些虚拟机都可以载入和执行同一种与平台无关的字节码,从而实现了程序的“一次编写,到处运行”。各种不同平台的虚拟机与所有平台都统一使用字节码存储程序。
Java语言中的各种变量,关键字和运算符号的语义最终都是由多条字节码命令组合而成的。
Class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有添加任何分隔符。
整个Class文件本质上是一张表,由下图所示数据项构成。
魔数与Class文件的版本
每个Class文件的头4个字节成为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。Class文件的魔数值为"0xCAFEBABY"。
紧接着魔数的4个字节存储的是Class文件的版本号:第5个和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。
一段简单的Java代码
package com.ljessie.jvm;
public class TestClass {
private int m;
public int inc(){
return m+1;
}
}
使用Javac命令编译TestClass.java,生成TestClass.class文件,然后使用vim命令查看TestClass.class文件,解决乱码之后,结果如下图:
头四个字节魔数的十六进制表示是:cafe babe,第五和第六个字节次版本号为:00 00,第七和第八个字节主版本号为:00 34,也就是十进制的52。
常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池数量计数值。
这个容器计数是从1开始的,在十六进制是13,即十进制的19,即常量池中有18项常量
TestClass_ConstantPool.jpg
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量比较接近于Java语言层面的常量概念,如字符串和final修饰的常量值等。而符号引用则包含下面三类常量:
类和接口的全限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符
手工解析常量池比较痛苦,我这里直接使用javap -verbose C:\workspace\BioDemo\src\main\java\com\ljessie\jvm\TestClass.class命令,查看TestClass.class文件得到
Classfile /C:/workspace/BioDemo/src/main/java/com/ljessie/jvm/TestClass.class
Last modified 2020-2-13; size 291 bytes
MD5 checksum 4ab0ac51fd79f1b9b4a7745e53d4c434
Compiled from "TestClass.java"
public class com.ljessie.jvm.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/ljessie/jvm/TestClass.m:I
#3 = Class #17 // com/ljessie/jvm/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/ljessie/jvm/TestClass
#18 = Utf8 java/lang/Object
{
public com.ljessie.jvm.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
}
SourceFile: "TestClass.java"
常量池的第一项,指向第4和第15项常量,第4项常量指向第18项常量,即java/lang/Object,第15项常量指向第7和第8项,第7项指向<init>,第8项指向()V。
访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public型;是否定义为abstract类型;如果是类的话,是否被生命为final等。具体的标志位以及标志的含义见下表
access_flags一共有16个标志位可用,当前只定义了其中8个,没有使用到的标志位一律要求为0。TestClass是一个普通Java类,被public修饰,不是final和abstract,因此,ACC_PUBLIC,ACC_SUPER为真,其他6个值为假。因此它的access_flags值应为0x0001|0x0020=0x0021。
访问标志21.jpg
即用Javap命令查看TestClass.class文件中的flags
访问标志flag.jpg
类索引,父类索引与接口集合
类索引(this_class)和父类索引(super_class)都是一个u2型数据,接口索引集合是一组u2类型的数据的集合。Class文件中由这三项数据来确定这个类的继承关系。
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
类索引,父类索引和接口索引集合都按顺序排列在访问标志之后。类索引和父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
类索引.jpg
类索引为0003,父类索引为0004。类索引0003在常量池中指向第17项常量,第17项常量是:com/ljessie/jvm/TestClass;父类索引0004,指向常量池中第18项常量,第18项常量是:java/lang/Object
类索引2.jpg
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段不包括局部变量。描述一个字段可以包含的信息有:作用域(public,private等),实例变量还是类变量(static),可变性(final),并发可见性(volatile),字段数据类型(int,long等),字段名称。这些信息,各个修饰符都是布尔值,要么有某个修饰符,要么没有。而字段名称和数据类型引用常量池中的常量来描述。下图表示字段表的最终格式。
字段表结构.jpg
字段修饰符放在access_flags中,与类中的access_flags类似,都是一个u2型数据,可以设置的标志位和含义如下图。
字段访问标志.jpg
ACC_PUBLIC,ACC_PRIVATE,ACC_PROTECTED最多只能选一个,ACC_FINAL,ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC,ACC_STATIC,ACC_FINAL。
跟随access_flags标志的是两项索引值:name_index和descriptor_index。他们都是对常量池的引用,代表着字段的简单名称以及字段和方法的描述符。
全限定名: com/ljessie/jvm/TestClass是这个类的全限定名。
简单名称:没有类型和参数修饰的方法或者字段名称,TestClass中的inc()方法和m字段的简单名称分别是"inc"和m。
描述符:描述字段的数据类型,方法的参数列表和返回值。根据描述符规则,基本数据类型和void类型都用一个大写字母表示,而对象类型则用L加对象的全限定名来表示。
描述符标识.jpg
对于Test Class.class文件来说,字段表集合如下图:
字段表class.jpg
0001表示这个类中只有一个字段表数据;0002是access_flags标志,值为ACC_PRIVATE;0005代表字段的简单名称name_index,代表常量池第五项:m;0006代表字段的描述符descriptor_index,代表常量池第六项:I,描述符I又标识基本类型int。可推断出Test Class中定义的字段为:"private int m;"。
字段表字段.jpg
字段表集合中,不会列出从父类继承来的字段。但是在内部类中,会自动添加指向外部类实例的字段。
方法表集合
方法表的结构和字段表一样,依次包括访问标志(access_flags),名称索引(name_index),描述符索引(descriptor_index)和属性表集合(attributes)。下图表示方法表结构。
方法表结构.jpg
方法表的访问标志中增加了ACC_SYNCHRONIZED,ACC_NATIVE等标志。
方法访问标志.jpg
方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面。
方法表class.jpg
在TestClass中,方法表第一个数据0002代表集合中有两个方法(<init>和inc()),第一个方法的访问标志为0001,也就是ACC_PUBLIC;名称索引值为0007,代表常量池第7项<init>;描述符索引为0008,代表常量池第8项()V;属性表计数器0001,表示属性表集合有一项属性;属性名称索引为0009,对应常量池Code,说明此属性是方法的字节码描述。
方法表常量池.jpg
如果父类的方法在子类中没有被重写,方法表集合中,就不会出现来自父类的方法信息。但是有可能会出现编译器自动添加的方法,例如<init>。
属性表集合
在Class文件,字段表,方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
虚拟机规范中,预定义的属性如下表:
预定义属性1.jpg
预定义属性2.jpg
预定义属性3.jpg
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构是自定义的。属性表结构如下:
属性表结构.jpg
1.Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在放发表的属性集合中,但并非所有的方法都必须存在这个属性,譬如接口和抽象类中的方法,就不存在Code属性。
Code属性表结构.jpg
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码和元数据两部分,那么在整个Class文件中,Code属性用于描述代码,所有其他数据都用于描述元数据。
继续以TestClass.class为例,分析<init>中的Code属性。如下图
Code属性结构实例.jpg
0001:操作数栈的最大深度,在方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机运行的时候,需要根据这个值分配栈帧中的操作栈深度。
0001:本地变量表容量,代表了局部变量表所需的存储空间。单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte,char,int,boolean等不超过32位的数据类型,每个局部变量需要1个Slot,double和long这种64位的数据类型,需要两个Slot。Slot可以重用,Javac编译器会根据变量的作用域分配Slot给各个变量使用。
0005:字节码长度为5。
然后按照顺序依次读入五个字节码指令(每个指令代表含义,查看虚拟机字节码指令表):
2A:代表指令为aload_0,含义是将第0个Slot中为reference类型的,本地变量推送到操作数栈顶。
B7:代表指令为invokespecial,作用是以栈顶的reference类型的数据所指向的对象,作为方法接受者,调用此对象的实例构造器方法,private方法或者父类方法。
0001:为invokespecial参数,查常量池0001对应的常量为实例构造器<init>方法的符号引用。
B1:代表指令return,含义是返回此方法,返回值为void。
属性表集合中还有其他属性,这里就不展开说了。
网友评论