今天看了个小问题,即日志中出现了 OOM,但是发现没有对应的堆转储生成,导致 Agent 未监控到 OOM,仔细查看相关日志,发现是 Spring 的请求处理逻辑中对 Throwable 进行了 catch 然后包装了相关异常,该组应用使用的 Spring 版本为 5.3.20,相关源码位于 DispatcherServlet.java at v5.3.20:
1 | /** |
即在处理请求的过程中如果出现了 Error,则会被 65-69 行中的 catch (Throwable err)
捕获并包装为 NestedServletException
,其注释也有说明:
As of 4.3, we’re processing Errors thrown from handler methods as well, making them available for @ExceptionHandler methods and other scenarios.
即从 4.3 开始,此处处理了 Error,并将其交给异常处理器或其他场景处理。但是这造成的问题为我们所需的堆转储文件并没生成,比如我们在全局异常处理器中仅进行了简单的处理并打印了日志,则会出现如下输出:
1 | class: org.springframework.web.util.NestedServletException |
即仅打印了 NestedServletException
异常,OOM 仅含有最基础的信息,最重要的 OOM 栈帧并未体现,导致问题难以排查。关于 NestedServletException
并未体现栈帧的问题,在该类的注释中也有说明,源码位于 NestedServletException.java at v5.3.20:
1 | /** |
可知 NestedServletException
继承自 ServletException
,且注释特意提到:
Note that the plain ServletException doesn’t expose its root cause at all, neither in the exception message nor in printed stack traces! While this might be fixed in later Servlet API variants (which even differ per vendor for the same API version), it is not reliably available on Servlet 2.4 (the minimum version required by Spring 3.x), which is why we need to do it ourselves.
即 ServletException
无论是在其异常消息还是打印的栈帧中均未包含原始异常的信息,所以 NestedServletException
覆写了 getMessage
方法并包含了原始异常的信息,注意此处也仅包含了原始异常的信息,而未包含原始异常的栈帧,导致上面提到的 OOM 仅含有基础的提示信息。关于 4.3 引入的这一项改动可参考:@ExceptionHandler is able to process Error thrown from handler method · spring-projects/spring-framework@f6cb30b · GitHub。而关于为何要进行这样的改动,可以参考 Juergen Hoeller 在 SPR-13788 中的回复:
We tend to catch Throwable in quite a few places these days… in particular for close sequences where we’d like to continue with remaining clean-up steps if a particular resource close call failed for any reason.
Even OutOfMemoryError is not unrecoverable in that context, since it may for example happen when the JVM failed to allocate a large array… with enough free memory for further steps remaining afterwards, just with the layout not allowing for as large an array.
Since the log level is ‘error’ there anyway, what specifically did you miss? What would have been the actual benefit of letting the OOME through?
而关于是否 catch Throwable,其实是存在一些争议的,关于该问题的讨论可参考以下链接:
- Is It a Bad Practice to Catch Throwable? | Baeldung
- Why catch Exceptions in Java, when you can catch Throwables? - Stack Overflow
- Catching java.lang.OutOfMemoryError? - Stack Overflow
在当前的 Spring 实现之上,我们临时给出的解决方案如下:
1 | public class GlobalExceptionHandler implements HandlerExceptionResolver { |
即在全局异常处理器中对 NestedServletException
做特殊处理,如果原始异常为 OOM,则直接抛出,以便生成堆转储文件供后续分析,如果是 Error 子类的其他错误,则进行对应日志记录。
Reference
@ExceptionHandler cannot handler java.lang.Error despite the annotation accept ? extends Throwable SPR-11106 · Issue #15732 · spring-projects/spring-framework · GitHub
NoClassDefFoundError