1、基本概念
1、JIT
一般情况下,jvm 以解释的方式来执行 class 文件中的字节码,但当一段代码被频繁调用时,更好的做法是将其对应的字节码再次编译生成平台对应的机器码,这样就不需要每次都解释了,此之谓 JIT。
虚拟机执行代码的两种方式:
- 加载并执行 java 程序对应的 jvm 指令字节码;
- 加载 jvm 指令字节码,并将其翻译成宿主机 CPU 的本地指令集,然后执行,即 JIT;
2、字节码是平台无关的,虚拟机是平台相关的
虚拟机有不同的版本,也就是在安装 jdk 或 jre 时,linux 平台要安装 linux 版本,Windows 要安装 windows 版本,不能乱装。
Java 代码会被编译成 .class 文件,这种字节码文件是平台无关的,也就是说,可以在任意平台的 JVM 上执行,这就是所谓的 一次编译,到处运行。
3、语言与虚拟机
Java 虚拟机不与任何语言绑定,它只关注于 Class 文件,Class 文件中包含了虚拟机的指令集,符号表,以及若干其他辅助信息。
Java 语言中的各种变量、关键字和运算符号的语义最终都可以表示成多条字节码命令的组合,因此字节码命令所能提供的语义描述能力会比 Java 语言本身更加强大。
语言的能力与表现力是不同的概念,汇编语言尽管不够灵活,不适于快速开发,但它无所不能。
4、ELF
ELF 是 Linux 平台中目标文件
和可执行文件
的格式,目标文件中的内容包括编译后的机器指令代码、数据,还包括链接时所需要的一些信息,如符号表、调试信息、字符串等。
一般目标文件将信息按类型,以 Section 的形式存储,有时候也叫 Segment。编译后的机器指令放在代码段,全局变量和局部静态变量数据放在数据段。
class 文件与 elf 文件有相似之处,也有很多不同,可以对比理解。
5、JVM 指令操作码的长度只有一个字节:所以最大指令数目为 256!!!
2、Class 文件结构
Class 文件是一组以 8 位字节为基础单位的二进制流,也就是字节码流,各个数据项目严格按照顺序紧凑地排列在 class 文件中。
Class 文件的结构本质上是一种数据模型,存哪些信息,怎么存,存在哪里。是典型的系统数据或者底层数据建模的模式,跟网络协议中的数据建模方式很像,这类模型文件的特点是紧凑。
平时做业务层数据建模的时候,可以通过类,比如 UML,或者通过 XML Schema,JSON schema,Yaml schema,或 SQL Schema 的形式来建模,模型文件要简洁、易读,易修改。
class 文件结构的定义为给谁用的呢?编译器和加载器。
编译器会根据 class 文件的结构定义将 java 文件转换为 class 文件,而类加载器会根据 class 文件的结构定义读取的 class 文件字节流来创建 Class 实例。
所以,只要编译器与加载器它们俩对 class 文件结构有一致统一的认知,就可以了。而不像在业务建模的时候,你设计出来的业务模型,客户、产品经理、研发经理,甚至程序员,都要看懂,所以对易读性的要求很高,常常需要配合使用图表、图形,比如类图,流程图,交互图等等。
class 文件结构没这么多讲究。
下面是某个 class 文件的字节码信息,开头四个字节 CA FE BA BE
是魔数 ( 上图中的 u4 magic
),所有的 class 文件都以此开头,后面的四个字节 00 00 00 33
是版本( 上图中的 u2 minor_version 和 u2 major_version
), 后面的两个字节 00 17
是常量池的大小( 上图中的 u2 constant_pool_count
),对应十进制数为 23。后面的字节,可以按照上图中的 Class 结构文件,依次类推。
也可以使用 jclasslib.exe 来查看 class 文件内容,或者使用 javap 命令。
access flags 值得详细了解一下:
- final: 不可被继承
- super:使用 invokespecial 调用父类方法时,特殊对待
- interface:接口类
- abstract:不可实例化
- synthetic:该类由编译器生成,没有对应的 java 文件
- annotation:注解类
- enum:枚举类
常量池
Class 文件中的常量池可以理解为资源仓库,主要存放两大类信息:字面量和符号表。
Java 代码在编译的时候,不像 c 和 c++ 那样有链接的过程,Java 实在虚拟机加载 Class 文件的时候进行动态链接。因此,Class 文件中不会保存方法、字段的内存布局信息。在类加载时,虚拟机从 class 文件的常量池中获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
public class Person implements Action {
public String name;
public int age;
public String gender;
{
name = "tom";
age = 18;
}
public Person() {
}
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String say() {
return String.format("name = %s, age = %s", name, age);
}
public void walk() {
System.out.print("walk");
}
}
该 java 文件对应的 class 文件的常量池大小为 62,CONSTANT_Class_info 的数据为 6, CONSTANT_Fieldref_info 的数目为 4。
CONSTANT_Class_info 对应的具体类信息:
- java/lang/Object
- chorus/Person
- chorus/Action
- java/lang/Integer
- java/lang/String
- java/lang/System
- java/io/PrintStream
CONSTANT_Fieldref_info 对应的具体字段信息:
- Person.name
- Person.age
- Person.gender
- System.out
字段表
1、字段包括类变量及实例变量,不包括在方法内部声明的局部变量
2、字段表中不会列出从超类或者父接口中继承而来的字段
但是有可能列出原 Java 代码之中不存在的字段,也就是 SYNTHETIC 字段,比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
方法表
1、方法的代码存放在方法属性表中一个名为 Code 的属性里面
2、<clinit>
与 <init>
如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。
有时,会出现由编译器自动添加的方法,最典型的便是类构造器方法 <clinit>
和示例构造器方法 <init>
。
<clinit>
可以理解为类中的 static{} 块,但并不是所有的 static {} 块都会生成对应的 <clinit>
方法,编译器会做一定程度的优化。
属性表
Java SE 8 的预定义属性为 23 项:
1、Code 属性
Code 属性中存储 Java 方法编译生成的字节码指令信息;
接口或者抽象类中的方法不存在 Code 属性。
Code 属性是 Class 文件中最重要的一个属性,Java 程序中的信息可以分代码和元数据两部分:
- 代码: Code 属性
- 元数据: 类、字段、方法定义以及其他信息;
能够直接阅读字节码是工作中分析 Java 代码语义问题的基本技能。
2、InnerClass 属性
InnerClass 属性用于记录内部类和宿主类之间的关联,如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成 InnerClasses 属性。
3、EnclosingMethod 属性
当且仅当一个类为局部类或者匿名类时,该类才能拥有此属性,这个属性用于标识这个类所在的外围方法。
例如以下这段代码: org.postgresql.Driver
编译器会生成一个匿名类: org.postgresql.Driver$1
3、字节码
1、操作数栈
Java 虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。汇编语言的指令集就是面向寄存器架构的,有很多寄存器相关的指令。
寄存器是 CPU 的重要组成部分,分为指令寄存器(IR),程序计数器(PC),其中指令寄存器又分为:数据寄存器,地址寄存器,段寄存器等等。
2、操作码数目
受限于 Java 虚拟机操作码的长度设定,Java 指令集的操作码总数不超过 256 个。
3、局部变量表和操作数栈
java 虚拟机中的局部变量表和操作数栈,可以理解为汇编语言中的函数栈与寄存器。
4、指令分类
- 加载与存储指令:
- 运算指令:
- 类型转换指令:
- 对象创建与访问指令:
- 操作数栈管理指令:
- 控制转移指令:
- 方法调用与返回指令:
- 异常处理指令
- 同步指令
5、栈帧:Stack Frame
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表中的 Code 属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,仅仅取决于具体的虚拟机实现。
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量:
6、字节码指令中有特定含义的字符:
- i 代表 int:例如
iconst, iload
- l 代表 long:
- s 代表 short
- b 代表 byte: 例如
bipush, sipush
- c 代表 char
- f 代表 float
- d 代表 double
- a 代表 reference
- ia 代表 int 型数组
- fa 代表 float 数组
- da 代表 double 数组
- aa 代表数组的数组
7、iload_<n>
这种形式的指令,比如 iload_0, iload_1, iload_2
,代表某个带有一个操作数的通用指令的特殊形式,对于这若干特殊指令来说,它们省略掉了显示的操作数。
8、ifeq, ifne, iflt, ifge, ifgt, ifle
if
: 表示该指令为条件判断并跳转的指令,eq
: equal 等于lt
: less than 小于gt
: great than 大于ge
: 大于等于le
:小于等于