1. 概述
- Java 源代码经过编译生成 class 文件
- 在不同的操作系统上分别实现 JVM, JVM 在不同操作系统上实现差异很大, 如线程, 图形界面等, 由 JVM 屏蔽与操作系统的接口
1.1 Class 文件格式
1.1.1 魔数 & 版本 & 常量池个数
- Magic Number
- 确定这是一个 Java 文件
- Minor / Major Version: 版本号
- 16 进制
- Major Version (0x34) = 52
- 常量池个数 (0x36) = 54
- 大端模式(Big-Endian): 高位在前
00 36
而不是36 00
1.1.2 常量池
0036
代表常量池常量的个数, 后面的 07
通过查表发现含义为 ClassInfo
的 tag
值, 而 name_index
值为 2, 代表类名在第二个常量中.
第二个常量开头为 01
, 查表得知是一个 Utf8
字符串, 0021
代表长度 length
值为 33. 而后面 33 个字节 63 6F 6D 2F 63 6F 64 65 72 69 73 69 6E 67 2F 65 78 61 6D 70 6C 65 2F 45 6D 70 6C 6F 79 65 65 56 31
转换成字符串之后的值为 com/coderising/example/EmployeeV1
1 | CONSTANT_Class_info { |
1 | CONSTANT_Utf8_info { |
1.1.2.1 常量池实例
索引 | 类型 | 操作数 1 | 操作数 2 | 含义 |
---|---|---|---|---|
#1 | ClassInfo | #2 | ||
#2 | Utf8 | org/destiny/jvm/model/Employee | ||
#3 | ClassInfo | #4 | ||
#4 | Utf8 | java/lang/Object | ||
#5 | Utf8 | name | ||
#6 | Utf8 | Ljava/lang/String | ||
#7 | Utf8 | age | ||
#8 | Utf8 | I | ||
#9 | Utf8 | |||
#10 | Utf8 | … | ||
#11 | Utf8 | … | ||
#12 | MethodRef | #3 | #13 | java.lang.Object<init>()V |
#13 | NameAndType | #9 | #14 | <init>()V |
#14 | Utf8 | ()V | ||
#15 | FieldRef | #1 | #16 | org/destiny/jvm/model/Employee 包含一个 Ljava/lang/String 类型的变量 name |
#16 | NameAndType | #5 | #6 | Ljava/lang/String 类型的变量 name |
1.1.3 访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public 类型 |
ACC_FINAL | 0x0002 | 声明为 final 类型 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 声明为接口 |
ACC_ABSTRACT | 0x0400 | Abstract 类型 |
ACC_SYNTHETIC | 0x1000 | 这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 注解 |
ACC_ENUM | 0x4000 | 枚举 |
1.1.4 类索引, 父类索引
类索引和父类索引都是指向常量池的索引
由于 Java 采用动态连接
动态连接是一个将符号引用解析为直接引用的过程。当java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向另一个类的符号引用
那么虚拟机就必须解析这个符号引用。在解析时,虚拟机执行两个基本任务
- 查找被引用的类,(如果必要的话就装载它)
- 将符号引用替换为直接引用,这样当它以后再次遇到相同的引用时,它就可以立即使用这个直接引用,而不必花时间再次解析这个符号引用了。
1.1.5 接口
1.1.6 字段
1 | u2 fields_count; // 字段数量 |
标志字符含义
header 1 | header 2 |
---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
V | void |
L | 对象类型的通用前缀, 如 Ljava/lang/Object |
1.1.7 方法
1 | u2 methods_count; // 方法数量 |
(Ljava/lang/String;)V
表示参数为 String, 返回值为 void 的方法
(Ljava/lang/String;IF)V
表示参数为 String, int, float, 返回值为 void 的方法
1.1.8 属性
- 方法和字段都可能有属性
- 方法中可能有
Code
属性, 字段可能有Constant Value
属性
- 方法中可能有
- 属性中可能嵌套属性
code
属性中还可能有Line Number Table
,Local Variable Table
,Stack Map Table
等属性
- 虚拟机的实现中还可以自定义属性
1.1.8.1 Constant Value
如果某字段为静态类型(access_flag 中包含 ACC_STATIC 标志)
将会被分配 Constant Value 属性
1 | ConstantValue_attribute { |
1.1.8.2 Code
1 | Code_attribute { |
Code 属性中的字节码
字节码 | 命令 | 含义 |
---|---|---|
2A | aload_0 | 从局部变量表第 0 个值压入操作数栈 |
B4 00 15 | getfield #21 | 获取对象的字段值 |
10 1E | bipush 30 | 将 30 压入栈中 |
A2 00 0E | if_icmp_ge 20 | 将当前 |
1.1.8.3 LineNumber
code属性的一个子属性
可选的变长属性, 维护 Java 源代码行号与字节码行号(偏移量之间的对应关系)
1 | LineNumberTable_attribute { |
1.1.8.4 LocalVariableTable 属性
code属性的一个子属性
可选的变长属性, 维护栈帧中局部变量表中变量与 Java 源码中定义变量的关系
1 | LocalVariableTable_attribute { |
1.2 JVM 运行时动态行为
- 线程中包含函数栈帧, 其中每个函数帧表示某一个函数的调用过程
- 在每一个函数帧的内部, JVM 又细分了
局部变量表
,操作数栈
等 - 局部变量和操作数栈中的变量会引用堆中的对象
- 常量池引用指向方法区, 方法区保存了类的元数据以及方法的字节码
1.2.1 实例
Java 源码:
1 | public class Test { |
转换成字节码后:
1 | demo: |
调用 add
函数, 生成新的函数帧
0: aload_1
: 将局部变量表第 1 个变量压入操作数栈;1: aload_2
: 将局部变量表第 2 个变量压入操作数栈;2: iadd
: 将操作数栈顶端的两个元素弹出, 相加并将结果压入栈顶3: istore_3
: 将操作数栈栈顶元素放在局部变量表第 3 个元素中4: return
: 执行完毕
2. ClassLoader
2.1 Java 是动态链接
- C: 编译 -> 链接 -> 生成
.exe
-> 执行- 函数 A 调用函数 B, 在链接时会直接在函数 A 中记录函数 B 的地址
- Java: 编译 ->
.class
-> 装载执行- 类 A 中使用了另一个类 B, 在 A.class 中只保存类 B 的名称, 而不会保留 B 的 “地址”
- 在运行时根据名称来查找类, 装载类
2.2 类加载器的委托模型
工作原理
2.3 类加载器的命名空间
类加载器 + 类名
唯一确定一个类, 只有同一个加载器加载的类才是相同的类.
2.4 验证
2.5 自定义类加载器
1 | public class MyClassLoader extends ClassLoader { |
DefineClass
方法
protected final Class<?> defineClass(String name, byte[] b, int off, int length) throws ClassFormatError;
- 只要传递给该方法一个合法字节数组, 就可以转化成一个 Class 对象, 这就意味着可以从任何地方组装类:
- 磁盘
- zip 文件
- 网络
- 运行时动态生成
3. 常量池
3.1 常见结构
1 | CONSTANT_Class_info { |
3.2 访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public 类型 |
ACC_FINAL | 0x0002 | 声明为 final 类型 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 声明为接口 |
ACC_ABSTRACT | 0x0400 | Abstract 类型 |
ACC_SYNTHETIC | 0x1000 | 这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 注解 |
ACC_ENUM | 0x4000 | 枚举 |
4. 字段 & 方法
4.1 字段
1 | u2 fields_count; // 字段数量 |
可以看到上图中有两个字段, 分别为 String
类型的 name
, 和 int
类型的 age
Name Index
表示常量池中变量名称的索引Desc Index
表示常量池中变量类型的索引
4.2 方法
1 | u2 methods_count; // 方法数量 |
以第一个方法为例:
Name Index
表示方法名称为<init>
, 即构造方法Desc Index
表示方法签名为(Ljava/lang/String;I)V
, 即(String, int):void
4.3 属性
4.3.1 Code 属性
1 | Code_attribute { |
code 属性一般由两个常见的子属性, 分别是:
- LineNumberTable
- LocalVariableTable
4.3.2 LocalLineTable
通过该属性可以完成字节码与 Java 源码的行号映射
可以在 debug 的时候准确找到源码
并且抛出异常的时候堆栈信息可以找到对应行号
1 | LineNumberTable_arrtibute { |
4.3.3 LocalVariableTable
LocalVariableTable 属性建立了方法中的局部变量与源代码中的局部变量之间的对应关系。
每个 LocalVariableTable 的 local_variable_table 部分可以看做是一个数组, 每个数组项是一个叫做local_variable_info的结构, 该结构描述了某个局部变量的变量名和描述符, 还有和源代码的对应关系。
下面讲解 local_variable_info
的各个部分:
start_pc
是当前 local_variable_info 所对应的局部变量的作用域的起始字节码偏移量;length
是当前local_variable_info
所对应的局部变量的作用域的大小。 也就是从字节码偏移量start_pc
到start_pc+length
就是当前局部变量的作用域范围;name_index
指向常量池中的一个CONSTANT_Utf8_info
, 该CONSTANT_Utf8_info
描述了当前局部变量的变量名;descriptor_index
指向常量池中的一个CONSTANT_Utf8_info
, 该CONSTANT_Utf8_info
描述了当前局部变量的描述符;index
描述了在该方法被执行时,当前局部变量在栈中局部变量表中的位置。
由此可知, 方法中的每个局部变量都会对应一个local_variable_info 。
1 | LocalVariableTable_attribute { |
解析以上字节码得到:
start pc | length | slot | name | descript |
---|---|---|---|---|
0 | 15 | 0 | this | Lorg/destiny/jvm/model/Employee |
0 | 15 | 1 | name | Ljava/lang/String |
0 | 15 | 2 | age | I |
在解析 code
属性时需要注意的两点:
code
属性中包含了方法真正的字节码code
属性中包含几个子属性, 包括LineNumberTable
,LocalVariableTable
等, 也需要进行解析.
在 Field
, Method
, Attribute
三者中, 我们可以抽象出如下的关系:
4.3.4 Exceptions
如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来, 并且记录对应的catch{}块中的起始机器指令地址.
当运行时在try块中有异常抛出的话, JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转.
1 | u2 exception_table_length; // 捕获异常表的长度 |
exception_table
记录了该 code 属性中所有显示抛出的异常信心, 包括异常的作用于及类型.
5. 字节码指令
5.1 main 方法字节码
Employee 的 main 方法:
1 | public static final main(String[] args) { |
经过编译后的字节码:
5.1.1 new
new indexbyte1 indexbyte2
- 操作: 创建一个对象
- (indexbyte1 << 8) | indexbyte2 得到一个指向常量池的索引
- BB 00 01 对应
new #1
, 对应的类就是org/destiny/jvm/model/Employee
- 在堆中创建一个新对象
- 将该对象的引用压入栈中
5.1.2 dup
- 操作: 复制操作数栈栈顶的值, 并压入栈中
5.1.3 ldc
ldc index
- 操作: 从运行时常量池中提取数据压入栈中
ldc #43
, 43 在常量池中的值为字符串destiny
5.1.4 bipush
bipush byte
- 将有符号 byte 扩展为一个 int 类型的值 value, 然后将 value 压入到操作数栈中.
- byte 是一个立即数而非常量池引用
5.1.5 invokespecial indexbyte1 indexbyte2
- 操作: 对一个对象进行初始化, 父类的初始化, 调用私有方法(因为没有多态性为)
- (indexbyte1 << 8) | indexbyte2 得到一个指向常量池的索引
invokespecial #45
- 常量池 #45 是一个
methodref
:<init>:(Ljava/lang/String;I)V
- 需要形成新的栈帧
5.1.6 astore_n
- 操作: 将栈顶的
reference
类型数据保存到局部变量表中 - astore_0
- astore_1
- astore_2
- astore_3
5.1.7 aload_n
- 操作: 从局部变量表中加载一个 reference 类型的值到操作数栈中
- aload_0
- aload_1
- aload_2
- aload_3
5.1.8 invokevirtual indexbyte1 indexbyte2
- 操作: 调用实例方法, 依据实例的具体类型进行分派(多态)
- (indexbyte1 << 8) | indexbyte2
invokevirtual #47
=>sayHello: ()V
- 也需要形成新的栈帧
5.1.9 return
- 操作: 方法返回, 从当前函数栈帧退出, 无返回值.
5.2 方法指令
1 | public Employee(String name, int age) { |
5.2.1 aload_0
- 操作: 从局部变量表中加载 index 为 0 的 reference 类型的值到操作数栈中
5.2.2 aload_1
5.2.3 putfield indexbyte1 indexbyte2
- 操作: 给一个对象字段赋值
- (indexbyte1 << 8) | indexbyte2
putfield #15
=>putfield name:Ljava/lang/String
5.3.3 iload_2
- 操作: 从局部变量中把 index 为 2 的 int 类型的值加载到操作数栈中
- reference 类型使用
aload
, int 类型使用iload
5.4 字节码指令的设计实现
使用 命令模式
来抽象该场景, 即将所有字节码指令抽象为命令对象, 基类声明 command
方法, 再根据操作数的不同泛化出不同的抽象子类
6 JVM 执行引擎
6.1 Java 命令
1 | java -cp path1;path2 org.destiny.jvm.Employee |
- cp: classpath(s), 默认是当前路径
- class name: 系统需要找到这个类的 main 方法, 然后执行它的字节码
6.2 执行过程
- 加载类
- 工具:
ClassFileLoader
- 目的地: 方法区
- 工具:
- 获取类的
public static void main(String[] args)
方法- 从方法区寻找
- 执行
main
方法的字节码- 字节码指令
- 栈帧(StackFrame)
- 堆(Heap)
6.3 字节码指令的分类
类型 | 指令 |
---|---|
依次执行 | new bipush ldc dup |
暂停当前栈帧并创建新栈帧 | invokespecial invokevirtual |
跳转到另一行去执行 | if_icmp_ge if_icmple goto |
退出当前栈帧 | return |
7. 垃圾回收机制
7.1 Java 对象的内存布局
- MarkWord: 标注对象的元信息
- GC 年龄
- 锁的标志位
- ClassPointer: 指向方法区的类信息的指针
- InstanceData: 类实例对象的数据
- 方法信息保存在方法区
- padding: 填充
7.2 对象分配和垃圾回收
- 对象优先分配在新生代
- 如果 Eden 区没有足够的空间, 则触发一次 MinorGC
- Java 对象大多具有生命周期短暂的特点, MinorGC 非常频繁, 速度也很快
- 大对象直接进入老年代
- 可以根据参数设置阈值
- 长期存活对象进入老年代
- 每个对象都有一个年龄(age), 在 MarkWord 中
- 如果 age 超过阈值, 则晋升到老年代
- 动态年龄判断
- 如果在 Survivor 空间中相同年龄的所有对象大小的总数和大于 Survivor 空间的一半, 年龄大于或等于该年龄的对象可以直接进入老年代