JVM栈帧深度解析:规范与实战

目录

[JVM 栈帧深度剖析:从规范细节到底层实现与实战溯源](#JVM 栈帧深度剖析:从规范细节到底层实现与实战溯源)

[一、栈帧核心组件的深度解析(基于《Java 虚拟机规范》)](#一、栈帧核心组件的深度解析(基于《Java 虚拟机规范》))

[1. 局部变量表:槽位复用、类型对齐与隐式参数的底层逻辑](#1. 局部变量表:槽位复用、类型对齐与隐式参数的底层逻辑)

(1)槽位(Slot)的本质与复用机制

(2)宽变量(long/double)的特殊处理

(3)隐式参数this的传递逻辑

[2. 操作数栈:类型检查、指令匹配与返回值传递的底层细节](#2. 操作数栈:类型检查、指令匹配与返回值传递的底层细节)

(1)max_stack的确定与栈深度约束

(2)操作数的类型检查机制

(3)方法返回值的传递逻辑

[3. 动态链接:符号引用解析的两种时机与虚方法表(vtable)优化](#3. 动态链接:符号引用解析的两种时机与虚方法表(vtable)优化)

(1)符号引用的结构(运行时常量池中的CONSTANT_Methodref_info)

(2)静态解析(非虚方法):类加载时确定直接引用

(3)动态解析(虚方法):调用时通过虚方法表(vtable)定位

[4. 方法返回地址:异常处理表(Exception Table)的结构与 finally 块的特殊处理](#4. 方法返回地址:异常处理表(Exception Table)的结构与 finally 块的特殊处理)

(1)异常处理表的结构(Code属性中的ExceptionTable)

[(2)finally 块的底层实现:字节码复制(而非单独的栈帧处理)](#(2)finally 块的底层实现:字节码复制(而非单独的栈帧处理))

[二、栈帧与其他 JVM 组件的协同逻辑](#二、栈帧与其他 JVM 组件的协同逻辑)

[1. 与程序计数器(PC)的协同:指令地址的 "记录与恢复"](#1. 与程序计数器(PC)的协同:指令地址的 “记录与恢复”)

[2. 与方法区的协同:运行时常量池与类元信息的依赖](#2. 与方法区的协同:运行时常量池与类元信息的依赖)

[3. 与本地方法栈的协同:native 方法的栈帧差异](#3. 与本地方法栈的协同:native 方法的栈帧差异)

三、特殊场景下的栈帧处理

[1. 栈帧的内存分配方式:连续内存 vs 离散内存](#1. 栈帧的内存分配方式:连续内存 vs 离散内存)

[2. 栈帧压缩(64 位 JVM 的内存优化)](#2. 栈帧压缩(64 位 JVM 的内存优化))

[3. 栈帧调试信息(LineNumberTable 与 LocalVariableTable)](#3. 栈帧调试信息(LineNumberTable 与 LocalVariableTable))

四、实战溯源:通过工具分析栈帧(jstack、javap)

[1. 使用jstack分析栈溢出时的栈帧调用链](#1. 使用jstack分析栈溢出时的栈帧调用链)

[2. 使用javap -v分析栈帧的字节码证据](#2. 使用javap -v分析栈帧的字节码证据)

[五、总结:栈帧是 JVM 方法执行的 "逻辑引擎"](#五、总结:栈帧是 JVM 方法执行的 “逻辑引擎”)


JVM 栈帧深度剖析:从规范细节到底层实现与实战溯源

栈帧作为 Java 虚拟机(JVM)执行 Java 方法的核心载体,其设计与实现严格遵循《Java 虚拟机规范》,但不同 JVM(如 HotSpot、OpenJ9)在具体落地时会有优化。若要 "深入" 理解栈帧,需突破 "结构描述" 的表层,挖掘规范约束、底层交互、特殊场景处理、JVM 优化细节,以及与程序计数器、方法区、本地方法栈的协同逻辑。本文将从 "组件深度解析→跨组件交互→特殊场景处理→实战溯源" 四个维度,彻底拆解栈帧的底层逻辑。

一、栈帧核心组件的深度解析(基于《Java 虚拟机规范》)

栈帧的四大核心组件(局部变量表、操作数栈、动态链接、方法返回地址)并非孤立存在,每个组件都有严格的规范约束和底层实现细节,这些细节直接决定了方法执行的正确性与效率。

1. 局部变量表:槽位复用、类型对齐与隐式参数的底层逻辑

局部变量表是栈帧中最 "静态" 的组件,但其底层存在诸多易被忽略的规范细节,这些细节直接影响 JVM 的字节码校验与内存效率。

(1)槽位(Slot)的本质与复用机制
  • Slot 的内存布局:规范规定每个 Slot 占用 4 字节(32 位),但实际 JVM 会根据平台(32 位 / 64 位)进行优化(如 64 位 HotSpot 中,Slot 仍按 4 字节对齐,避免内存碎片)。
  • 复用的触发条件 :当变量的 "作用域结束"(即字节码中变量的scope属性标记的范围之外),其 Slot 会被后续定义的变量复用。复用的核心目的是减少局部变量表的内存占用,尤其对长方法(局部变量多)效果显著。

字节码证据 :以以下代码为例,通过javap -v查看局部变量表的 Slot 复用:

java 复制代码
public void testSlotReuse() {
    {
        int a = 1; // 作用域:代码块内
        System.out.println(a);
    }
    int b = 2; // 复用a的Slot
    System.out.println(b);
}

编译后的局部变量表(LocalVariableTable属性)如下:

java 复制代码
LocalVariableTable:
  Start  Length  Slot  Name   Signature
     10       8     1     b   I
      2       6     1     a   I  // a的Slot为1,作用域结束后,b复用Slot 1
      0      18     0  this   Lcom/example/StackFrameDemo;

可见,ab共用 Slot 1,JVM 通过Start(变量开始的字节码偏移量)和Length(变量作用域的字节码长度)控制 Slot 的复用范围。

(2)宽变量(long/double)的特殊处理

规范规定:longdouble是 64 位类型,需占用2 个连续的 Slot (称为 "宽变量"),且访问时必须 "原子性操作"(即不能单独访问其中一个 Slot),否则会抛出ClassFormatError

  • 索引对齐规则 :宽变量的起始 Slot 索引必须是 "偶数"(部分 JVM 放宽此约束,但规范建议对齐以提升性能)。例如,long c若从 Slot 2 开始,会占用 Slot 2 和 3,后续变量只能从 Slot 4 开始。
  • 字节码指令差异 :操作宽变量的指令与普通变量不同(如lload加载longdstore存储double),JVM 通过指令类型确保宽变量的原子访问。
(3)隐式参数this的传递逻辑
  • 非静态方法 :规范要求局部变量表的 Slot 0 必须存储this(当前对象的引用),且该参数是 "隐式注入" 的 ------ 开发者未显式声明,但编译器会自动添加到方法的参数列表中。
  • 静态方法 :无this参数,局部变量表的 Slot 0 直接存储第一个显式参数。

字节码证据 :非静态方法public void add(int x)的参数列表,通过javap -v查看:

复制代码
MethodParameters:
  Name                           Flags
  x                              (无flags,显式参数)
  this                           (隐式参数,编译器添加)

局部变量表中,Slot 0 为this,Slot 1 为x,印证了隐式参数的存在。

2. 操作数栈:类型检查、指令匹配与返回值传递的底层细节

操作数栈是栈帧中最 "动态" 的组件,其运行时行为直接受字节码指令控制,且 JVM 通过严格的类型检查确保操作数栈的安全性。

(1)max_stack的确定与栈深度约束
  • max_stack的计算时机 :方法编译为字节码时,编译器会分析方法中所有指令的操作数栈深度需求,取最大值作为max_stack(存储在Code属性中)。例如,a + b * c的指令序列中,操作数栈的最大深度为 2(bc压栈后执行imul,再压入a执行iadd),因此max_stack=2
  • 栈深度溢出 :运行时若操作数栈的深度超过max_stack,JVM 会抛出StackOverflowError(注意:此错误与虚拟机栈大小无关,是操作数栈自身深度超限)。
(2)操作数的类型检查机制

JVM 通过 "字节码校验器"(Bytecode Verifier)确保操作数栈的类型安全,核心规则包括:

  • 指令与类型匹配 :例如iadd(int 加法)指令只能操作两个int类型的操作数,若栈顶是float类型,会抛出VerifyError
  • 操作数数量匹配 :例如invokevirtual(调用虚方法)指令要求操作数栈中压入 "对象引用 + 方法参数",数量必须与方法的参数列表一致,否则校验失败。

实例 :错误代码public void wrongAdd() { int a = 1; float b = 2.0f; int c = a + b; }编译时会报错,本质是编译器提前检测到iadd无法处理int+float,若强行构造字节码绕过编译,JVM 的字节码校验器会拒绝执行。

(3)方法返回值的传递逻辑

返回值的传递依赖 "当前栈帧的操作数栈" 与 "调用者栈帧的操作数栈" 的交互,不同返回类型的指令(ireturnlreturnfreturndreturnareturnreturn)对应不同处理:

  • 基本类型返回 :例如ireturn(int 返回)会将当前栈帧操作数栈顶的int值弹出,压入调用者栈帧的操作数栈顶;
  • 引用类型返回areturn会将对象引用弹出,压入调用者栈帧的操作数栈;
  • 无返回值(void)return指令仅弹出当前栈帧,不传递任何值。

关键细节 :若方法抛出未捕获的异常,返回值传递逻辑会中断,JVM 直接通过异常表定位处理流程,不会执行return系列指令。

3. 动态链接:符号引用解析的两种时机与虚方法表(vtable)优化

动态链接的核心是 "符号引用→直接引用" 的解析,但解析时机并非唯一 ------《Java 虚拟机规范》允许 "静态解析"(类加载时)和 "动态解析"(方法调用时),两种方式对应不同的方法类型(非虚方法 vs 虚方法)。

(1)符号引用的结构(运行时常量池中的CONSTANT_Methodref_info

动态链接依赖的符号引用存储在方法区的运行时常量池中,每个方法引用对应一个CONSTANT_Methodref_info结构,包含三部分:

  • class_index:指向方法所属类的符号引用(如com/example/User);
  • name_and_type_index:指向方法名和参数类型的符号引用(如add:(II)I);
  • tag:标记常量类型(值为 10,代表方法引用)。
(2)静态解析(非虚方法):类加载时确定直接引用

对于 "非虚方法"(不会被重写的方法),JVM 在类加载的 "解析阶段"(链接阶段的子阶段)就将符号引用解析为直接引用,后续调用无需再次解析,提升效率。非虚方法包括:

  • 静态方法(static修饰):属于类,不会被重写;
  • 私有方法(private修饰):仅当前类可见,不会被重写;
  • 构造器(<init>方法):每个类的构造器唯一,不会被重写;
  • final修饰的方法:无法被重写。

实例 :调用Math.abs(-1)(静态方法)时,符号引用在Math类加载的解析阶段已解析为abs方法在方法区的内存地址,后续调用直接使用该地址。

(3)动态解析(虚方法):调用时通过虚方法表(vtable)定位

对于 "虚方法"(可能被重写的方法,如实例方法),JVM 无法在类加载时确定直接引用(因为子类可能重写方法),只能在方法调用时(运行时)动态解析。为避免每次调用都重新查找子类方法,JVM 引入虚方法表(vtable) 优化。

  • vtable 的结构:每个类加载后,JVM 会为其创建一个 vtable,表中存储该类所有虚方法的直接引用,按 "方法签名"(方法名 + 参数类型 + 返回值类型)排序;
  • vtable 的继承规则:子类 vtable 会继承父类 vtable 的所有条目,若子类重写某方法,会替换 vtable 中对应条目的直接引用(指向子类方法);
  • 动态解析流程 :调用虚方法时,JVM 先获取对象的实际类型(通过对象头的klass指针),再查找该类型的 vtable,根据方法签名找到直接引用,执行方法。

实例User u = new Student(); u.getName();getName是虚方法):

  1. 获取u的实际类型(Student);
  2. 查找Student的 vtable,找到getName方法的直接引用;
  3. 执行Student.getName()方法。

vtable 的引入将虚方法调用的 "查找时间" 从 "O (n)"(遍历子类方法)优化为 "O (1)"(直接查表),是 JVM 提升虚方法调用效率的核心手段。

4. 方法返回地址:异常处理表(Exception Table)的结构与 finally 块的特殊处理

方法返回地址不仅包含正常返回的 PC 值,还依赖 "异常处理表" 处理异常返回,而 finally 块的执行逻辑也与异常处理表深度绑定。

(1)异常处理表的结构(Code属性中的ExceptionTable

每个栈帧对应的字节码中都包含一个ExceptionTable(异常处理表),用于记录 "异常类型→处理逻辑" 的映射,表中每个条目包含四个字段:

  • start_pc:异常监控的起始字节码偏移量(从该位置开始监控异常);
  • end_pc:异常监控的结束字节码偏移量(到该位置结束监控,不包含end_pc本身);
  • handler_pc:异常处理逻辑的字节码偏移量(若在start_pc~end_pc间抛出异常,跳转到handler_pc执行);
  • catch_type:捕获的异常类型(指向运行时常量池中的CONSTANT_Class_info,若值为 0,代表捕获所有异常,对应catch (Throwable))。

字节码证据:以下代码的异常处理表:

java 复制代码
public void testException() {
    try {
        int a = 1 / 0;
    } catch (ArithmeticException e) {
        System.out.println("除数为0");
    }
}

编译后的ExceptionTable

复制代码
ExceptionTable:
  from    to  target type
     2     5     8   Class java/lang/ArithmeticException
  • from=2:try 块的起始字节码偏移量(iconst_1指令);
  • to=5:try 块的结束字节码偏移量(idiv指令,不包含to=5);
  • target=8:catch 块的起始字节码偏移量(getstatic指令,打印日志);
  • type=ArithmeticException:捕获的异常类型。
(2)finally 块的底层实现:字节码复制(而非单独的栈帧处理)

finally 块的 "无论正常 / 异常都执行" 特性,并非通过特殊的栈帧机制实现,而是编译器在编译阶段将 finally 块的字节码 "复制" 到两个位置:

  1. 正常返回路径:在return系列指令执行前,插入 finally 块的字节码;
  2. 异常返回路径:在异常处理表的handler_pc执行后,插入 finally 块的字节码。

实例:以下代码的字节码逻辑:

java 复制代码
public int testFinally() {
    try {
        return 1;
    } finally {
        System.out.println("finally执行");
    }
}

编译后的字节码流程:

  1. 执行iconst_1(将 1 压入操作数栈);
  2. 复制 finally 块的字节码(打印日志),插入到return前;
  3. 执行ireturn(返回 1);
  4. 若 try 块抛出异常,异常处理表会定位到 finally 块的字节码,执行后再重新抛出异常。

关键细节 :若 finally 块中包含return指令,会覆盖 try/catch 块的返回值 ------ 因为 finally 块的return指令会先执行,直接弹出当前栈帧,导致 try/catch 块的return指令无法执行。

二、栈帧与其他 JVM 组件的协同逻辑

栈帧并非孤立存在,其创建、执行、销毁过程需与程序计数器(PC)、方法区、本地方法栈等组件紧密交互,这些交互是 JVM 方法执行链的核心。

1. 与程序计数器(PC)的协同:指令地址的 "记录与恢复"

程序计数器(线程私有)的核心作用是 "记录当前线程执行的字节码指令地址",与栈帧的协同体现在两个关键节点:

  • 栈帧创建时:PC 寄存器记录当前调用指令的下一条指令地址(即方法执行完成后需要回到的 "返回地址"),该地址会被存储到新创建栈帧的 "方法返回地址" 中;
  • 栈帧销毁时:当前栈帧弹出后,PC 寄存器会被设置为栈帧中存储的 "返回地址",线程从该地址继续执行调用者的字节码。

实例main()调用add(1,2)的协同流程:

  1. main()中调用add的字节码指令是invokestatic com/example/Math.add:(II)I,其偏移量为0x0010
  2. PC 寄存器记录下一条指令地址(0x0015,即add执行完成后main()需要继续执行的地址);
  3. 创建add的栈帧,将0x0015存入 "方法返回地址";
  4. add执行完成后,弹出栈帧,PC 寄存器被设置为0x0015main()从该地址继续执行。

2. 与方法区的协同:运行时常量池与类元信息的依赖

栈帧的动态链接和方法执行依赖方法区存储的两类信息:

  • 运行时常量池 :提供符号引用(如CONSTANT_Methodref_info),动态链接需从这里获取方法的类、名、参数类型等信息;
  • 类元信息:包括类的 vtable、方法的字节码、异常表等,栈帧执行方法时需从类元信息中读取字节码指令,查找 vtable。

关键交互:调用虚方法时,栈帧的动态链接流程需两次访问方法区:

  1. 从运行时常量池获取CONSTANT_Methodref_info(符号引用);
  2. 根据符号引用中的class_index找到类元信息,获取该类的 vtable,定位直接引用。

3. 与本地方法栈的协同:native 方法的栈帧差异

对于native方法(如System.currentTimeMillis()),其执行依赖 "本地方法栈"(而非 Java 虚拟机栈),对应的 "本地栈帧" 与 Java 栈帧有本质区别:

  • 结构差异:本地栈帧的结构由本地平台(如 Windows、Linux)决定,不遵循《Java 虚拟机规范》的栈帧结构(无局部变量表、操作数栈等);
  • 执行逻辑:本地栈帧调用本地库(如 C/C++ 库)的函数,执行完成后通过 JNI(Java Native Interface)将结果返回给 Java 栈帧;
  • 状态标记 :Java 虚拟机栈中会为native方法创建一个 "占位栈帧"(Placeholder Frame),标记该方法为本地方法,避免虚拟机栈为空时线程被误判为终止。

三、特殊场景下的栈帧处理

1. 栈帧的内存分配方式:连续内存 vs 离散内存

《Java 虚拟机规范》未规定栈帧的内存分配方式,不同 JVM 有不同实现:

  • 连续内存分配:HotSpot 采用此方式,Java 虚拟机栈是一块连续的内存区域,栈帧按 "压栈" 顺序连续分配,弹出时直接释放顶部内存(高效,适合栈帧大小固定的场景);
  • 离散内存分配:部分 JVM(如 JRockit)采用链表存储栈帧,每个栈帧是独立的内存块,通过指针链接,适合栈帧大小动态变化的场景(但效率较低)。

HotSpot 的优化 :为避免栈帧内存溢出,HotSpot 采用 "栈扩展" 机制 ------ 当虚拟机栈剩余内存不足时,尝试向操作系统申请更多内存(而非直接抛出StackOverflowError),若申请失败才抛出错误。

2. 栈帧压缩(64 位 JVM 的内存优化)

在 64 位 JVM 中,对象引用(reference)默认占用 8 字节,会导致局部变量表的 Slot 占用内存翻倍(从 4 字节→8 字节)。为减少内存占用,HotSpot 引入 "压缩指针"(Compressed Oops)优化:

  • 原理 :将 64 位引用压缩为 32 位,通过 "基地址 + 偏移量" 的方式访问对象(基地址存储在CompressedClassSpaceBase寄存器,偏移量为 32 位);
  • 对栈帧的影响 :局部变量表中reference类型的 Slot 仍按 4 字节分配(压缩后),操作数栈中reference的压栈 / 弹栈也按 4 字节处理,大幅减少栈帧的内存占用。

启用方式 :64 位 JVM 默认启用压缩指针(-XX:+UseCompressedOops),禁用时需添加-XX:-UseCompressedOops(仅建议在内存超过 32GB 时禁用)。

3. 栈帧调试信息(LineNumberTable 与 LocalVariableTable)

栈帧的 "附加信息" 中包含两类关键调试信息,这些信息不影响方法执行,但对调试器(如 IDEA Debug)至关重要:

  • LineNumberTable:记录 "字节码偏移量→源码行号" 的映射,调试时通过该表定位源码行(如断点位置);
  • LocalVariableTable:记录 "字节码偏移量→局部变量名、类型、Slot 索引" 的映射,调试时通过该表显示局部变量的名称和值(而非仅显示 Slot 索引)。

关闭调试信息 :编译时可通过javac -g:none关闭调试信息,减少 class 文件大小,但会导致调试时无法查看源码行和局部变量名(仅显示var0var1等匿名变量)。

四、实战溯源:通过工具分析栈帧(jstack、javap)

理解栈帧的最佳方式是结合工具分析实际场景,以下通过两个核心工具展示栈帧的实战应用。

1. 使用jstack分析栈溢出时的栈帧调用链

当程序抛出StackOverflowError时,jstack可捕获当前线程的虚拟机栈信息,展示栈帧的调用链(从栈顶到栈底)。

实例:无限递归导致栈溢出:

java 复制代码
public class StackOverflowDemo {
    public static void recursion() {
        recursion(); // 无限递归
    }
    public static void main(String[] args) {
        recursion();
    }
}

运行后抛出StackOverflowError,使用jstack <PID>查看栈帧:

plaintext

复制代码
"main" #1 prio=5 os_prio=0 cpu=0.00ms elapsed=0.00s tid=0x0000022f3c801000 nid=0x5a8 runnable  [0x000000b8f0dff000]
   java.lang.Thread.State: RUNNABLE
        at com.example.StackOverflowDemo.recursion(StackOverflowDemo.java:3)
        at com.example.StackOverflowDemo.recursion(StackOverflowDemo.java:3)
        at com.example.StackOverflowDemo.recursion(StackOverflowDemo.java:3)
        ... (重复1000+次,展示栈帧调用链)

可见,虚拟机栈中积累了大量recursion方法的栈帧,每个栈帧对应一次递归调用,最终超过虚拟机栈的最大深度(-Xss设置),触发栈溢出。

2. 使用javap -v分析栈帧的字节码证据

javap -v可反编译 class 文件,查看栈帧的max_localsmax_stack、局部变量表、异常表等细节,是分析栈帧底层逻辑的核心工具。

实例 :分析public int add(int x, int y) { return x + y; }的字节码:

复制代码
public int add(int, int);
  descriptor: (II)I
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=2, locals=3, args_size=3  // stack=max_stack=2,locals=max_locals=3
         0: iload_1                  // 加载Slot 1的x到操作数栈
         1: iload_2                  // 加载Slot 2的y到操作数栈
         2: iadd                     // 执行int加法,栈深度从2→1
         3: ireturn                  // 返回栈顶的结果
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       4     0  this   Lcom/example/Math;
          0       4     1     x   I
          0       4     2     y   I
    LineNumberTable:
      line 5: 0
      line 6: 3

通过字节码可清晰看到:

  • max_stack=2(操作数栈的最大深度为 2);
  • max_locals=3(Slot 0 为this,1 为x,2 为y);
  • 操作数栈的压栈、运算、弹栈流程(iload→iadd→ireturn)。

五、总结:栈帧是 JVM 方法执行的 "逻辑引擎"

深入理解栈帧后会发现,它并非简单的 "数据容器",而是 JVM 实现方法执行的 "逻辑引擎"------ 其组件设计、规范约束、与其他 JVM 组件的协同,都围绕 "正确性" 和 "效率" 两大目标:

  • 正确性:通过局部变量表的 Slot 复用规则、操作数栈的类型检查、异常表的异常处理,确保方法执行符合 Java 语法和 JVM 规范;
  • 效率:通过静态解析(非虚方法)、vtable(虚方法)、栈帧连续内存分配、压缩指针等优化,减少内存占用和执行延迟。

对于 Java 开发者而言,深入栈帧的价值不仅在于排查栈溢出、局部变量未初始化等问题,更在于理解 JVM 的底层执行逻辑 ------ 例如,为何虚方法调用比非虚方法慢(动态解析 vs 静态解析)、为何 finally 块的return会覆盖 try 块的返回值(字节码复制逻辑)、为何 64 位 JVM 需要压缩指针(减少栈帧内存占用)。

这些底层细节的积累,是从 "会用 Java" 到 "理解 Java" 的关键一步,也是后续学习 JVM 性能优化、字节码增强(如 ASM、ByteBuddy)、虚拟机原理的基础。

相关推荐
流绪染梦2 小时前
多表联查时处理一对多的信息,将子表字段放入数组
java·数据库
Arva .2 小时前
G1收集器
java·jvm·算法
没有bug.的程序员2 小时前
高并发电商场景:JVM资源规划实战
java·jvm·电商·资源规划
dddaidai1232 小时前
深入JVM(一):对象创建和内存分配
java·jvm
喜欢流萤吖~2 小时前
JSP 内置对象解析:功能、作用域与常用方法
java·开发语言
DKunYu2 小时前
1.Spring-Cloud初识
java·spring cloud·微服务
小坏讲微服务2 小时前
Spring Boot 4.0 + MyBatis-Plus 实战响应式编程的能力实战
java·spring boot·后端·mybatis
by__csdn2 小时前
javascript 性能优化实战:垃圾回收优化
java·开发语言·javascript·jvm·vue.js·性能优化·typescript
BAStriver2 小时前
关于Flowable的使用小结
java·spring boot·spring·flowable