分类 Java 相关 下的文章

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

诊断由于低效的代码引起的 CPU 突然增高

临近大促, 有同事发现某个应用在某些时候 CPU 的使用率很是怪异, 经常在某个水平上持续一段时间. 如下图:
cpu.png

图形看上去像叠罗汉一样, 看上去还很美观 (以后将会有专门的一篇讲各种奇怪的监控图形). 这里来解释一下为什么会出现这种情况: 其实这个是因为这个机器(虚拟机) 有 4 个 CPU 核心, 一旦有一个 CPU 被使用到 100%, 并且持续一段时间, 就会升到将近 25%, 保持一段直线. 若同时有第二个 CPU 被使用到 100%, 那么就保持到将近 50%, 以此类推, 最高到接近 100%. 同样原因, 如果有一个 CPU 从煎熬中解脱, 那么将掉 25% 左右.

一开始, 某个同事做了一段时间的 CPU profiling, 从 profiling 的结果看, CPU 花费在了这段代码上面:

"DefaultThreadPool-24" daemon prio=10 tid=0x00007fa85040e000 nid=0xfee9 runnable [0x00007fa849d4b000]
   java.lang.Thread.State: RUNNABLE
    at java.util.HashMap.get(HashMap.java:421)
    at java.util.Collections$UnmodifiableMap.get(Collections.java:1357)
    at java.util.AbstractMap.equals(AbstractMap.java:460)
    at java.util.Collections$UnmodifiableMap.equals(Collections.java:1394)
    at java.util.ArrayList.indexOf(ArrayList.java:303)
...  省略 ...

当时推测, 是不是又是并发使用 HashMap 导致的死循环?
仔细分析一下, 其实不是, 如果是死循环, 这个线程将永远不会退出来, 除非线程死掉, 可是我们看到某个机器的 CPU 会自动降下来. 于是他又推测, 是不是 HashMap 里面一个 bucket 里面太多了? 如果一个 bucket 里面太多, HashMap 将会根据元素的增多增加 bucket, 所以这也不是.

另外, 通过使用 htop 查看进程的使用情况, 也能看到其实燃烧 CPU 的线程过段时间就变掉.

那么, 如果某个代码某段时间内疯狂循环, 也能把 CPU 加热. 于是, 我们做了 heap dump 查看是不是这种情况. 从 heap dump 看, 上面 stack trace 里面的 ArrayList 有 95733 个元素, 而每个元素都是 HashMap, 每个 HashMap 大概有 10 个元素.
burnCPU.png

这种情况下, 会不会很耗 CPU 呢? 答案是当然: 首先我们看 ArrayList.indexOf() 方法怎么做? 它会遍历每个元素去调用他们的 equals() 方法. 平均对比元素个数为 N/2.

    public int indexOf(Object o) {
        if (o == null) {
            for (int i = 0; i < size; i++)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = 0; i < size; i++)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

在看 HashMap 的 equals() 方法: 虽然做了很多快速判断, 但是我们的 case 里大概率还是要每个元素的 key 和 value 都做一次 equals() 对比.

public boolean equals(Object o) {
        if (o == this)
            return true;

        if (!(o instanceof Map))
            return false;
        Map<?,?> m = (Map<?,?>) o;
        if (m.size() != size())
            return false;

        try {
            Iterator<Entry<K,V>> i = entrySet().iterator();
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                K key = e.getKey();
                V value = e.getValue();
                if (value == null) {
                    if (!(m.get(key)==null && m.containsKey(key)))
                        return false;
                } else {
                    if (!value.equals(m.get(key)))
                        return false;
                }
            }
        } catch (ClassCastException unused) {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }

        return true;
    }

从上面的代码来看, 由于ArrayList 里大量的数据加上每个元素都是 HashMap,最终要花费大量的CPU 时间去做对比, 导致 CPU 在某个时间段过热.

使用 tcpkill 杀掉 tcp 连接

debian/Ubuntu 系列安装 tcpkill

sudo apt install dsniff -y

redhat/CentOS 系列安装 tcpkill

yum -y install dsniff -y

杀掉连接

sudo tcpkill -i eth0 -9 port 50185
sudo tcpkill ip host 10.168.1.1
sudo tcpkill -9 port 32145

查看连接

ss -t

由于频繁 JAXBContext.newInstance 导致应用程序变慢

最近有个应用开始接受一种新的service请求, 请求是由一个 batch 应用触发, 在短时间内产生大量请求, 导致整个应用多个方面出现问题, 比如平均延迟增加, CPU 几乎 100%, GC overhead 在 60% 上下, tomcat 允许的服务线程数达到上限, error 大量增多, timeout 增多.

从已有的数据看, GC overhead 占用 60% 左右, 说明还有 40% 是其它代码占用的. 并且 GC overhead 最终是由引用代码引起的, 从 GC 日志看, heap 总是可以回收, 只是回收慢一些, 说明如果修改了代码的问题, GC 问题大概率会自动修复. 一般由于 transaction time 增加, 会导致停留在内存的短暂数据增加, 导致 GC overhead 升高. 此时, CMS, G1 等 GC 算法之前积累的经验(何时开始回收的时间点) 不适用新的情况.

通过 async-profiler 做出 CPU 的使用火焰图, 发现占用 CPU 40% 的代码绝大部分都是在处理 javax/xml/bind/JAXBContext.newInstance 的事情. 如下:
JAXBContextNewInstance.png

关于为什么 JAXBContext.newInstance 会导致 CPU 比较高的问题, 其实互联网上已经有很多这样的问题: https://www.ibm.com/support/pages/jaxbcontext-initialization-takes-long-time

引用这篇文章里面的:

JAXB context (javax.xml.bind.JAXBContext) object instantiation is a resource intensive operation. JAXB Context instantiation involves the pre-load and pre-creation of contexts (called the pre-caching process) of all packages and classes associated with the context, and then all of the packages and classes which are statically (directly and indirectly) referenced from those. Performance latency will correlate with the number of classes which are passed during JAXB creation during this pre-caching process.

一般对于这种 xxxContext 对象, 都是某个过程, 某个环境只有一个的对象, 对于这个问题当中, 每个请求都尝试创建一个 Request level 的 JAXBContext, 导致应用出现问题.