StackOverflowError无法靠try-catch捕获,因JVM栈空间耗尽后无法构造异常对象或执行catch逻辑;需通过jstack、-XX:OnError自动dump等手段在崩溃前介入定位,重点排查显式/隐式递归、序列化循环引用及线程上下文中的隐藏引用链。

StackOverflowError 无法靠 try-catch 捕获,堆栈日志常被截断或只显示代理/序列化框架的重复帧——定位必须在崩溃前介入,而不是等它发生后再翻日志。
看堆栈是否规律重复,判断是显式递归还是隐式调用链
规律性重复(比如连续几十行 UserService.findUser → UserService.findUser)说明存在:
- 显式无限递归(缺少终止条件、计数器未更新)
- 构造函数或 toString() / equals() 中意外调用自身
- AOP 代理反复增强同一方法(如 Spring @Transactional 嵌套 + 自定义 MethodInterceptor)
非规律性重复(如 ObjectMapper.writeValueAsString → BeanSerializer.serialize → ArrayList.writeObject → … → 回到同一对象)大概率是:
- Jackson / Fastjson 序列化时遇到循环引用(User ↔ Dept)
- Hibernate 一级缓存中实体双向关联未配置 @JsonIgnore 或 FetchType.LAZY
- Lambda 闭包捕获了大对象,每次调用都压入胖栈帧
加 JVM 参数强制捕获崩溃瞬间的完整线索
别等线上出问题再手动连上去。启动时固定加上:
- -XX:MaxJavaStackTraceDepth=1000:避免默认截断(JDK 默认 -1 实际受限于栈空间)
- -XX:OnError="jstack -l %p > stack_%p.log":一崩就自动 dump 全线程栈,重点看报错线程底部 5~10 行
- -XX:+PrintGCDetails -XX:+PrintConcurrentLocks:辅助排除 GC 线程卡死或锁竞争间接导致的栈膨胀
注意:jstack -l 输出里若看到大量 java.lang.Throwable.getStackTraceElements 连续出现,基本可锁定是异常链循环引用(比如 e.initCause(e))
在 IDE 里用调试器快速验证循环引用
复现路径明确时(如某个接口必现),在抛出异常前下断点,用 Evaluate Expression 检查:
- e.getCause() == e(自引用)
- e.getCause() != null && e.getCause().getCause() == e(两层闭环)
- new HashSet().add(e).add(e.getCause()).size() == 1(任意深度闭环检测)
第三方库包装异常时尤其要小心:Spring 的 ExceptionUtils.unwrapInvocationTargetException、MyBatis 的 BindingException 构造逻辑,在旧版本中存在已知循环引用 bug,建议查对应 issue 列表确认是否升级修复
排查时最容易忽略的点是线程上下文和对象图结构
同一个方法在主线程不报错,但在 ForkJoinPool 或 Netty EventLoop 线程里报 StackOverflowError,往往是因为:
- ThreadLocal 存了大对象(如未关闭的 ObjectMapper 实例),随线程复用不断累积
- 对象图里存在隐藏的引用链(比如 HttpServletRequest → Session → 自定义监听器 → 当前 service 实例)
- 日志框架(Logback/Log4j2)在格式化 MDC 或异常时触发了 toString() 链式调用
这类问题不会出现在单元测试里,必须在线上环境用 jstack 抓真实线程快照,结合业务请求参数反推对象构造路径


















