关于java:JVM优化过头了直接把异常信息优化没了

42次阅读

共计 4392 个字符,预计需要花费 11 分钟才能阅读完成。

你好呀,我是 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 大诚不欺我。

对于分层编译,做这样的一个简略的介绍。

学识很大,你要是有趣味能够去钻研钻研。

以上。

正文完
 0