Eric 发布的文章

Java 调用 native 代码真的有那么慢吗?

今天看到一个应用在测试环境下 Tomcat thread 全忙, 做了一个 CPU profiling 的火焰图, 看到基本所有的 Tomcat 线程都是 Runnable 状态, 并且都在下面 2 行代码.

"DefaultThreadPool-25" daemon prio=10 tid=0x00007f80b0108000 nid=0x2450 runnable [0x00007f8088ac3000]
   java.lang.Thread.State: RUNNABLE
    at sun.reflect.Reflection.getCallerClass(Native Method)
    at java.lang.Class.getConstructor(Class.java:1730)

没有看到死锁, 没有看到其他耗 CPU 的操作. 所以怀疑从 Java stack 到 Native stack 这步非常消耗 CPU. 于是研究了一下大概这种 Native 栈的 CPU 消耗.
从 performance 角度看, 在 Java 里面调用 Native 代码会有以下问题.

  1. 不能对短的方法做 inline;
  2. 要新建一个 native 栈;
  3. 不能对 native 方法做运行时优化;
  4. 要复制参数到 native 栈;

另外从一个 Stack Overflow 的问答中看到 有人测试 Native 代码可能导致 10 倍以上的性能降级, 不过我自己没有测试.

不过对于我这个例子, 我找到对应的生产应用, 并没有发现这种问题, 虽然能看到部分线程在做 thread dump 或者 CPU profiling 火焰图的时候停留在上面的 2 行. 并且生产环境中对应的请求是测试环境的 100 倍左右. 所以, 基本断定这个 Tomcat 线程全忙的问题, 并不是 Native 代码的性能问题导致的.

那么问题出在哪呢? 因为不管是做 Thread dump 还是 CPU 火焰图, 都是 CPU 运行栈的剪影, 并不能反映数据的情况. 真实的情况是: 上面 2 行代码处于一个循环中, 在生产环境中, 这个循环大概 100 以内, 可是测试环境下, 它要循环 5万次以上, 所以在 Thread dump 和 CPU profiling 中看到都是这块在运行.

参考: https://stackoverflow.com/questions/13973035/what-is-the-quantitative-overhead-of-making-a-jni-call

Java NullPointerException 出错栈只有一行

通常情况下, 一个新异常(Throwable, Exception 以及其子类)在创建的时候, 都会把当前线程的所有栈放到 Throwable 的 stackTrace 字段上面. 所以, 当我们打印出错栈的时候, 能完全看到该线程是在那一行出错的, 怎么一层层运行到这行代码的. 可是有时候, 我们却发现打印的栈里面只有一行, 没有整个出错栈. 例子如下:

java.lang.NullPointerException

当然正常情况下, 绝不可能只有一样, 即使从 main 函数运行, 或者整个出错栈在一个 Thread 或 Runnable 代码里面, 也不能只有这么一行出错栈. 那么问题出在哪呢?

host:~ admin$ java  -XX:+PrintFlagsFinal -version | grep OmitStackTraceInFastThrow.
     bool OmitStackTraceInFastThrow                 = true             {product}
openjdk version "1.8.0_221"

基于 Hotspot 的 JVM 有一个flag: OmitStackTraceInFastThrow, 默认它是打开的. 它的意思是: 对于一些 JVM 自带的 Exception, 当很少被创建的时候, 他们都是把全部 stacktrace 都放进去的. 当一个异常经常被创建的时候, 这部分代码就会被执行多次, 超过 JIT 编译的 threshold, 就会被 JIT 编译器编译. 编译的时候, 为了性能考虑, JVM 会把出错栈部分省略成只剩一行, 那么就是我们看到的样子.

原文解释:

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.

所以, 如果出了这种问题, 找不到出错栈, 就去找最早的错误, 里面是有出错栈的. 等到编译之后的代码去运行, 出错栈就被省略.

如果要强制它即使编译之后, 还有出错栈, 那么就需要在 JVM 启动时候, 加上 -XX:+OmitStackTraceInFastThrow 参数.

参考:

  1. https://www.oracle.com/java/technologies/javase/release-notes-introduction.html