代码编译的结果从本地机器码转变为字节码
- 概述
- 无关性的基石
- Class类文件的结构
- 常量池
- 字节码指令简介
- 公有设计,私有实现
- Class文件结构的发展
概述
虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,把我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式
无关性的基石
与平台无关的理想最终只有是现在操作系统以上的应用层:Oracle公司以及其他虚拟机发行商发布过许多可以运行在各种不同硬件平台和操作系统上的Java虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的“一次编写,到处运行”
各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式——**字节码(Byte Code)**是构成平台无关性的基石
Java虚拟机
第一版《Java虚拟机规范》中就承诺会对Java虚拟机进行适当的扩展,以便更好地支持其他语言运行于Java虚拟机之上
实现语言无关性的基础仍然是虚拟机和字节码存储格式。
作为一个通用的、与机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为他们语言的运行基础,以Class文件作为他们产品的交付媒介
虚拟机丝毫不关心Class的来源是什么语言,其与程序语言之间的关系如图
Java语言中的各种语法、关键字、常量变量和运算符号的语义最终都会由多条字节码指令组合来表达,决定了字节码指令所能提供的语言描述能力必须比Java语言本身更加强大才行
先偷个图,原文地址,有些内容已经跟不上时代了
https://www.cnblogs.com/chenyangyao/p/5240079.html
Class类文件的结构
Java技术能够一直保持着非常良好的向后兼容性,Class文件结构的稳定性非常重要。《Java虚拟机规范》对Class文件格式进行了几次更新,但基本上知识在原有结构基础上新增内容、扩充功能,并未对已定义的内容做出修改
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的比要数据,没有空隙存在
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据
该结构中只有2种类型:无符号数、表
无符号数
- 结构
属于基本的数据类型 - 描述作用
以u1、u2、u4、u8来分别代表1、2、4、8字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
表
- 结构
由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表都已"_info"结尾 - 描述作用
表用于描述有层次关系的符合结构的数据,整个Class文件本质上也可视作是一张表,这张表由下图格式构成,其数据项按照严格顺序排列构成
魔数与Class文件的版本
定义
每个Class文件的头4个字节被称为魔数(Magic Number)
作用
唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。
很多文件格式标准中都有使用魔数爱进行身份识别的习惯,如图片格式,GIF或者JPEG等在头文件种存有魔数
**使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。**文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过而且不会引起混淆
编译Class文件
如下所示,将该java文件编译成class文件
/**
* @Author jtao
* @Date 2021/1/24 22:48
* @Description 简单的Java代码
*/
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
以十六进制打开该编译好的class文件
- 魔数:0×CAFEBABY
- 次版本号:0×0000 JDK1.2至JDK12均未使用,固定为0.JDK12之后用于标识“技术预览版”,版本号标识为65535
- 主版本号:0×0037对应十进制55,即为JDK11编译,
常量池
常量池为Class文件里的资源仓库,是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据源之一,是Class文件中第一个出现的表类型数据项目
常量池入口
使用u类型数据代表常量池容量计数值,从1开始计数。0项常量空出来,用于后面
某些指向常量池的所引致的数据在特定情况下需要表达“不引用任何一个常量池项目的含义,可以把索引值设置为0来表示”
- 常量池入口:0×0016,代表十进制22,则代表常量池中由21项常量,索引范围为1~21
常量池存放的两大类型
主要存放字面量(Literal)和符号引用(Symbolic References)
- 字面量
接近Java语言层面的常量概念,如文本字符串、被声明为final的常量值等 - 符号引用:属于编译原理方面的概念
1.被模块导出或者开放的包(Package)
2.类和接口的全限定名(Full Qualified Name)
3.字段的名称和描述符(Descriptor)
4.方法的名称和描述符
5.方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
6.动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建或运行时解析、翻译到具体的内存地址中
常量池中每一项常量都是一个表,截至JDK13,常量表中分别有17种不同类型的常量。
该17类表结构起始的第一位是一个u1类型的标志位(tag),代表着当前常量属于哪种常量类型
常量池是很繁琐的数据,17种常量类型各自有着完全独立的数据结构,两两之间没有共性和联系
常量池常量
0A:代表10则为CONSTANT_Methodref_info 类中方法的符号引用,其有1个u1的tag,2个u2的index。则该常量全十六进制为0A00040012
09:为CONSTAN_Fieldref_info,字段的符号引用,其有1个u1 tag,2个u2 inex。则该常量全十六进制为0900030012
以此类推...
JDK分析字节码工具:javap
Orcale的JDK有专门用于分析Class文件字节码工具javap
用法: javap <options> <classes>
其中, 可能的选项包括:
-help --help -? 输出此用法消息
-version 版本信息
-v -verbose 输出附加信息
-l 输出行号和本地变量表
-public 仅显示公共类和成员
-protected 显示受保护的/公共类和成员
-package 显示程序包/受保护的/公共类
和成员 (默认)
-p -private 显示所有类和成员
-c 对代码进行反汇编
-s 输出内部类型签名
-sysinfo 显示正在处理的类的
系统信息 (路径, 大小, 日期, MD5 散列)
-constants 显示静态最终常量
-classpath <path> 指定查找用户类文件的位置
-cp <path> 指定查找用户类文件的位置
-bootclasspath <path> 覆盖引导类文件的位置
执行后可得出如下图,和上述分析完全相同
编译器自动生成常量
如上图括号内,其为编译器自动生成,会被字段表、放发表、属性表所引用。被用来描述一些不方便使用“固定字节”进行表达的内容,如方法的返回值是什么、有几个参数、每个参数的类型是什么
访问标志
这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果为类,是否为final;等含义
如下图所示,标记开始为访问标志
- 访问标志:0×0021为public,且在JDK1.2之后的编译器编译,符合
类索引、父索引与接口索引
-
类索引(this_class)和父索引(super_class)
一个u2类型的数据 -
接口索引(interfaces)
一组u2类型的数据集合 -
Class文件由这三项数据来确定该类型的继承关系
1.类索引用于确定这个类的全限定名
2.父索引用于确定这个类的父类全限定名。除Object外,所有Java类都有父类,因此除了Object外,所有Java类的父类索引都不为0
3.接口索引集合用来描述这个类实现了那些接口,按implements(如果Class为接口,则关键字为extends)关键字后的接口顺序从左到右排列在接口索引集合中
上图的索引为000300040000,则表示,类索引为3,父类索引为4,接口索引集合大小为0
对应字节码文件符合
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。
字段包括类级变量以及示例级变量,但不包括在方法内部声明的局部变量
- 字段可以包括的修饰符有
1.字段的作用域 public、private、protected
2.是示例变量还是类变量 static
3.可变性 final
4.并发可见性 volatile、是否强制主从内存毒刺额
5.可否被序列化 transient
6.字段数据类型 基本类型、对象、数组
7.字段名称
各个修饰符都是boolean值,要么有,要么没有,适合使用标位置来表示。字段名字、定义数据类型无法固定,必须引用常量池中的常量来描述
全限定名
将类全名中的"."替换成"/"
为了使用多个连续的全限定名不产生会混淆,在最后会加入";"表示全限定名结束
简单名称
没有类型和参数修饰的方法或者字段名称
描述符
用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
- 字段容量计数器:0×0001 代表这个类只有一个字段表数据
- filed_info:
1.access_flags(字段修饰符):0×0002 则该字段为private修饰为true,其他修饰符为false
2.name_index(字段名索引):0×0005 则其常量索引为5,为utf-8的m
3.descriptor_index(字段描述符索引):0×0006,指向字符串"I"
则可以推断该字段为private int m;
字段属性表
之后跟随着一个属性表集合,用于存储一些额外的信息,字段表可以在属性表中附加描述0或多项的额外星系
0×0000 则表示没有字段属性表。如果private int m=100;就会存在额外的属性
Java字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但对于Class文件格式来说,只要两个字段的描述符不是完全相同,那字段重名就是合法的
方法表集合
Class中对方法的描述与对字段的描述采用基本一致的方式。但个别标识符因为复方和字段不同而不同
0×0002 代表方法有2个
0×0001 代表这个方法public为true其他修饰符为false
0×0007 代表方法名索引为7,指向常量池
0×0008 描述索引值为8,指向常量池()V
0×0001 属性表计数器为1,该方法属性表集合有1项属性
0×0009 属性名称索引为9,指向常量Code,说明该属性是方法的字节码描述
如果父类方法在子类中没有被重写,放发表集合中就不会出现来自父类的方法信息。但可能会出现由编译器自动添加的方法如类构造器方法和实例构造器方法
同一个Class文件中,两个方法有相同的名称和特征签名,但返回值不同,也可以共存
属性表集合
属性表(attribute_info):Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息
属性表集合的限制较宽松,不要求严格的顺序,《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向疏星表中写入自己定义的属性信息,Java虚拟机运行时会忽略不认识的属性
每一个属性,其名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量
Code属性
方法体代码编译后,变为字节码指令储存在Code属性内
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据
- 0009 指向Code
- 00 00 00 2F 属性值长度为47
- 00 01 栈最大深度为1
- 00 01 存储空间变量槽Solt容量为1
- 00 00 00 05 字节码长度所占空间为5:虚拟机读取到字节码区域的长度后,依照顺序一次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令
- 2A B7 00 01 B1 读入翻译
1.读入2A,查表得对应指令为aload_0,将第0个变量槽中为reference类型的本地变量推送到操作数栈顶
2.读入B7,查表得B7对应指令为invokspecial,以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器、private或父类的方法。该方法有u2类型参数说明具体调用哪一个
3.读入00 01 invokspecial指令的参数,从常量池获得该方法,如下图
4.读入B1 查表得B1对应指令为return,从方法的返回,并且返回值为void,执行后方法正常结束。
可以从中看出它执行过程中的数据交换、方法调用等操作都是基于栈(操作数栈)的
- 除了常量池外的其他代码
{
public 第六章.Class类文件结构.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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this L第六章/Class类文件结构/TestClass;
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 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this L第六章/Class类文件结构/TestClass;
}
- Locals和Args_size为1原因:
由于this关键字存在,通过this可以访问方法所属的对象,仅仅是通过在Javac编译器编译的适合把对this关键字的访问转变为对一个普通方法参数的访问,让后在JVM调用实例方法时自动传入此参数。因此在实例方法的局部变量表中至少会存在一个指向但概念对象实例的局部变量,局部变量表中也会预留出第一个变量槽位来存放对象实例的引用,所以实例方法参数值从1开始计算
异常表属性
在字节码指令之后的是这个方法的显示异常处理表集合,异常表对于Code属性来说,并不是必须存在,如上述字节码
- 举例
/**
* @Author jtao
* @Date 2021/1/25 17:26
* @Description 异常表运作演示
*/
public class TestException {
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
Classfile /E:/Ideacode/工作workspace/JVM/target/classes/第六章/Class类文件的结构/TestException.class
Last modified 2021-1-25; size 654 bytes
MD5 checksum fc71c786e4f5cbc112c5b74fad0db0c3
Compiled from "TestException.java"
public class 第六章.Class类文件的结构.TestException
SourceFile: "TestException.java"
minor version: 0
major version: 55
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#22 // java/lang/Object."<init>":()V
#2 = Class #23 // java/lang/Exception
#3 = Class #24 // 第六章/Class类文件的结构/TestException
#4 = Class #25 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 L第六章/Class类文件的结构/TestException;
#12 = Utf8 inc
#13 = Utf8 ()I
#14 = Utf8 x
#15 = Utf8 I
#16 = Utf8 e
#17 = Utf8 Ljava/lang/Exception;
#18 = Utf8 StackMapTable
#19 = Class #26 // java/lang/Throwable
#20 = Utf8 SourceFile
#21 = Utf8 TestException.java
#22 = NameAndType #5:#6 // "<init>":()V
#23 = Utf8 java/lang/Exception
#24 = Utf8 第六章/Class类文件的结构/TestException
#25 = Utf8 java/lang/Object
#26 = Utf8 java/lang/Throwable
{
public 第六章.Class类文件的结构.TestException();
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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this L第六章/Class类文件的结构/TestException;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: istore_2
4: iconst_3
5: istore_1
6: iload_2
7: ireturn
8: astore_2
9: iconst_2
10: istore_1
11: iload_1
12: istore_3
13: iconst_3
14: istore_1
15: iload_3
16: ireturn
17: astore 4
19: iconst_3
20: istore_1
21: aload 4
23: athrow
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
LineNumberTable:
line 13: 0
line 14: 2
line 19: 4
line 14: 6
line 15: 8
line 16: 9
line 17: 11
line 19: 13
line 17: 15
line 19: 17
line 20: 21
LocalVariableTable:
Start Length Slot Name Signature
2 6 1 x I
9 8 2 e Ljava/lang/Exception;
11 6 1 x I
0 24 0 this L第六章/Class类文件的结构/TestException;
21 3 1 x I
StackMapTable: number_of_entries = 2
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
}
编译器位这段代码生成了三条异常表记录,对应三条可能出现的代码执行路径
Ecxeptions属性
与Code属性平级,不是Code属性内部的异常表属性
作用是列举出方法中可能抛出的受查异常(Checked Exceptions),即为方法描述时在throws关键字后面列举的异常
LineNumberTable 属性
描述Java源码行号与字节码号码(字节码的偏移量)之间的对应关系
LocalVariableTable及LocalVariableTypeTable属性
-
LocalVariableTable
描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系 -
LocalVariableTypeTable
在JDK5引入泛型后加入,与LocalVariableTable相似
SourceFile及SourceDebugExtension属性
-
SourceFile
记录生成这个Class文件的源码文件名称 -
SourceDebugExtension
JDK5新增,为了方便在编译器和动态生成的Class中加入供程序员使用的自定义内容,用于存储额外的代码调试信息
ConstantValue属性
通知虚拟机自动为静态变量赋值
InnerClass属性
记录内部类与宿主类之间的关联
Deprecated及Synthetic属性
都属于标志类型的布尔值,只存在有和没有的区别,无属性值
- Deprecated
表示某个类、方法、字段已经被程序作者定为不在推荐使用 - Synthetic
表示此字段、方法法并不是由Java源码直接产生的,而是由编译器自行添加的
StackMapTable属性
在JDK6中增加到Class文件规范中,是一个相当复杂的变长属性,位于Code属性的属性表中
Signature属性
JDK5增加到Class文件规范之中,是一个可选的定长属性,可以出现于类、字段表和方法表结构的属性表中
BootstrapMethod属性
在JDK7增加到Class文件规范之中,是一个复杂的变长属性,位于类文件的属性表中
MethodParameters属性
JDK8是加入Class文件格式,用字啊方法表中的边长属性。记录方法的各个形参名称和信息
模块化相关属性
- Moudel属性
非常复杂的变长属性,除了表示该模块的名称、版本、标志信息外,还存储了该模块requires、exorts、opens、uses和provides定义的全部内容
运行时注解相关属性
- RuntimeVisibleAnnotations
变长属性,记录了类、字段或方法的声明上记录运行时可见注解,当我们使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的
字节码指令简介
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。
Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令不包含操作数,只有一个操作码,指令参数都存放在操作数栈中
如果不考虑异常处理,Java虚拟机的解释器可以使用下面的代码作为基本的执行模式来理解
do{
自动计算PC寄存器的值+1;
根据PC寄存器指示的位置,从字节码流中取出操作码;
if(字节码存在操作数) 从字节码流中取出操作数;
执行操作码所定义的操作;
} while (字节码流长度 > 0);
字节码与数据类型
在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息
- 例如
iload指令用于从局部变量表中加载int型的数据到操作数栈中
fload指令加载的是float类型的数据
加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
运算指令
用于对两个操作数栈上的值进行某种特定的运算,并把结果重新存入到操作栈定
类型转换指令
将两种不同的数值类型相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题
对象创建于访问指令
实例和数组都是对象,但JVM对实例和数组的创建与操作使用了不同的字节码指令。
对象创建后,通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素
指令包含
- 创建类实例的指令:new
- 创建数组的指令:newarray、anewarrary、multianewarray
- 访问类字段(static)、实例字段(非static)
- 把一个数组元素元素加载到操作数栈的指令:baload、caload、saload、isload、aload、faload、daload、aaload
- 将一个操作数栈的值存储到数组元素中的指令:bastore、castore...
- 取数组长度的指令:arraylength
- 检查类实例类型的指令:instanceof、checkcast
操作数栈管理命令
直接操作数栈的指令
控制转移指令
可以让JVM有条件或无条件地从指定位置指令的下一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值
方法调用和返回指令
异常处理指令
同步指令
公有设计,私有实现
《Java虚拟机规范》描绘了Java虚拟机应有的共同程序存储格式:Class文件格式以字节码指令集。
一个优秀的JVM实现,在满足规范的约束下对具体实现做出修改和优化是完全可行的,并且《Java虚拟机规范》也鼓励实现者这样做。
##虚拟机的实现方式
- 将输入的Java虚拟机代码在加载时或执行时翻译成另一种虚拟机的指令集
- 将输入的Java虚拟机代码在加载时或执行时翻译成宿主机处理程序的本地指令集(即时编译器代码生成技术)
精确定义的虚拟机行为和目标文件格式,不应当对虚拟机实现者的创造性产生太多的限制,Java虚拟机是被设计成可以允许有众多不同的实现,并且各种实现可以在保持兼容性的同时提供不同的新的、有趣的解决方案
Class文件结构的发展
Class文件结构一致处于一个相对比较稳定的状态,Class文件的主体结构、字节码指令的语义和数量几乎没有出现过变动,所有对Class文件格式的改进,都集中在访问标志、属性表这些设计上原本就是可扩展的数据结构中添加新内容
Class文件格式所具备的平台中立、紧凑、稳定和可扩展的特点,是Java计数体系实现平台无关、语言无关两项特性的主要支柱
Comments | 0 条评论