学习虚拟线程原理
概述
虚拟线程(VirtualThread)的根基是 Continuation,这个词翻译是延续,我自己的理解在编程语境中是中断恢复的意思。 如果想了解 VirtualThread 直接查看 JDK 源码即可,而 Continuation 的具体实现都在 VM 中。我想深入了解一下 Continuation 在 vm 中的具体实现。
例子
运行结果:
continuation 的 yield 流程
解析 yield 方法的 jdk 实现
根据上面的方法内容得知,关键是这个 doYield 的 native 调用,在此处完成了 continuation 的挂起
解析 native 方法 doYield
虽然 doYield 是个 native 方法,执行的是 Jni 调用,理应在 vm 源码找到对应的 C/C++ 方法。但 vm 为了提高执行效率,将该方法编码成了机器码。生成机器码的方法如下(x86_64):
continuation 的 freeze 流程
从上面的调用可知,yield 方法的会调用 freeze(冻结) 方法,所以 vm 官方是把 yield 要做的事情抽象成了“冻结”的概念。 freeze_entry 的方法内容如下:
下面方法被上面的方法调用
freeze 的 slowPath 流程
之前看过 Continuation 的相关介绍,首次 yield 需要复制完整栈帧到堆内存中,之后由于懒加载策略的存在就不会出现全部复制的情况,所以我认为复制全部栈帧的过程对应的就是 slowPath。优先查看 slow 是因为最好按照执行的先后顺序来查看源码,这样比较容易理解。
关于 start frame
可以先不考虑抢断与否,看下 last_frame()
继续看调用
关于 JavaFrameAnchor 的内容,接着看
recurse freeze 递归冻结
此时有三个分支,分别是 jit优化后的帧,解释执行的帧,优化后的帧。而需要优先分析的帧是解释执行的帧和优化后的帧。
解释执行的帧
由于 recurse_freeze_java_frame 这个方法比较关键,决定了真正的返回值,所以需要重点看下。
接下来看下 finalize_freeze 这个方法,这里涉及到了 StackChunk 这个结构体。 简单来说就是这个类由三部分组成,对象元信息+存储的栈数据+gc数据
上面方法涉及了 frame::metadata_words
StackChunk 结构各部分的赋值解析
由于 continuation 持有 StackChunk 链表的尾部指针,所以 continuation 的主体部分实际是由 StackChunk 构成的,因此主要分析一下 StackChunk 的各字段赋值。
结合 StackChunk 结构体 的内容,可知它由三部分组成:
基础部分
栈帧数据
GC数据
基础部分
基础部分-size
记录当前 Chunk 缓存的 stack 大小。
基础部分-sp
先简述一下线程栈的基本信息:
每个栈帧都有自己的起始和结束地址,栈帧的结束位置一般成为栈顶(或者栈帧顶)。
而 SP 寄存器被用作栈顶寄存器,记录栈顶的地址。
还有个 BP 寄存器,也和栈帧有关,作用是栈帧的基址寄存器(也就是栈帧的起始位置)。
而 BP 寄存器的值也可以通过 SP + frame_size 计算出来
而 cont 选择的是存储 SP 和 frame_size
由于存在线程和 cont 两个概念,所有统一一下下文的称呼:
线程栈:完整的线程执行栈帧,涵盖了 cont 的栈帧。线程栈 > cont 栈。
cont 栈:cont 开始执行时,会在线程栈上产生一个 cont 帧序列。
cont 栈底:开始执行 cont 时的第一个栈帧。
cont 栈顶:当前正在执行的方法,既是 cont 栈顶,也是线程栈顶。
frame 帧基址:栈帧的起始地址
由于冻结方法会 cont 栈转移堆中存储。那么将 sp 寄存器的值原样存到堆中就有问题,只能记录 sp 的偏移值。而偏移值只能是相对于 cont 栈底帧的基址的偏移。
接下来看下实际赋值的位置是否符合预期。