虚拟机栈不直接持有循环引用对象,但循环引用可能引发无限递归,导致栈帧持续压栈而抛出stackoverflowerror;应通过栈帧调用链识别“a→b→a→b…”等闭环递归模式,结合jstack分析、代码审查(如tostring重写、json序列化、树遍历)及调试时观察局部变量对象关系来定位。

虚拟机栈本身不直接“持有”循环引用的对象,但循环引用可能在方法调用链中触发无限递归,最终表现为栈帧持续压栈、栈深度异常增长,从而引发 StackOverflowError。识别这类问题,关键不是在栈里找“对象间的循环引用”,而是通过栈帧调用链发现**逻辑上构成闭环的递归调用模式**。
看栈帧调用链是否出现重复方法嵌套
当 JVM 抛出 StackOverflowError 时,实际堆栈 trace 中常出现同一组方法反复交替出现,例如:
-
toJson(Emp)→toJson(Dept)→toJson(Emp)→toJson(Dept)→ … -
processNode(Node)→traverse(Node)→processNode(Node)→ …
这种“A→B→A→B…”或“A→A→A…”的调用序列,就是循环调用的直接证据。它往往源于对象模型设计(如 Emp ↔ Dept 双向引用)+ 序列化/遍历逻辑未设访问标记或终止条件。
用 jstack 定位高深度栈帧中的重复模式
线程未崩溃但 CPU 持续飙高时,可主动诊断:
- 用
top -H -p <pid></pid>找到占用 CPU 最高的线程 ID(十进制) - 转成十六进制(如
printf "%x\n" 12345),得到 tid - 执行
jstack <pid> | grep -A 20 "nid=0x<tid>"</tid></pid> - 观察该线程栈最顶部连续几十行:是否出现相同方法名密集堆叠?是否有明显递归入口(如
serialize()、toString()、hashCode())反复出现?
结合代码检查易触发循环调用的典型场景
以下写法极易导致栈溢出,需重点审查:
- 重写了
toString()、hashCode()或equals(),但在实现中直接打印/计算了双向关联对象(如dept.getEmps()和emp.getDept()互相调用) - JSON 库(如 Jackson、Fastjson)默认开启循环引用检测失败后 fallback 到深度递归,尤其在未配置
@JsonManagedReference/@JsonBackReference或@JsonIgnore时 - 自定义树/图遍历方法,未使用 visited 集合或深度限制,且子节点又反向引用父节点
调试时关注栈帧局部变量里的对象关系
在 IDE 调试模式下(如 IntelliJ),展开当前线程的 Stack Frames,逐层查看每个栈帧的 Variables:
- 若发现相邻栈帧中,局部变量所引用的对象彼此持有对方(例如 frame1 中
emp.dept == frame2 中 dept,而dept.emp == frame1 中 emp),即构成对象级循环 - 此时再回溯调用路径:是否正处在某个无终止条件的递归序列中?比如
emp.toJson()调dept.toJson(),后者又调回emp.toJson()











