TOC
Open TOC
笔记
这里是软工一的笔记
https://docs.oracle.com/javase/specs/index.html
https://blog.csdn.net/luanlouis/category_9263262.html
https://doocs.github.io/jvm/
JVM 与字节码
Java 代码编译成字节码
字节码在虚拟机中执行
Java 虚拟机提供的语言无关性
JVM Run-Time Data Areas
- The PC Register
- Java Virtual Machine Stacks
- Heap
- Method Area
- Run-Time Constant Pool
- Native Method Stacks
https://blog.jamesdbloom.com/JVMInternals.html
https://www.cnblogs.com/czwbig/p/11127124.html
虚拟机对象探秘
对象的内存布局
https://www.cnblogs.com/zhengbin/p/6490953.html
对象的访问定位
访问数据
实例数据值(对象中各个实例字段的数据)
对象类型数据(对象类型、父类、实现的接口、方法等)
访问方式
句柄访问方式
直接指针访问方式
垃圾回收
https://www.cnblogs.com/czwbig/p/11127159.html
当没有引用类型变量指向某个对象在堆中所占用的内存空间时,该对象将无法再被访问,Java 中将其视为垃圾
垃圾回收申请
垃圾回收机制
垃圾回收器要负责完成 3 件任务
- 分配内存
- 确保被引用的对象的内存不被错误回收
- 回收不再被引用的对象的内存空间
一般情况下,当垃圾回收器在进行回收操作的时候,整个应用的执行是被暂时中止 (stop-the-world) 的
垃圾回收机制最基本的做法是分代回收。内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。一般的实现是划分成 3 个世代
对于不同的世代可以使用不同的垃圾回收算法
- 年轻世代的内存区域被进一步划分成伊甸园 (Eden) 和两个存活区 (survivor space)
- 而对于年老和永久世代的内存区域,则采用的是不同的回收算法,称为标记 - 清除 - 压缩 (Mark-Sweep-Compact)
相关话题
https://www.jianshu.com/p/6060cc53aca7
类文件结构
https://louluan.blog.csdn.net/article/details/39892027
字节码指令集
https://en.wikipedia.org/wiki/Java_bytecode
虚拟机类加载
https://louluan.blog.csdn.net/article/details/50529868
https://www.cnblogs.com/czwbig/p/11127222.html
虚拟机字节码执行
栈帧结构
Java 指令与字节码
https://blog.jamesdbloom.com/JavaCodeToByteCode_PartOne.html
示例
基础
- iconst_1(bipush 100):将整数 1 压入栈顶
- istore_1:将栈顶整数值存入局部变量表的 slot1(slot0 是参数 this)
- iload_1:将 slot1 压入栈顶
- 区分 return 和 ireturn
方法调用
实验攻略 旧
相关资源
https://minguw.gitbook.io/jvm/
https://github.com/wym0120/JVVM
Lab 4
ClassFileReader::readClassFile
顺序
Lab 5
DCMPG 和 DCMPL 中先比较是否相等(浮点数)
long → int
写成如下形式则报错
另外不要混用 >>>
和 >>
https://stackoverflow.com/questions/16763917/what-is-the-purpose-of-the-unsigned-right-shift-operator-in-java
int → long
勿忘显式类型转换
Lab 6
TODO: doc
实验攻略 新
相关资源
https://amnore.github.io/VJVM/
Lab 1
overview
一堆花里胡哨的注解……
主要关注 Main.java
中的 Dump
类
- 构造
VMContext
类
- 其中构造了 bootstrapLoader 和 userLoader
- 需要从 searchPaths 中构造
ClassSearchPath
类,提供 findClass
方法得到 InputStream
- 调用
loadClass
,其中实现了双亲委托加载机制 (Parent-First)
- 通过
findClass
方法得到 InputStream
- 调用
JClass
类的构造函数,其中解析了 class 文件
- dump 得到的
JClass
对象
class descriptor
考虑 loadClass
方法的参数 descriptor
形式为 Llab2/HelloWorld;
而对应需要寻找的类文件名称为 lab2/HelloWorld.class
所以实际上有关系
searchPaths
关注 bootstrapLoader
的 searchPaths
需要判断是否存在
dump
查看 class 文件的内容
dump 实际上实现了 javap
命令的部分功能
Lab 2
overview
运行第一个 Java 程序来分析一下执行过程
传递参数
此处为
构造 VMContext
此处 userClassPath
即为 /home/vgalaxy/Desktop/jvm-2022/testdata/build
构造线程 JThread
,实际上就是一个 JFrame
的栈
加载 entryClass
,此处即为 lab2.HelloWorld
找到对应的 main
方法
然后 interpreter 开始工作
构造初始 JFrame
在进行函数调用时,参数首先从操作数栈的栈顶弹出,然后被复制进被调用方法栈帧的局部变量表中
此处局部变量表为 vars
然后线程开始运行
弹出顶部的 JFrame
根据程序计数器和栈帧的方法构造对应的指令对象
然后调用每个指令对象的 run
方法即可
runtime env
https://doocs.github.io/jvm/01-jvm-memory-structure.html
terminal
在终端中运行
记得切换为 jdk8
slots
需要注意操作数栈对应 OperandStack
类,其中包含了 Slots
而局部变量表直接对应 Slots
类
使用如下方式进行模拟
于是 LONG
和 DOUBLE
会有一个 slot 是空的
另外不确定操作数栈在 popSlots
时是否需要逆向拷贝
因为 copyTo
只是正向拷贝
发现了 commit 记录
https://github.com/amnore/VJVM-public/commit/d78d33b2e54d720f18a8097cdf2bec5c2295fdf0
method descriptor
考虑 method 的 descriptor
先参数后返回值
例如 void main(String[] args)
的 descriptor 为
具体参考 MethodDescriptors
类和 Descriptors
类
IO
使用了 native 方法
debugger
加入 -d
选项来启动调试器
如
于是可以观察到对应的指令序列
简单实现断点功能后,可以这样打断点
源代码级调试器
- Code 属性的 LineNumberTable 属性
指令与源代码行的映射
- Code 属性的 LocalVariableTable 属性
名称到局部变量表位置的映射
more opcode
https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-7.html
NOP.java
XCONST_Y.java
XPUSH.java
LDCX.java
https://www.zhihu.com/question/296143618
生成新的 class 文件用于测试
后来发现有测试用例 Conv.java
局部变量表 → 操作数栈
XLOAD.java
操作数栈 → 局部变量表
XSTORE.java
生成新的 class 文件用于测试
POPX.java
DUPX.java
DUPX_XY.java
SWAP.java
IINC.java
LIOPR.java
XNEG.java
XOPR.java
此时可以通过本地测试 Add.java
和 Arith.java
X2Y.java
IFCOND.java
IF_XCMPCOND.java
LCMP.java
XCMPCOND.java
GOTO.java
XRETURN.java
此时可以通过全部本地测试
note
在实现指令的过程中有如下想法
OperandStack
在 pop
时需要将对应的 Slots
清空
否则 Slots
中对类型的检查可能会失败
opcode 占一个字节
offset 占两个字节
而 offset 是相对于 opcode 而言的
所以译码完成后,相对于跳转指令的 opcode 有 3 字节的偏移
显式分派
根据指令名称或者额外的枚举信息对同类指令进行不同的处理
主要体现在 OperandStack
的 push 和 pop 方法对不同类型的处理
典型的例子是 XCMPCOND
和 X2Y
隐式分派
通过某种方式直接保存方法的类型信息
框架代码 XCONST_Y
和 XRETURN
是很好的范本
这里的关键是,非静态方法的第一个参数实际上是 this 对象
所以这里函数式接口的描述符分别为
XOPR
的实现便完全采用了这种技术
需要注意,这种技术并不是通用的
例如 X2Y
最初的实现采用了隐式分派,然而对于装箱后的基本类型,其强制类型转换是不允许的,遂仍采用显式分派
手册中描述了多种情形
其实只要全部视为一个 slot,保持相对顺序即可
Slots
类内部会对类型进行检查
对于手册中指令细节的描述,例如算术运算或类型转换的复杂性,其实根本不必细究
因为只要在代码中使用对应的运算符,编译器就会自动将其翻译为对应的指令,然后真实的 JVM 就会执行这些指令
VJVM 只是套了层皮……
todo