栈帧是java方法调用时在虚拟机栈中分配的真实内存块,包含局部变量表、操作数栈、动态链接和返回地址;前两者共享同一片malloc分配的字宽数组但逻辑分离,后两者位于栈帧数据区,通过method*等指针间接关联元信息。

栈帧里存的是局部变量表、操作数栈、动态链接和返回地址
OpenJDK(HotSpot)中每个 Java 方法调用都会生成一个栈帧,它不是抽象概念,而是真实分配在 Java 线程栈上的连续内存块。这个内存块由三部分组成:局部变量表(Local Variable Table)、操作数栈(Operand Stack)、以及栈帧数据区(含动态链接、方法返回地址、异常处理表指针等)。其中前两者被 HotSpot 实现为同一块 malloc 分配的字宽数组,GC 不扫描它们——因为引用是否存活靠 GC Roots 遍历时从寄存器/栈帧 tag 信息判定,而非逐字节解析。
局部变量表怎么区分 int 和 reference 类型
编译期就确定了每个 slot 的类型语义,但运行时 JVM 需要精确识别哪些 slot 存的是对象引用(用于 GC Root 枚举),哪些是基本类型(如 int、long)。HotSpot 提供两种机制:
- 默认模式(
TaggedStackInterpreter=false):依赖 class 文件里的LocalVariableTable属性 + 方法签名 + 字节码控制流分析,在 GC 时重建 slot 类型映射 - 启用
-XX:+TaggedStackInterpreter后:每个局部变量 slot 对应多占一个 tag slot(紧邻),值为frame::Tag::ref或frame::Tag::prim,GC 直接读 tag 判定是否为引用 - 注意:
long和double占两个连续 slot,但只打一个 tag;byte/short/char入栈前转为int,tag 也是prim
操作数栈和局部变量表共享物理内存但逻辑分离
HotSpot 把局部变量表和操作数栈合并成一个以字长(64 位平台为 8 字节)为单位的数组,起始地址由寄存器 r14 指向。但它们的索引方向相反:
- 局部变量表从低地址向高地址增长,索引 0 是第一个参数(
this对非静态方法) - 操作数栈从高地址向低地址“压栈”,实际复用物理栈指针
rsp,push/pop 指令直接操作 CPU 栈 - 所以同一个数组里,
r14+0是局部变量表起点,rsp-8可能是当前操作数栈顶 —— 它们不重叠,但共用一块 malloc 内存
动态链接和返回地址藏在栈帧数据区
这部分不参与字节码运算,但对方法调用链至关重要:
-
_method指针:指向当前Method*对象,用于查找常量池、获取_from_interpreted_entry等入口地址 -
_return_pc:存储调用者方法下一条指令地址,方法 return 时跳转至此 -
_monitor_block_top和_last_sp:用于同步块和异常处理恢复,尤其在解释器与 JIT 切换时保证栈帧可回滚 - 动态链接本身不存字符串或符号,而是通过
_method关联到运行时常量池,实现多态分派
Method*、ConstantPool*、Klass* 等 C++ 对象指针关联;而 GC 扫描栈帧时,既要看 tag(如果启用),也要结合 method 的 signature 和字节码 PC 偏移做保守推断——这正是为什么某些 native 方法或 JNI 调用后,局部变量明明已出作用域,对象却迟迟不被回收。











