Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。
任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(如类或接口也可以动态生成,直接送入类加载器中)
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前(大端)的方式分割成若干个8个字节进行存储。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:
Class结构:
Class文件的头4个字节被称为魔数(Magic Number),作用是确定这个文件是否为一个能被虚拟机接受的Class文件。值为 0xCAFEBABE。
minor version:第5和6字节存储次版本号。
次版本号,曾经在Java 2出现前被短暂使用过,从JDK 1.2以后,直到JDK 12之前次版本号均未使用,全部固定为零。
而到了JDK 12时期,由于JDK提供的功能集已经非常庞大,有一些复杂的新特性需要以“公测”的形式放出,所以设计者重新启用了副版本号,将它用于标识“技术预览版”功能特性的支持。如果Class文件中使用了该版本JDK尚未列入正式特性清单中的预览功能,则必须把次版本号标识为65535,以便Java虚拟机在加载类文件时能够区分出来。
major_version:第7和8字节存储主版本号。
Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。
例如:JDK 1.1能支持版本号为45.0~45.65535的Class文件,无法执行版本号为46.0以上的Class文件,而JDK 1.2则能支持45.0~46.65535的Class文件。
JDK 11
openjdk 11.0.6 2020-01-14
cafe babe 0000 0037 ...
JDK 8
java version "1.8.0_241"
cafe babe 0000 0034 ...
常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值,从1开始计数。
0x23代表35,即常量池中有34项常量,索引1~34。
cafe babe 0000 0037 0023
设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。
Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,容量计数都与一般习惯相同,是从0开始。
常量池主要存放:
常量池中每一项常量都是一个表,表结构起始的第一位是个u1类型的标志位,代表着当前常量属于哪种常量类型,之后的结构与常量类型相关。
如:
cafe babe 0000 0037 0023 0a00 0700 1409
0x0a代表Methodref,接下来u2(0x0007)代表声明方法的类描述符(Class)的索引项,后面的u2(0x0014)代表名称及类型描述符(NameAndType)的索引项。
使用javap -verbose Test.class
查看字节码:
Classfile .../Test.class
Last modified 2020年3月6日; size 494 bytes
MD5 checksum 895a2d8401ca34e5c6d1da1a6cb7f5f2
Compiled from "Test.java"
public class Test
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // Test
super_class: #7 // java/lang/Object
interfaces: 0, fields: 2, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #7.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #23 // Test
#4 = String #24 // this is a test
#5 = Methodref #25.#26 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Fieldref #3.#27 // Test.count:I
#7 = Class #28 // java/lang/Object
#8 = Utf8 TEST
#9 = Utf8 Ljava/lang/String;
#10 = Utf8 ConstantValue
#11 = Utf8 count
#12 = Utf8 I
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 print
#18 = Utf8 SourceFile
#19 = Utf8 Test.java
#20 = NameAndType #13:#14 // "<init>":()V
#21 = Class #29 // java/lang/System
#22 = NameAndType #30:#31 // out:Ljava/io/PrintStream;
#23 = Utf8 Test
#24 = Utf8 this is a test
#25 = Class #32 // java/io/PrintStream
#26 = NameAndType #33:#34 // println:(Ljava/lang/String;)V
#27 = NameAndType #11:#12 // count:I
#28 = Utf8 java/lang/Object
#29 = Utf8 java/lang/System
#30 = Utf8 out
#31 = Utf8 Ljava/io/PrintStream;
#32 = Utf8 java/io/PrintStream
#33 = Utf8 println
#34 = Utf8 (Ljava/lang/String;)V
...
常量池中存在一些代码中并没有出现的常量,如LocalVariableTable等,他们是由编译器自动生成的。它们将会被用来描述一些不方便使用“固定字节”进行表达的内容,譬如描述方法的返回值是什么,有几个参数,每个参数的类型是什么等。
接下来的u2代表访问标志(access_flags),用于识别一些类或者接口层次的访问信息。
各个标志之间取或后的值为flag值。
比如:0x0021代表ACC_PUBLIC,ACC_SUPER。
Class文件中由这三项数据来确定该类型的继承关系。
字段表用于描述接口或类中声明的变量。包括类级变量及实例变量,不包括局部变量,不会列出继承来的字段,可能出现代码中不存在的字段。
字段可以包括的修饰符:
下两项都是对常量池项的引用:
对于数组类型,每一维度将使用一个前置的‘[‘字符来描述,如一个定义为java.lang.String[][]
类型的二维数组将被记录成[[Ljava/lang/String;
,一个整型数组int[]
将被记录成[I
。
用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。
如:int indexOf(char[]source,int sourceOffset, int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”
Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,仅在访问标志和属性表集合的可选项中有所区别。
如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样地,有可能会出现由编译器自动添加的方法,最常见的便是类构造器<clinit>()
方法和实例构造器<init>()
方法。
Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
属性:
Code
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中。
Exceptions:列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。
LineNumberTable:描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。
LocalVariableTable:描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系。
LocalVariableTypeTable:使用字段的特征签
名来完成泛型的描述。
ConstantValue:通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。
InnerClass:记录内部类与宿主类之间的关联。
StackMapTable:在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
Signature:记录泛型签名信息。可以出现于类、字段表和方法表结构的属性表中。
RuntimeVisibleAnnotations:记录了类、字段或方法的声明上记录运行时可见注解。
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。
由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。
如果不考虑异常处理的话,那Java虚拟机的解释器可以使用下面这段伪代码作为最基本的执行模型来理解:
do {
自动计算PC寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作码;
if (字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
} while (字节码流?度 > 0);
在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息。
如:iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。
因为Java虚拟机的操作码长度只有一字节,Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持它。大多数对于boolean、byte、short和char类型数据的操作,实际上都是使用相应的对int类型作为运算类型(Computational Type)来进行的。
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
注:_<n>中n指1,2,3。iload_0等价于操作数为0的iload
对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
Java虚拟机直接支持以下数值类型的宽化类型转换:
窄化类型转换必须使用显式转换:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
前面四条调用指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
方法返回指令是根据返回值的类型区分的,包括ireturn(boolean、byte、char、short和int类型使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。
使用athrow显式抛出异常,用异常表处理异常。
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,锁)来实现的。
方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。
同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义。
void onlyMe(Foo f) {
synchronized(f) {
doSomething();
}
}
生成如下字节码:
Method void onlyMe(Foo)
0 aload_1 // 将对象f入栈
1 dup // 复制栈顶元素(即f的引用)
2 astore_2 // 将栈顶元素存储到局部变量表变量槽 2中
3 monitorenter // 以栈定元素(即f)作为锁,开始同步
4 aload_0 // 将局部变量槽 0(即this指针)的元素入栈
5 invokevirtual #5 // 调用doSomething()方法
8 aload_2 // 将局部变量Slow 2的元素(即f)入栈
9 monitorexit // 退出同步
10 goto 18 // 方法正常结束,跳转到18返回
13 astore_3 // 从这步开始是异常路径,?下面异常表的Taget 13
14 aload_2 // 将局部变量Slow 2的元素(即f)入栈
15 monitorexit // 退出同步
16 aload_3 // 将局部变量Slow 3的元素(即异常对象)入栈
17 athrow // 把异常对象重新抛出给onlyMe()方法的调用者
18 return // 方法正常返回
Exception table:
From To Target Type
4 10 13 any
13 16 13 any
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有其对应的monitorexit指令,而无论这个方法是正常结束还是异常结束。编译器会自动产生一个异常处理程序,这个异常处理程序声明可处理所有的异常,它的目的就是用来执行monitorexit指令。
参考资料:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》 周志明 著
原文:https://www.cnblogs.com/JL916/p/12435379.html