JVM 对空指针的优化
现象
线上有业务出现了空指针,但让人奇怪的是只是打印了
java.lang.NullPointerException
,而没有打印对应的调用堆栈信息,这让我们无法知道这个空指针到底是哪一行代码产生的:
原因
通过网上搜索,发现是 HotSpot JVM 对异常的一个优化,要理解为什么做这个优化,就得了解 JVM 是如何处理异常的
JVM 处理异常的流程
当程序抛出一个异常时,JVM 会创建一个对应的
Exception
对象,这个对象里会保存一份调用堆栈的快照然后 JVM 会从异常发生处开始,顺着堆栈一直往上找,直到找到了一个异常处理代码,通常来说就是
catch
语句找到异常处理代码后,JVM 会进行类型匹配,看看
catch
的类型和抛出的异常类型是否匹配,匹配就由此处语句处理,不匹配则继续往上找。这就是俗称的爬栈如果还找不到处理语句,异常会一直往上抛,直到 JVM 里的运行时系统,然后 JVM 回查抛出异常所在的线程和线程组是否设置了
UncaughtExceptionHandler
,没有就自己处理了从上面流程可以看出,处理异常时 JVM 除了要分配内存创建新对象外,还要爬栈做类型匹配。而为了保证异常堆栈的准确性,每次抛出时异常对象都必须是新建的,如果抛出大量异常还是挺费资源的,所以一般不推荐滥用异常实现流程控制
而对于一些特定的隐式异常,比如
NullPointerException
、ArithmeticException
之类的,如果在同一个地方频繁抛出,HotSpot 的 C2 编译器会自行就决定使用一个叫fast throw
的技术来进行优化这个异常抛出的流程fast throw
技术对于一些频繁抛出的特定异常,HotSpot 会直接抛出一个预先定义好的、类型匹配的异常对象
这个对象的
StackTrace、message
都直接被设置成了空,因此不需要每次抛出都要新建对象,也不需要爬栈。速度非常快,也节省资源但因为堆栈被清空,所以我们只知道异常名,和捕获异常的代码行,不知道异常调用链,连异常发生在代码哪一行也不知道
解决方案
fast throw
优化是默认开启的,如果想要关闭,需要在启动参数里添加-XX:-OmitStackTraceInFastThrow
关联思考
Java 中的异常
try
块里内容应该尽量少,只包含实际抛异常的内容即可。就像本次,由于try
里的内容过多,导致无法通过猜测定位问题,只能重启服务检查型异常使用场景很狭隘,不要随便使用。检查型异常向上抛,是为了让调用者自己灵活处理。想法很好,但实际上大部分检查型异常调用者都处理不了,只能接着往上抛,然后最外层统一包一下。即使处理,也基本上就打个错误日志,然后
return
,这就失去了检查型异常的初衷。现在Spring
等框架代码基本不会抛检查型异常静态异常对象
异常有个特殊地方就是:一定要在发生时创建,才能包含正确的调用栈,在
java.lang.Throwable
的文档里也有强调以前好像有见过做优化,然后把异常对象设置为静态,导致了堆栈固化,从而无法查找问题的案例
java.lang.Throwable
JavaDoc:Typically, these instances are freshly created in the context of the exceptional situation so as to include relevant information (such as stack trace data).
参考资料
版权声明: 本文为 InfoQ 作者【陈德伟】的原创文章。
原文链接:【http://xie.infoq.cn/article/163c7dc0e235839fb8609748b】。文章转载请联系作者。
评论