你好呀,我是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(); } } }}
来,就这代码,你猜猜写出个什么花儿来?
当然了,有猜到的敌人,也有没猜到的敌人。
很好,那么请猜出来了的同学迅速拉到文末,实现一键三连的工作后,就能够进来了。
没有猜出来的同学,我把代码一跑起来,你就晓得我要说啥了:
一瞬间的事儿,瞅见了吗?神奇吗?产生疑难了吗?
没关系,你要没看清楚,我还能给你截个图:
在抛出肯定次数的空指针异样后,异样堆栈没了。
这就是我题目说的:太扯了吧?异样信息忽然就没了。
.png)
你说为啥?
为啥?
这事就得从 2004 年讲起了。
那一年,SUN 公司于 9 月 30 日 18 点公布了 JDK 5。
在其 release-notes 中有这样一段话:
https://www.oracle.com/java/t...
次要是框起来的这句话,看不明确没关系,我用我八级半的英语给你翻译一下。
咱们一句句的来:
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...
该网站提供了很多性能,这是其中的几个性能:
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/...
我这里贴一下论断:
对于打消异样的性能耗费,他提出了三个解决方案:
重构你的代码不应用它们。
缓存异样实例。
重写 fillInStackTrace 办法。
通过小日...小日子过的还不错的日本的站点,迷信上网,输出要害信息后,知乎的这个链接排在第二个:
https://www.zhihu.com/questio...
这个问题上面,有一个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/202...
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→4:C2 编译器忙碌。先以 2 级疾速编译,等收集到足够的 Profiling 信息后再转为3级,最终当 C2 不再忙碌时再转到 4 级。
- 0→3→1/0→2→1:2/3级编译后因为办法不太重要转为 1 级。如果 C2 无奈编译也会转到 1 级。
- 0→(3→2)→4:C1 编译器忙碌,编译工作既能够期待 C1 也能够疾速转到 2 级,而后由 2 级转向 4 级。
如果你之前不晓得分层编译这回事,没关系,当初有这样的一个概念就行了。面试不会考的,释怀。
接下来,就要提到一个参数了:
-XX:TieredStopAtLevel=___
看名字你也晓得了,这个参数的作用是让分层编译停在某一层,默认值为 4,也就是到 C2 编译。
那我把该值批改为 3,岂不是就只能用 C1 了,那就不能利用 C2 帮我优化异样啦?
试验一波:
果然如此,R大诚不欺我。
对于分层编译,做这样的一个简略的介绍。
学识很大,你要是有趣味能够去钻研钻研。
以上。