分类 Java 相关 下的文章

关于 Java 应用的 线程栈内存大小

对于 Java 应用, 对于内存的占用, 主要是Java堆, 通常我们为了性能考虑, 会对堆设置最大值, 否则 JVM 就会根据它锁了解的系统参数, 设置一个可能的最大值. 除了堆之外, 其实还有好几部分, 也可能占用不少内存. 比如元数据区(老年代), 线程栈, 编译后的代码, GC 程序, 编译器, JVM符号表之类的. 这里可以通过添加启动参数: -XX:NativeMemoryTracking=summary or -XX:NativeMemoryTracking=detail 来追踪原生内存的占用. 比如:
nmt.png

比如上面的截图中, kiyomi 看到 Thread 部分 reserve 了 1052M, 已经提交占用了 1052M, 查看 thread dump, 发现现在大概有 1003 个线程, 基本每个线程占用 1M.

另外我们知道, 可以使用 Xss 参数设置每个线程栈的大小, 比如:
-Xss1m
-Xss1024k
-Xss1048576
上面的参数等同于:
-XX:ThreadStackSize

在 Linux 64 bit 上, 默认是 1MB. 从 Oracle 的官方文档: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
可以看到具体的说明.

从上面的文档可以看出, 每个栈在 64bit 的 Linux 上使用 1M, 可是我从另外一个机器上看到大概有 20K 个线程, 可是占用的内存并不是 20G. 于是我们看到 对于 IBM JDK 和 OpenJ9 JDK 它们都用 3 个参数:

  1. -Xiss<size> Set initial Java thread stack size 2 KB
  2. -Xss<size> Set maximum Java thread stack size 320 KB (31/32-bit); 1024 KB (64-bit)
  3. -Xssi<size> Set Java thread stack size increment 16 KB
    这里它们对 Xss 的解读是最大的可用栈大小. 所以, 就有个疑问, 对于 HotSpot JDK, 是每个栈必须占用 1M, 还是这也是最大值???

更新:
根据这个 Bug 描述: https://bugs.openjdk.java.net/browse/JDK-8191369
应该是最大值, 之前的 NMT 简单的把 reserved 和 commited 置成 线程数 * Xss. 其实真正的 committed 可能并么有这么多, 虽然声明使用 1M 虚拟内存, 可能实际使用的物理内存页比较少. 所以 commited 比较少.

比如下面这个例子, 该 JVM 在一个 container 里面, 设置的最大可用内存是 16G, 已经使用 13.58G. 当前线程数是 11266 个, 堆大小设置的是 8G, 可以看到 Thread 通过 NMT 显示的是大概 10G, 总的 commit 已经大约 20G. 其实这个 Thread 占用的都是根据线程数 * Xss 算出来的, 都是根据最大值算出来的, 所以其实最后算出的 commit 是不准的. 根据 top 可得到该进程的 RSS 大概是 10G. 所以可以推算 Thread 占用少于 2 个 G.

nmt1.png

修复之后的版本:
newNMT.png

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 在某个时间段过热.