2021年1月

关于 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