网站动态好还是静态好,网站开发技术及特点,电子工程师在哪里报名,哪些网站discuz做的你好呀#xff0c;我是why。
你猜这次我又要写个啥没有卵用的知识点呢#xff1f; 不好意思#xff0c;问的稍微有点早了#xff0c;啥提示都没给#xff0c;咋猜呢#xff0c;对吧#xff1f;
先给你上个代码#xff1a;
public class ExceptionTest {public stati…你好呀我是why。
你猜这次我又要写个啥没有卵用的知识点呢 不好意思问的稍微有点早了啥提示都没给咋猜呢对吧
先给你上个代码
public class ExceptionTest {public static void main(String[] args) {String msg null;for (int i 0; i 500000; i) {try {msg.toString();} catch (Exception e) {e.printStackTrace();}}}
}来就这代码你猜猜写出个什么花儿来
当然了有猜到的朋友也有没猜到的朋友。
很好那么请猜出来了的同学迅速拉到文末完成一键三连的任务后就可以出去了。
没有猜出来的同学我把代码一跑起来你就知道我要说啥了 一瞬间的事儿瞅见了吗神奇吗产生疑问了吗 没关系你要没看清楚我还能给你截个图 在抛出一定次数的空指针异常后异常堆栈没了。
这就是我标题说的太扯了吧异常信息突然就没了。 你说为啥
为啥
这事就得从 2004 年讲起了。
那一年SUN 公司于 9 月 30 日 18 点发布了 JDK 5。
在其 release-notes 中有这样一段话 https://www.oracle.com/java/technologies/javase/release-notes-introduction.html 主要是框起来的这句话看不明白没关系我用我八级半的英语给你翻译一下。
我们一句句的来 The compiler in the server VM now provides correct stack backtraces for all “cold” built-in exceptions. 对于所有的内置异常编译器都可以提供正确的异常堆栈的回溯。 For performance purposes, when such an exception is thrown a few times, the method may be recompiled. 出于性能的考虑当一个异常被抛出若干次后该方法可能会被重新编译。重要 After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. 在重新编译之后编译器可能会选择一种更快的策略即不提供异常堆栈跟踪的预分配异常。重要 To disable completely the use of preallocated exceptions, use this new flag: -XX:-OmitStackTraceInFastThrow. 如果要禁止使用预分配的异常请使用这个新参数-XX:-OmitStackTraceInFastThrow。
这几句话先不管理解没有。但是至少知道它这里描述的场景不就是刚刚代码演示的场景吗
它最后提到了一个参数 -XX:-OmitStackTraceInFastThrow二话不说先拿来用了看看效果再说 同样的代码加入该启动参数后异常堆栈确实会从头到尾一直打印。
不知道你感觉到没有加入该启动参数后程序运行时间明显慢了很多。
在我的机器上没加该参数程序运行时间是 2826 ms加上该参数运行时间是 5885 ms。
说明确实是有提升性能的功能。
到底是咋提升的下一节说。
先说个其他的。
这里都提到 JVM 参数了我顺便再分享一个网站 https://club.perfma.com/topic/OmitStackTraceInFastThrow 该网站提供了很多功能这是其中的几个功能 JVM 参数查询功能那必须得有 很好用的你以后遇到不知道是干啥用的 JVM 参数可以在这个网站上查询一下。
到底为啥
前面讲了是出于性能原因从 JDK 5 开始会出现异常堆栈丢失的现象。
那么性能问题到底在哪
来我们一起看一下最常见的空指针异常。
以本文为例看一下异常抛出的时候调用路径 最终会走到这个 native 方法 java.lang.Throwable#fillInStackTrace(int) fill In Stack Trace顾名思义填入堆栈跟踪。
这个方法会去爬堆栈而这个过程就是一个相对比较消耗性能的过程。
为啥比较耗时呢
给你看个比较直观的 这类的异常堆栈才是我们比较常见的这么长的堆栈信息可不消耗性能吗。
现在我们现在再回去看这句话 For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. 出于性能的考虑当一个异常被抛出若干次后该方法可能会被重新编译。在重新编译之后编译器可能会选择一种更快的策略即不提供异常堆栈跟踪的预分配异常。
所以你能明白这个“出于性能的考虑”这句话具体指的就是节约 fillInStackTrace爬堆栈的这个性能消耗。
更加深入一点的研究对比你可以看看这个链接 http://java-performance.info/throwing-an-exception-in-java-is-very-slow 我这里贴一下结论 关于消除异常的性能消耗他提出了三个解决方案 重构你的代码不使用它们。 缓存异常实例。 重写 fillInStackTrace 方法。 通过小日…小日子过的还不错的日本的站点输入关键信息后知乎的这个链接排在第二个 https://www.zhihu.com/question/21405047 这个问题下面有一个R大的回答粘贴给你看看 大家都不约而同的提到了重写 fillInStackTrace 方法这个性能优化小技巧也就是我们可以这样去自定义异常 用一个不严谨的方式测试一下你就看这个意思就行 重写了 fillInStackTrace 方法直接返回 this 的对象比调用了爬栈方法的原始方法快了不是一星半点儿。
其实除了重写 fillInStackTrace 方法之外JDK 7 之后还提供了这样的一个方法 java.lang.Throwable#Throwable(java.lang.String, java.lang.Throwable, boolean, boolean) 可以通过 writableStackTrace 入参来控制是否需要去爬栈。
那么到底什么时候才应该去用这样的一个性能优化手段呢
其实R大的回答里面说的很清楚了 其实我们写业务代码的异常信息打印还是非常有必要的。
但是对于一些追求性能的框架就可以利用这个优势。
比如我在 disruptor 和 kafka 的源码里面都找到了这样的优化落地源码。
先看 disruptor 的 com.lmax.disruptor.AlertException Overridden so the stack trace is not filled in for this exception for performance reasons.由于性能的原因重载后的堆栈跟踪不会被填入这个异常。 再看 kafka 的 org.apache.kafka.common.errors.ApiException avoid the expensive and useless stack trace for api exceptions避免对api异常进行昂贵而无用的堆栈跟踪 而且你注意到了吗上面着两个框架中直接把 synchronized 都干掉了。如果你也打算重写那么也可以分析一下你的场景中是否可以去掉 synchronized性能又可以来一点提升。
另外R大的回答里面还提到了这个优化是 C2 的优化。
我们可以简单的证明一下。
分层编译
前面提到的 C2其实还有一个对应的 C1。这里说的 C1、C2 都是即时编译器。
你要是不熟悉 C1、C2那我换个说法。
C1 其实就是 Client Compiler即客户端编译器特点是编译时间较短但输出代码优化程度较低。
C2 其实就是 Server Compiler即服务端编译器特点是编译耗时长但输出代码优化质量也更高。
大家常常提到的 JVM 帮我们做的很多“激进”的为了提升性能的优化比如内联、快慢速路径分析、窥孔优化包括本文说的“不显示异常堆栈”都是 C2 搞的事情。
多说一句在 JDK 10 的时候呢又推出了 Graal 编译器其目的是为了替代 C2。
至于为什么要替换 C2额原因之一是这样的… http://icyfenix.cn/tricks/2020/graalvm/graal-compiler.html C2 的历史已经非常长了可以追溯到 Cliff Click 大神读博士期间的作品这个由 C 写成的编译器尽管目前依然效果拔群但已经复杂到连 Cliff Click 本人都不愿意继续维护的程度。
你看前面我说的 C1、C1 的特点刚好是互补的。
所以为了在程序启动、响应速度和程序运行效率之间找到一个平衡点在 JDK 6 之后JVM 又支持了一种叫做分层编译的模式。
也是为什么大家会说“Java 代码运行起来会越来越快、Java 代码需要预热”的根本原因和理论支撑。
在这里我引用《深入理解Java虚拟机HotSpot》一书中 7.2.1 小节[分层编译]的内容让大家简单了解一下这是个啥玩意。
首先我们可以使用 -XX:TieredCompilation 开启分层编译它额外引入了四个编译层级。 第 0 级解释执行。第 1 级C1 编译开启所有优化不带 Profiling。Profiling 即剖析。第 2 级C1 编译带调用计数和回边计数的 Profiling 信息受限 Profiling).第 3 级C1 编译带所有Profiling信息完全Profiling).第 4 级C2 编译。 常见的分层编译层级转换路径如下图所示 0→3→4常见层级转换。用 C1 完全编译如果后续方法执行足够频繁再转入 4 级。0→2→3→4C2 编译器繁忙。先以 2 级快速编译等收集到足够的 Profiling 信息后再转为3级最终当 C2 不再繁忙时再转到 4 级。0→3→1/0→2→12/3级编译后因为方法不太重要转为 1 级。如果 C2 无法编译也会转到 1 级。0→(3→2)→4C1 编译器繁忙编译任务既可以等待 C1 也可以快速转到 2 级然后由 2 级转向 4 级。 如果你之前不知道分层编译这回事没关系现在有这样的一个概念就行了。面试不会考的放心。
接下来就要提到一个参数了 -XX:TieredStopAtLevel___ 看名字你也知道了这个参数的作用是让分层编译停在某一层默认值为 4也就是到 C2 编译。
那我把该值修改为 3岂不是就只能用 C1 了那就不能利用 C2 帮我优化异常啦
实验一波 果然如此R大诚不欺我。